From 4c0b723074287016c9867daa380b3dff0d0cf488 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 4 Mar 2024 16:54:36 -0700 Subject: [PATCH 01/10] zcash_keys: Add missing entries to `zcash_keys-0.1.0` CHANGELOG --- zcash_keys/CHANGELOG.md | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index d77cf19c0..ea7d673a4 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -152,28 +152,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 build client code without `orchard` dependendencies. - `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 -- The following methods and enum variants have been placed behind an `orchard` - feature flag: +- The following methods, method arguments, and enum variants have been placed + 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::keys::DerivationError::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`: - `RecipientAddress` has been renamed to `Address`. - `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`: - `UnifiedSpendingKey::address` now takes an argument that specifies the receivers to be generated in the resulting address. Also, it now returns `Result` instead of `Option` so that we may better report to the user how 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 - `zcash_keys::address::AddressMetadata` From 05b8520fd195d77700d1013c5969adb2bb566881 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 23 Jan 2024 15:04:10 -0700 Subject: [PATCH 02/10] zcash_address: Add handling for Unified Metadata Items --- Cargo.lock | 1 + components/zcash_address/CHANGELOG.md | 31 +- components/zcash_address/Cargo.toml | 1 + components/zcash_address/src/encoding.rs | 11 +- components/zcash_address/src/kind/unified.rs | 353 ++++++++++++++---- .../zcash_address/src/kind/unified/address.rs | 213 ++++++----- .../zcash_address/src/kind/unified/fvk.rs | 107 ++---- .../zcash_address/src/kind/unified/ivk.rs | 109 ++---- components/zcash_address/src/test_vectors.rs | 2 + 9 files changed, 512 insertions(+), 316 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c255e6182..4beeea797 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6180,6 +6180,7 @@ dependencies = [ "core2", "f4jumble", "proptest", + "static_assertions", "zcash_encoding", "zcash_protocol", ] diff --git a/components/zcash_address/CHANGELOG.md b/components/zcash_address/CHANGELOG.md index fd60620bd..14831efa1 100644 --- a/components/zcash_address/CHANGELOG.md +++ b/components/zcash_address/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog + Changelog All notable changes to this library will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), @@ -7,9 +7,35 @@ and this library adheres to Rust's notion of ## [Unreleased] +### Added +- `zcash_address::unified`: + - `Address::receivers` + - `DataTypecode` + - `MetadataTypecode` + - `Item` + - `MetadataItem` +- `impl serde::{Serialize, Deserialize} for zcash_address::ZcashAddress` behind + the `serde` feature flag. + +### 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` arguments have changed. + - The result type of `Container::items_as_parsed` has changed. + - `ParseError::InvalidTypecodeValue` now wraps a `u32` instead of a `u64`. + - `ParseError` has added variant `NotUnderstood`. + ### Deprecated - `zcash_address::Network` (use `zcash_protocol::consensus::NetworkType` instead). +### Removed +- `zcash_address::unified::Container::items` Preference order is only + significant when considering unified address receivers; use + `Address::receivers` instead. +- `zcash_address::kind::unified::address::testing`: + - `{arb_transparent_typecode,, arb_shielded_typecode, arb_typecodes, arb_unified_address_for_typecodes}` + ## [0.6.2] - 2024-12-13 ### Fixed - Migrated to `f4jumble 0.1.1` to fix `no-std` support. @@ -28,8 +54,7 @@ and this library adheres to Rust's notion of ## [0.4.0] - 2024-08-19 ### Added -- `zcash_address::ZcashAddress::{can_receive_memo, can_receive_as, matches_receiver}` -- `zcash_address::unified::Address::{can_receive_memo, has_receiver_of_type, contains_receiver}` +- `zcash_address::ZcashAddress::{has_receiver_of_type, contains_receiver, contains_receiver}` - Module `zcash_address::testing` under the `test-dependencies` feature. - Module `zcash_address::unified::address::testing` under the `test-dependencies` feature. diff --git a/components/zcash_address/Cargo.toml b/components/zcash_address/Cargo.toml index 3aa72295c..5c87453f9 100644 --- a/components/zcash_address/Cargo.toml +++ b/components/zcash_address/Cargo.toml @@ -26,6 +26,7 @@ f4jumble = { version = "0.1.1", path = "../f4jumble", default-features = false, zcash_protocol.workspace = true zcash_encoding.workspace = true proptest = { workspace = true, optional = true } +static_assertions.workspace = true [dev-dependencies] assert_matches.workspace = true diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index b514c21e3..943ba209a 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -186,7 +186,10 @@ mod tests { use assert_matches::assert_matches; use super::*; - use crate::kind::unified; + use crate::{ + kind::unified, + unified::{Item, Receiver}, + }; use zcash_protocol::consensus::NetworkType; fn encoding(encoded: &str, decoded: ZcashAddress) { @@ -237,21 +240,21 @@ mod tests { "u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl", ZcashAddress { net: NetworkType::Main, - kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), + kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])), }, ); encoding( "utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s", ZcashAddress { net: NetworkType::Test, - kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), + kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])), }, ); encoding( "uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3", ZcashAddress { net: NetworkType::Regtest, - kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])), + kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])), }, ); diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index ec5c79e16..dba9654fc 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -1,16 +1,21 @@ //! Implementation of [ZIP 316](https://zips.z.cash/zip-0316) Unified Addresses and Viewing Keys. -use alloc::string::{String, ToString}; -use alloc::vec::Vec; +use alloc::{ + borrow::Cow, + string::{String, ToString}, + vec::Vec, +}; use core::cmp; use core::convert::{TryFrom, TryInto}; use core::fmt; use core::num::TryFromIntError; +use static_assertions::const_assert_eq; #[cfg(feature = "std")] use std::error::Error; use bech32::{primitives::decode::CheckedHrpstring, Bech32m, Checksum, Hrp}; +use zcash_encoding::MAX_COMPACT_SIZE; use zcash_protocol::consensus::NetworkType; @@ -27,9 +32,9 @@ const PADDING_LEN: usize = 16; /// The known Receiver and Viewing Key types. /// /// The typecodes `0xFFFA..=0xFFFF` reserved for experiments are currently not -/// distinguished from unknown values, and will be parsed as [`Typecode::Unknown`]. +/// distinguished from unknown values, and will be parsed as [`DataTypecode::Unknown`]. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Typecode { +pub enum DataTypecode { /// A transparent P2PKH address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316). P2pkh, /// A transparent P2SH address. @@ -44,7 +49,48 @@ pub enum Typecode { Unknown(u32), } -impl Typecode { +impl DataTypecode { + const fn into_u32(self) -> u32 { + match self { + DataTypecode::P2pkh => 0x00, + DataTypecode::P2sh => 0x01, + DataTypecode::Sapling => 0x02, + DataTypecode::Orchard => 0x03, + DataTypecode::Unknown(typecode) => typecode, + } + } +} + +const_assert_eq!( + DataTypecode::P2sh.into_u32(), + DataTypecode::P2pkh.into_u32() + 1 +); + +impl TryFrom for DataTypecode { + type Error = (); + + fn try_from(typecode: u32) -> Result { + match typecode { + 0x00 => Ok(DataTypecode::P2pkh), + 0x01 => Ok(DataTypecode::P2sh), + 0x02 => Ok(DataTypecode::Sapling), + 0x03 => Ok(DataTypecode::Orchard), + 0x04..=0xBF | 0xFD..=MAX_COMPACT_SIZE => Ok(DataTypecode::Unknown(typecode)), + _ => Err(()), + } + } +} + +impl From for u32 { + fn from(t: DataTypecode) -> Self { + DataTypecode::into_u32(t) + } +} + +impl DataTypecode { + /// A total ordering over the data typecodes that can be used to sort + /// receivers and/or key items in order of decreasing priority, + /// as specified in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses) pub fn preference_order(a: &Self, b: &Self) -> cmp::Ordering { match (a, b) { // Trivial equality checks. @@ -74,51 +120,203 @@ impl Typecode { (_, Self::P2pkh) => cmp::Ordering::Greater, } } +} - pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering { - u32::from(*a).cmp(&u32::from(*b)) +/// The known Metadata Typecodes +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MetadataTypecode { + /// Expiration height metadata as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316) + ExpiryHeight, + /// Expiration height metadata as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316) + ExpiryTime, + /// An unknown MUST-understand metadata item as specified in + /// [ZIP 316, Revision 1](https://zips.z.cash/zip-0316) + /// + /// A parser encountering this typecode MUST halt with an error. + MustUnderstand(u32), + /// An unknown metadata item as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316) + Unknown(u32), +} + +impl TryFrom for MetadataTypecode { + type Error = (); + + fn try_from(typecode: u32) -> Result { + match typecode { + 0xC0..=0xDF => Ok(MetadataTypecode::Unknown(typecode)), + 0xE0 => Ok(MetadataTypecode::ExpiryHeight), + 0xE1 => Ok(MetadataTypecode::ExpiryTime), + 0xE2..=0xFC => Ok(MetadataTypecode::MustUnderstand(typecode)), + _ => Err(()), + } + } +} + +impl From for u32 { + fn from(t: MetadataTypecode) -> Self { + match t { + MetadataTypecode::ExpiryHeight => 0xE0, + MetadataTypecode::ExpiryTime => 0xE1, + MetadataTypecode::MustUnderstand(value) => value, + MetadataTypecode::Unknown(value) => value, + } } } +/// An enumeration of the Unified Container Item Typecodes. +/// +/// Unified Address Items are partitioned into two sets: data items, which include +/// receivers and viewing keys, and metadata items. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Typecode { + /// A data (receiver or viewing key) typecode. + Data(DataTypecode), + /// A metadata typecode. + Metadata(MetadataTypecode), +} + +impl Typecode { + /// The typecode for p2pkh data items. + pub const P2PKH: Typecode = Typecode::Data(DataTypecode::P2pkh); + /// The typecode for p2sh data items. + pub const P2SH: Typecode = Typecode::Data(DataTypecode::P2sh); + /// The typecode for Sapling data items. + pub const SAPLING: Typecode = Typecode::Data(DataTypecode::Sapling); + /// The typecode for Orchard data items. + pub const ORCHARD: Typecode = Typecode::Data(DataTypecode::Orchard); +} + impl TryFrom for Typecode { type Error = ParseError; fn try_from(typecode: u32) -> Result { - match typecode { - 0x00 => Ok(Typecode::P2pkh), - 0x01 => Ok(Typecode::P2sh), - 0x02 => Ok(Typecode::Sapling), - 0x03 => Ok(Typecode::Orchard), - 0x04..=0x02000000 => Ok(Typecode::Unknown(typecode)), - 0x02000001..=u32::MAX => Err(ParseError::InvalidTypecodeValue(typecode as u64)), - } + DataTypecode::try_from(typecode) + .map_or_else( + |()| MetadataTypecode::try_from(typecode).map(Typecode::Metadata), + |t| Ok(Typecode::Data(t)), + ) + .map_err(|()| ParseError::InvalidTypecodeValue(typecode)) } } impl From for u32 { fn from(t: Typecode) -> Self { match t { - Typecode::P2pkh => 0x00, - Typecode::P2sh => 0x01, - Typecode::Sapling => 0x02, - Typecode::Orchard => 0x03, - Typecode::Unknown(typecode) => typecode, + Typecode::Data(tc) => tc.into(), + Typecode::Metadata(tc) => tc.into(), } } } impl TryFrom for usize { type Error = TryFromIntError; + fn try_from(t: Typecode) -> Result { u32::from(t).try_into() } } -impl Typecode { - fn is_transparent(&self) -> bool { - // Unknown typecodes are treated as not transparent for the purpose of disallowing - // only-transparent UAs, which can be represented with existing address encodings. - matches!(self, Typecode::P2pkh | Typecode::P2sh) +/// An enumeration of known Unified Metadata Item types. +/// +/// Unknown MUST-understand metadata items are NOT represented using this type, as the presence of +/// an unrecognized metadata item with a typecode in the `MUST-understand` range will result in a +/// parse failure, instead of the construction of a metadata item. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum MetadataItem { + /// The expiry height for a Unified Address or Unified Viewing Key + ExpiryHeight(u32), + /// The expiry time for a Unified Address or Unified Viewing Key + ExpiryTime(u64), + /// A Metadata Item with an unrecognized Typecode. MUST-understand metadata items are NOT + /// represented using this type, as the presence of an unrecognized metadata item with a + /// typecode in the `MUST-understand` range will result in a parse failure. + Unknown { typecode: u32, data: Vec }, +} + +impl MetadataItem { + /// Parse a metadata item for the specified metadata typecode from the provided bytes. + pub fn parse(typecode: MetadataTypecode, data: &[u8]) -> Result { + match typecode { + MetadataTypecode::ExpiryHeight => data + .try_into() + .map(u32::from_le_bytes) + .map(MetadataItem::ExpiryHeight) + .map_err(|_| { + ParseError::InvalidEncoding( + "Expiry height must be a 32-bit little-endian value.".to_string(), + ) + }), + MetadataTypecode::ExpiryTime => data + .try_into() + .map(u64::from_le_bytes) + .map(MetadataItem::ExpiryTime) + .map_err(|_| { + ParseError::InvalidEncoding( + "Expiry time must be a 64-bit little-endian value.".to_string(), + ) + }), + MetadataTypecode::MustUnderstand(tc) => Err(ParseError::NotUnderstood(tc)), + MetadataTypecode::Unknown(typecode) => Ok(MetadataItem::Unknown { + typecode, + data: data.to_vec(), + }), + } + } + + /// Returns the typecode for this metadata item. + pub fn typecode(&self) -> MetadataTypecode { + match self { + MetadataItem::ExpiryHeight(_) => MetadataTypecode::ExpiryHeight, + MetadataItem::ExpiryTime(_) => MetadataTypecode::ExpiryTime, + MetadataItem::Unknown { typecode, .. } => MetadataTypecode::Unknown(*typecode), + } + } + + /// Returns the raw bytes of this metadata item. + pub fn data(&self) -> Cow<'_, [u8]> { + match self { + MetadataItem::ExpiryHeight(h) => Cow::from(h.to_le_bytes().to_vec()), + MetadataItem::ExpiryTime(t) => Cow::from(t.to_le_bytes().to_vec()), + MetadataItem::Unknown { data, .. } => Cow::from(data), + } + } +} + +/// A Unified Encoding Item. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Item { + /// A data item; either a receiver (for Unified Addresses) or a key (for Unified Viewing Keys) + Data(T), + /// A metadata item. + Metadata(MetadataItem), +} + +impl Item { + /// Returns the typecode for this item. + pub fn typecode(&self) -> Typecode { + match self { + Item::Data(d) => Typecode::Data(d.typecode()), + Item::Metadata(m) => Typecode::Metadata(m.typecode()), + } + } + + /// The total ordering over items by their typecodes, used for encoding as specified + /// in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses) + pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering { + u32::from(a.typecode()).cmp(&u32::from(b.typecode())) + } + + /// Returns the raw binary representation of the data for this item. + pub fn data(&self) -> Cow<'_, [u8]> { + match self { + Item::Data(d) => Cow::from(d.data()), + 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 } } @@ -130,7 +328,7 @@ pub enum ParseError { /// The unified container contains a duplicated typecode. DuplicateTypecode(Typecode), /// The parsed typecode exceeds the maximum allowed CompactSize value. - InvalidTypecodeValue(u64), + InvalidTypecodeValue(u32), /// The string is an invalid encoding. InvalidEncoding(String), /// The items in the unified container are not in typecode order. @@ -141,6 +339,8 @@ pub enum ParseError { NotUnified, /// The Bech32m string has an unrecognized human-readable prefix. UnknownPrefix(String), + /// A `MUST-understand` metadata item was not recognized. + NotUnderstood(u32), } impl fmt::Display for ParseError { @@ -156,6 +356,13 @@ impl fmt::Display for ParseError { ParseError::UnknownPrefix(s) => { write!(f, "Unrecognized Bech32m human-readable prefix: {}", s) } + ParseError::NotUnderstood(tc) => { + write!( + f, + "MUST-understand metadata item with typecode {} was not recognized; please upgrade.", + tc + ) + } } } } @@ -166,32 +373,24 @@ impl Error for ParseError {} pub(crate) mod private { use alloc::borrow::ToOwned; use alloc::vec::Vec; - use core::cmp; use core::convert::{TryFrom, TryInto}; use core2::io::Write; - use super::{ParseError, Typecode, PADDING_LEN}; + use super::{DataTypecode, ParseError, Typecode, PADDING_LEN}; + use crate::unified::{Item, MetadataItem}; use zcash_encoding::CompactSize; use zcash_protocol::consensus::NetworkType; /// A raw address or viewing key. - pub trait SealedItem: for<'a> TryFrom<(u32, &'a [u8]), Error = ParseError> + Clone { - fn typecode(&self) -> Typecode; - fn data(&self) -> &[u8]; + pub trait SealedDataItem: Clone { + /// Parse a data item for the specified data typecode from the provided bytes. + fn parse(tc: DataTypecode, value: &[u8]) -> Result; - 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, - } - } + /// Returns the typecode of this data item. + fn typecode(&self) -> DataTypecode; - 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, - } - } + /// Returns the raw bytes of this data item. + fn data(&self) -> &[u8]; } /// A Unified Container containing addresses or viewing keys. @@ -203,7 +402,7 @@ pub(crate) mod private { /// Implementations of this method should act as unchecked constructors /// of the container type; the caller is guaranteed to check the /// general invariants that apply to all unified containers. - fn from_inner(items: Vec) -> Self; + fn from_inner(items: Vec>) -> Self; fn network_hrp(network: &NetworkType) -> &'static str { match network { @@ -234,7 +433,7 @@ pub(crate) mod private { ) .unwrap(); CompactSize::write(&mut writer, data.len()).unwrap(); - writer.write_all(data).unwrap(); + writer.write_all(&data).unwrap(); } } @@ -254,10 +453,13 @@ pub(crate) mod private { } /// Parse the items of the unified container. - fn parse_items>>(hrp: &str, buf: T) -> Result, ParseError> { - fn read_receiver( + fn parse_items>>( + hrp: &str, + buf: T, + ) -> Result>, ParseError> { + fn read_receiver( mut cursor: &mut core2::io::Cursor<&[u8]>, - ) -> Result { + ) -> Result, ParseError> { let typecode = CompactSize::read(&mut cursor) .map(|v| u32::try_from(v).expect("CompactSize::read enforces MAX_SIZE limit")) .map_err(|e| { @@ -285,12 +487,13 @@ pub(crate) mod private { length ))); } - let result = R::try_from(( - typecode, - &buf[cursor.position() as usize..addr_end as usize], - )); + let data = &buf[cursor.position() as usize..addr_end as usize]; + let result = match Typecode::try_from(typecode)? { + Typecode::Data(tc) => Item::Data(R::parse(tc, data)?), + Typecode::Metadata(tc) => Item::Metadata(MetadataItem::parse(tc, data)?), + }; cursor.set_position(addr_end); - result + Ok(result) } // Here we allocate if necessary to get a mutable Vec to unjumble. @@ -326,11 +529,9 @@ pub(crate) mod private { /// A private function that constructs a unified container with the /// specified items, which must be in ascending typecode order. - fn try_from_items_internal(items: Vec) -> Result { - assert!(u32::from(Typecode::P2sh) == u32::from(Typecode::P2pkh) + 1); - - let mut only_transparent = true; + fn try_from_items_internal(items: Vec>) -> Result { let mut prev_code = None; // less than any Some + let mut only_transparent = true; for item in &items { let t = item.typecode(); let t_code = Some(u32::from(t)); @@ -338,13 +539,15 @@ pub(crate) mod private { return Err(ParseError::InvalidTypecodeOrder); } else if t_code == prev_code { return Err(ParseError::DuplicateTypecode(t)); - } else if t == Typecode::P2sh && prev_code == Some(u32::from(Typecode::P2pkh)) { + } else if t == Typecode::Data(DataTypecode::P2sh) + && prev_code == Some(u32::from(DataTypecode::P2pkh)) + { // P2pkh and P2sh can only be in that order and next to each other, // otherwise we would detect an out-of-order or duplicate typecode. return Err(ParseError::BothP2phkAndP2sh); } else { prev_code = t_code; - only_transparent = only_transparent && t.is_transparent(); + only_transparent = only_transparent && item.is_transparent_data_item(); } } @@ -362,7 +565,7 @@ pub(crate) mod private { } } -use private::SealedItem; +use private::SealedDataItem; /// The bech32m checksum algorithm, defined in [BIP-350], extended to allow all lengths /// supported by [ZIP 316]. @@ -382,9 +585,10 @@ impl Checksum for Bech32mZip316 { /// Trait providing common encoding and decoding logic for Unified containers. pub trait Encoding: private::SealedContainer { - /// Constructs a value of a unified container type from a vector - /// of container items, sorted according to typecode as specified - /// in ZIP 316. + /// Constructs a value of a unified container type from a vector of container + /// items. These items will be sorted according to typecode as specified in ZIP + /// 316, so this method is not necessarily round-trip compatible with + /// [`Container::items_as_parsed`]. /// /// This function will return an error in the case that the following ZIP 316 /// invariants concerning the composition of a unified container are @@ -392,8 +596,8 @@ pub trait Encoding: private::SealedContainer { /// * the item list may not contain two items having the same typecode /// * the item list may not contain only transparent items (or no items) /// * the item list may not contain both P2PKH and P2SH items. - fn try_from_items(mut items: Vec) -> Result { - items.sort_unstable_by(Self::Item::encoding_order); + fn try_from_items(mut items: Vec>) -> Result { + items.sort_unstable_by(Item::encoding_order); Self::try_from_items_internal(items) } @@ -429,20 +633,9 @@ pub trait Encoding: private::SealedContainer { /// Trait for Unified containers, that exposes the items within them. pub trait Container { - /// The type of item in this unified container. - type Item: SealedItem; - - /// Returns the items contained within this container, sorted in preference order. - fn items(&self) -> Vec { - let mut items = self.items_as_parsed().to_vec(); - // Unstable sorting is fine, because all items are guaranteed by construction - // to have distinct typecodes. - items.sort_unstable_by(Self::Item::preference_order); - items - } + /// The type of data items in this unified container. + type DataItem: SealedDataItem; - /// Returns the items in the order they were parsed from the string encoding. - /// - /// This API is for advanced usage; in most cases you should use `Self::items`. - fn items_as_parsed(&self) -> &[Self::Item]; + /// Returns the items in encoding order. + fn items_as_parsed(&self) -> &[Item]; } diff --git a/components/zcash_address/src/kind/unified/address.rs b/components/zcash_address/src/kind/unified/address.rs index 41be7ab99..d1352f3b5 100644 --- a/components/zcash_address/src/kind/unified/address.rs +++ b/components/zcash_address/src/kind/unified/address.rs @@ -1,11 +1,11 @@ use zcash_protocol::{constants, PoolType}; -use super::{private::SealedItem, ParseError, Typecode}; +use super::{private::SealedDataItem, DataTypecode, Item, ParseError}; use alloc::vec::Vec; -use core::convert::{TryFrom, TryInto}; +use core::{cmp, convert::TryInto}; -/// The set of known Receivers for Unified Addresses. +/// The enumeration of Unified Address Receivers of known types. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Receiver { Orchard([u8; 43]), @@ -15,34 +15,39 @@ pub enum Receiver { Unknown { typecode: u32, data: Vec }, } -impl TryFrom<(u32, &[u8])> for Receiver { - type Error = ParseError; +impl Receiver { + fn preference_order(a: &Self, b: &Self) -> cmp::Ordering { + DataTypecode::preference_order(&a.typecode(), &b.typecode()) + } +} - fn try_from((typecode, addr): (u32, &[u8])) -> Result { - match typecode.try_into()? { - Typecode::P2pkh => addr.try_into().map(Receiver::P2pkh), - Typecode::P2sh => addr.try_into().map(Receiver::P2sh), - Typecode::Sapling => addr.try_into().map(Receiver::Sapling), - Typecode::Orchard => addr.try_into().map(Receiver::Orchard), - Typecode::Unknown(_) => Ok(Receiver::Unknown { +impl SealedDataItem for Receiver { + fn parse(typecode: DataTypecode, data: &[u8]) -> Result { + match typecode { + DataTypecode::P2pkh => data.try_into().map(Receiver::P2pkh), + DataTypecode::P2sh => data.try_into().map(Receiver::P2sh), + DataTypecode::Sapling => data.try_into().map(Receiver::Sapling), + DataTypecode::Orchard => data.try_into().map(Receiver::Orchard), + DataTypecode::Unknown(typecode) => Ok(Receiver::Unknown { typecode, - data: addr.to_vec(), + data: data.to_vec(), }), } .map_err(|e| { - ParseError::InvalidEncoding(format!("Invalid address for typecode {}: {}", typecode, e)) + ParseError::InvalidEncoding(format!( + "Invalid address for typecode {:?}: {:?}", + typecode, e + )) }) } -} -impl SealedItem for Receiver { - fn typecode(&self) -> Typecode { + fn typecode(&self) -> DataTypecode { match self { - Receiver::P2pkh(_) => Typecode::P2pkh, - Receiver::P2sh(_) => Typecode::P2sh, - Receiver::Sapling(_) => Typecode::Sapling, - Receiver::Orchard(_) => Typecode::Orchard, - Receiver::Unknown { typecode, .. } => Typecode::Unknown(*typecode), + Receiver::P2pkh(_) => DataTypecode::P2pkh, + Receiver::P2sh(_) => DataTypecode::P2sh, + Receiver::Sapling(_) => DataTypecode::Sapling, + Receiver::Orchard(_) => DataTypecode::Orchard, + Receiver::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode), } } @@ -64,7 +69,7 @@ impl SealedItem for Receiver { /// ``` /// # use core::convert::Infallible; /// use zcash_address::{ -/// unified::{self, Container, Encoding}, +/// unified::{self, Container, Encoding, Item}, /// ConversionError, TryFromRawAddress, ZcashAddress, /// }; /// @@ -95,38 +100,65 @@ impl SealedItem for Receiver { /// /// // We can obtain the receivers for the UA in preference order /// // (the order in which wallets should prefer to use them): -/// let receivers: Vec = ua.items(); +/// let receivers: Vec = ua.receivers(); /// /// // And we can create the UA from a list of receivers: -/// let new_ua = unified::Address::try_from_items(receivers)?; +/// let new_ua = unified::Address::try_from_items(receivers.into_iter().map(Item::Data).collect())?; /// assert_eq!(new_ua, ua); /// # Ok(()) /// # } /// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Address(pub(crate) Vec); +pub struct Address(pub(crate) Vec>); + +impl Address { + /// Returns the receiver items for this address, in order of decreasing preference. + /// + /// The receiver for a wallet to send to can safely be chosen by selecting the first receiver + /// of a type that wallet supports from the result. + pub fn receivers(&self) -> Vec { + let mut result = self + .0 + .iter() + .filter_map(|item| match item { + Item::Data(r) => Some(r.clone()), + Item::Metadata(_) => None, + }) + .collect::>(); + result.sort_unstable_by(Receiver::preference_order); + result + } +} impl Address { /// Returns whether this address has the ability to receive transfers of the given pool type. pub fn has_receiver_of_type(&self, pool_type: PoolType) -> bool { - self.0.iter().any(|r| match r { - Receiver::Orchard(_) => pool_type == PoolType::ORCHARD, - Receiver::Sapling(_) => pool_type == PoolType::SAPLING, - Receiver::P2pkh(_) | Receiver::P2sh(_) => pool_type == PoolType::TRANSPARENT, - Receiver::Unknown { .. } => false, + self.0.iter().any(|item| match item { + Item::Data(Receiver::Orchard(_)) => pool_type == PoolType::ORCHARD, + Item::Data(Receiver::Sapling(_)) => pool_type == PoolType::SAPLING, + Item::Data(Receiver::P2pkh(_)) | Item::Data(Receiver::P2sh(_)) => { + pool_type == PoolType::TRANSPARENT + } + Item::Data(Receiver::Unknown { .. }) => false, + Item::Metadata(_) => false, }) } /// Returns whether this address contains the given receiver. pub fn contains_receiver(&self, receiver: &Receiver) -> bool { - self.0.contains(receiver) + self.0 + .iter() + .any(|item| matches!(item, Item::Data(r) if r == receiver)) } /// Returns whether this address can receive a memo. pub fn can_receive_memo(&self) -> bool { - self.0 - .iter() - .any(|r| matches!(r, Receiver::Sapling(_) | Receiver::Orchard(_))) + self.0.iter().any(|r| { + matches!( + r, + Item::Data(Receiver::Sapling(_)) | Item::Data(Receiver::Orchard(_)) + ) + }) } } @@ -148,16 +180,16 @@ impl super::private::SealedContainer for Address { /// The HRP for a Bech32m-encoded regtest Unified Address. const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_ADDRESS; - fn from_inner(receivers: Vec) -> Self { + fn from_inner(receivers: Vec>) -> Self { Self(receivers) } } impl super::Encoding for Address {} impl super::Container for Address { - type Item = Receiver; + type DataItem = Receiver; - fn items_as_parsed(&self) -> &[Receiver] { + fn items_as_parsed(&self) -> &[Item] { &self.0 } } @@ -173,10 +205,10 @@ pub mod testing { sample::select, strategy::Strategy, }; - use zcash_encoding::MAX_COMPACT_SIZE; use super::{Address, Receiver}; - use crate::unified::Typecode; + use crate::unified::{DataTypecode, Item}; + use zcash_encoding::MAX_COMPACT_SIZE; prop_compose! { fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] { @@ -188,61 +220,62 @@ pub mod testing { } /// A strategy to generate an arbitrary transparent typecode. - pub fn arb_transparent_typecode() -> impl Strategy { - select(vec![Typecode::P2pkh, Typecode::P2sh]) + fn arb_transparent_typecode() -> impl Strategy { + select(vec![DataTypecode::P2pkh, DataTypecode::P2sh]) } /// A strategy to generate an arbitrary shielded (Sapling, Orchard, or unknown) typecode. - pub fn arb_shielded_typecode() -> impl Strategy { + fn arb_shielded_typecode() -> impl Strategy { prop_oneof![ - Just(Typecode::Sapling), - Just(Typecode::Orchard), - ((::from(Typecode::Orchard) + 1)..MAX_COMPACT_SIZE).prop_map(Typecode::Unknown) + Just(DataTypecode::Sapling), + Just(DataTypecode::Orchard), + ((::from(DataTypecode::Orchard) + 1)..MAX_COMPACT_SIZE) + .prop_map(DataTypecode::Unknown) ] } /// A strategy to generate an arbitrary valid set of typecodes without /// duplication and containing only one of P2sh and P2pkh transparent - /// typecodes. The resulting vector will be sorted in encoding order. - pub fn arb_typecodes() -> impl Strategy> { + /// typecodes. + fn arb_typecodes() -> impl Strategy> { prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| { - prop::collection::hash_set(arb_shielded_typecode(), 1..4).prop_map(move |xs| { - let mut typecodes: Vec<_> = xs.into_iter().chain(transparent).collect(); - typecodes.sort_unstable_by(Typecode::encoding_order); - typecodes - }) + prop::collection::hash_set(arb_shielded_typecode(), 1..4) + .prop_map(move |xs| xs.into_iter().chain(transparent).collect::>()) }) } - /// Generates an arbitrary Unified address containing receivers corresponding to the provided - /// set of typecodes. The receivers of this address are likely to not represent valid protocol - /// receivers, and should only be used for testing parsing and/or encoding functions that do - /// not concern themselves with the validity of the underlying receivers. - pub fn arb_unified_address_for_typecodes( - typecodes: Vec, + /// A strategy to generate a vector of unified address receivers containing random data. The + /// resulting receivers may not be valid according to protocol rules; this generator is only + /// intended for use in testing parsing and serialization. + fn arb_unified_address_receivers( + typecodes: Vec, ) -> impl Strategy> { typecodes .into_iter() .map(|tc| match tc { - Typecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(), - Typecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(), - Typecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(), - Typecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(), - Typecode::Unknown(typecode) => vec(any::(), 32..256) + DataTypecode::P2pkh => uniform20(0u8..).prop_map(Receiver::P2pkh).boxed(), + DataTypecode::P2sh => uniform20(0u8..).prop_map(Receiver::P2sh).boxed(), + DataTypecode::Sapling => uniform43().prop_map(Receiver::Sapling).boxed(), + DataTypecode::Orchard => uniform43().prop_map(Receiver::Orchard).boxed(), + DataTypecode::Unknown(typecode) => vec(any::(), 32..256) .prop_map(move |data| Receiver::Unknown { typecode, data }) .boxed(), }) .collect::>() } - /// Generates an arbitrary Unified address. The receivers of this address are likely to not - /// represent valid protocol receivers, and should only be used for testing parsing and/or - /// encoding functions that do not concern themselves with the validity of the underlying - /// receivers. + /// A strategy to generate an arbitrary Unified Address containing only receivers, without + /// additional metadata. The receivers in the resulting address may not be valid according to + /// protocol rules; this generator is only intended for use in testing parsing and + /// serialization. pub fn arb_unified_address() -> impl Strategy { arb_typecodes() - .prop_flat_map(arb_unified_address_for_typecodes) - .prop_map(Address) + .prop_flat_map(arb_unified_address_receivers) + .prop_map(|rs| { + let mut items = rs.into_iter().map(Item::Data).collect::>(); + items.sort_unstable_by(Item::encoding_order); + Address(items) + }) } } @@ -254,17 +287,16 @@ mod tests { use alloc::borrow::ToOwned; use assert_matches::assert_matches; + use proptest::{prelude::*, sample::select}; + use zcash_protocol::consensus::NetworkType; + use super::{Address, ParseError, Receiver}; use crate::{ - kind::unified::{private::SealedContainer, Container, Encoding}, - unified::address::testing::arb_unified_address, + kind::unified::{private::SealedContainer, Encoding as _}, + unified::{address::testing::arb_unified_address, Item, Typecode}, }; - use proptest::{prelude::*, sample::select}; - - use super::{Address, ParseError, Receiver, Typecode}; - proptest! { #[test] fn ua_roundtrip( @@ -354,11 +386,14 @@ mod tests { fn duplicate_typecode() { // Construct and serialize an invalid UA. This must be done using private // methods, as the public API does not permit construction of such invalid values. - let ua = Address(vec![Receiver::Sapling([1; 43]), Receiver::Sapling([2; 43])]); + let ua = Address(vec![ + Item::Data(Receiver::Sapling([1; 43])), + Item::Data(Receiver::Sapling([2; 43])), + ]); let encoded = ua.to_jumbled_bytes(Address::MAINNET); assert_eq!( Address::parse_internal(Address::MAINNET, &encoded[..]), - Err(ParseError::DuplicateTypecode(Typecode::Sapling)) + Err(ParseError::DuplicateTypecode(Typecode::SAPLING)) ); } @@ -366,7 +401,10 @@ mod tests { fn p2pkh_and_p2sh() { // Construct and serialize an invalid UA. This must be done using private // methods, as the public API does not permit construction of such invalid values. - let ua = Address(vec![Receiver::P2pkh([0; 20]), Receiver::P2sh([0; 20])]); + let ua = Address(vec![ + Item::Data(Receiver::P2pkh([0; 20])), + Item::Data(Receiver::P2sh([0; 20])), + ]); let encoded = ua.to_jumbled_bytes(Address::MAINNET); // ensure that decoding catches the error assert_eq!( @@ -379,7 +417,10 @@ mod tests { fn addresses_out_of_order() { // Construct and serialize an invalid UA. This must be done using private // methods, as the public API does not permit construction of such invalid values. - let ua = Address(vec![Receiver::Sapling([0; 43]), Receiver::P2pkh([0; 20])]); + let ua = Address(vec![ + Item::Data(Receiver::Sapling([0; 43])), + Item::Data(Receiver::P2pkh([0; 20])), + ]); let encoded = ua.to_jumbled_bytes(Address::MAINNET); // ensure that decoding catches the error assert_eq!( @@ -411,18 +452,18 @@ mod tests { fn receivers_are_sorted() { // Construct a UA with receivers in an unsorted order. let ua = Address(vec![ - Receiver::P2pkh([0; 20]), - Receiver::Orchard([0; 43]), - Receiver::Unknown { + Item::Data(Receiver::P2pkh([0; 20])), + Item::Data(Receiver::Orchard([0; 43])), + Item::Data(Receiver::Unknown { typecode: 0xff, data: vec![], - }, - Receiver::Sapling([0; 43]), + }), + Item::Data(Receiver::Sapling([0; 43])), ]); // `Address::receivers` sorts the receivers in priority order. assert_eq!( - ua.items(), + ua.receivers(), vec![ Receiver::Orchard([0; 43]), Receiver::Sapling([0; 43]), diff --git a/components/zcash_address/src/kind/unified/fvk.rs b/components/zcash_address/src/kind/unified/fvk.rs index 534d6c783..ad558d85e 100644 --- a/components/zcash_address/src/kind/unified/fvk.rs +++ b/components/zcash_address/src/kind/unified/fvk.rs @@ -1,10 +1,10 @@ use alloc::vec::Vec; -use core::convert::{TryFrom, TryInto}; +use core::convert::TryInto; use zcash_protocol::constants; use super::{ - private::{SealedContainer, SealedItem}, - Container, Encoding, ParseError, Typecode, + private::{SealedContainer, SealedDataItem}, + Container, DataTypecode, Encoding, Item, ParseError, }; /// The set of known FVKs for Unified FVKs. @@ -41,31 +41,27 @@ pub enum Fvk { }, } -impl TryFrom<(u32, &[u8])> for Fvk { - type Error = ParseError; - - fn try_from((typecode, data): (u32, &[u8])) -> Result { +impl SealedDataItem for Fvk { + fn parse(typecode: DataTypecode, data: &[u8]) -> Result { let data = data.to_vec(); - match typecode.try_into()? { - Typecode::P2pkh => data.try_into().map(Fvk::P2pkh), - Typecode::P2sh => Err(data), - Typecode::Sapling => data.try_into().map(Fvk::Sapling), - Typecode::Orchard => data.try_into().map(Fvk::Orchard), - Typecode::Unknown(_) => Ok(Fvk::Unknown { typecode, data }), + match typecode { + DataTypecode::P2pkh => data.try_into().map(Fvk::P2pkh), + DataTypecode::P2sh => Err(data), + DataTypecode::Sapling => data.try_into().map(Fvk::Sapling), + DataTypecode::Orchard => data.try_into().map(Fvk::Orchard), + DataTypecode::Unknown(typecode) => Ok(Fvk::Unknown { typecode, data }), } .map_err(|e| { - ParseError::InvalidEncoding(format!("Invalid fvk for typecode {}: {:?}", typecode, e)) + ParseError::InvalidEncoding(format!("Invalid fvk for typecode {:?}: {:?}", typecode, e)) }) } -} -impl SealedItem for Fvk { - fn typecode(&self) -> Typecode { + fn typecode(&self) -> DataTypecode { match self { - Fvk::P2pkh(_) => Typecode::P2pkh, - Fvk::Sapling(_) => Typecode::Sapling, - Fvk::Orchard(_) => Typecode::Orchard, - Fvk::Unknown { typecode, .. } => Typecode::Unknown(*typecode), + Fvk::P2pkh(_) => DataTypecode::P2pkh, + Fvk::Sapling(_) => DataTypecode::Sapling, + Fvk::Orchard(_) => DataTypecode::Orchard, + Fvk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode), } } @@ -84,7 +80,7 @@ impl SealedItem for Fvk { /// # Examples /// /// ``` -/// use zcash_address::unified::{self, Container, Encoding}; +/// use zcash_address::unified::{self, Container, Encoding, Item}; /// /// # #[cfg(not(feature = "std"))] /// # fn main() {} @@ -95,28 +91,22 @@ impl SealedItem for Fvk { /// /// let (network, ufvk) = unified::Ufvk::decode(example_ufvk)?; /// -/// // We can obtain the pool-specific Full Viewing Keys for the UFVK in preference -/// // order (the order in which wallets should prefer to use their corresponding -/// // address receivers): -/// let fvks: Vec = ufvk.items(); +/// // We can obtain the pool-specific Full Viewing Keys for the UFVK. +/// let fvks: &[Item] = ufvk.items_as_parsed(); /// /// // And we can create the UFVK from a list of FVKs: -/// let new_ufvk = unified::Ufvk::try_from_items(fvks)?; +/// let new_ufvk = unified::Ufvk::try_from_items(fvks.to_vec())?; /// assert_eq!(new_ufvk, ufvk); /// # Ok(()) /// # } /// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Ufvk(pub(crate) Vec); +pub struct Ufvk(pub(crate) Vec>); impl Container for Ufvk { - type Item = Fvk; + type DataItem = Fvk; - /// Returns the FVKs contained within this UFVK, in the order they were - /// parsed from the string encoding. - /// - /// This API is for advanced usage; in most cases you should use `Ufvk::receivers`. - fn items_as_parsed(&self) -> &[Fvk] { + fn items_as_parsed(&self) -> &[Item] { &self.0 } } @@ -145,7 +135,7 @@ impl SealedContainer for Ufvk { /// [zip-0316]: https://zips.z.cash/zip-0316 const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_FVK; - fn from_inner(fvks: Vec) -> Self { + fn from_inner(fvks: Vec>) -> Self { Self(fvks) } } @@ -159,10 +149,10 @@ mod tests { use proptest::{array::uniform1, array::uniform32, prelude::*, sample::select}; - use super::{Fvk, ParseError, Typecode, Ufvk}; - use crate::kind::unified::{ - private::{SealedContainer, SealedItem}, - Container, Encoding, + use super::{Fvk, ParseError, Ufvk}; + use crate::{ + kind::unified::{private::SealedContainer, Encoding as _}, + unified::{Item, Typecode}, }; use zcash_protocol::consensus::NetworkType; @@ -220,8 +210,8 @@ mod tests { shielded in arb_shielded_fvk(), transparent in prop::option::of(arb_transparent_fvk()), ) -> Ufvk { - let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect(); - items.sort_unstable_by(Fvk::encoding_order); + let mut items: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect(); + items.sort_unstable_by(Item::encoding_order); Ufvk(items) } } @@ -328,11 +318,14 @@ mod tests { fn duplicate_typecode() { // Construct and serialize an invalid Ufvk. This must be done using private // methods, as the public API does not permit construction of such invalid values. - let ufvk = Ufvk(vec![Fvk::Sapling([1; 128]), Fvk::Sapling([2; 128])]); + let ufvk = Ufvk(vec![ + Item::Data(Fvk::Sapling([1; 128])), + Item::Data(Fvk::Sapling([2; 128])), + ]); let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET); assert_eq!( Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]), - Err(ParseError::DuplicateTypecode(Typecode::Sapling)) + Err(ParseError::DuplicateTypecode(Typecode::SAPLING)) ); } @@ -353,32 +346,4 @@ mod tests { Err(ParseError::OnlyTransparent) ); } - - #[test] - fn fvks_are_sorted() { - // Construct a UFVK with fvks in an unsorted order. - let ufvk = Ufvk(vec![ - Fvk::P2pkh([0; 65]), - Fvk::Orchard([0; 96]), - Fvk::Unknown { - typecode: 0xff, - data: vec![], - }, - Fvk::Sapling([0; 128]), - ]); - - // `Ufvk::items` sorts the fvks in priority order. - assert_eq!( - ufvk.items(), - vec![ - Fvk::Orchard([0; 96]), - Fvk::Sapling([0; 128]), - Fvk::P2pkh([0; 65]), - Fvk::Unknown { - typecode: 0xff, - data: vec![], - }, - ] - ) - } } diff --git a/components/zcash_address/src/kind/unified/ivk.rs b/components/zcash_address/src/kind/unified/ivk.rs index 7b776b000..3c909aacf 100644 --- a/components/zcash_address/src/kind/unified/ivk.rs +++ b/components/zcash_address/src/kind/unified/ivk.rs @@ -1,10 +1,10 @@ use alloc::vec::Vec; -use core::convert::{TryFrom, TryInto}; +use core::convert::TryInto; use zcash_protocol::constants; use super::{ - private::{SealedContainer, SealedItem}, - Container, Encoding, ParseError, Typecode, + private::{SealedContainer, SealedDataItem}, + Container, DataTypecode, Encoding, Item, ParseError, }; /// The set of known IVKs for Unified IVKs. @@ -46,31 +46,27 @@ pub enum Ivk { }, } -impl TryFrom<(u32, &[u8])> for Ivk { - type Error = ParseError; - - fn try_from((typecode, data): (u32, &[u8])) -> Result { +impl SealedDataItem for Ivk { + fn parse(typecode: DataTypecode, data: &[u8]) -> Result { let data = data.to_vec(); - match typecode.try_into()? { - Typecode::P2pkh => data.try_into().map(Ivk::P2pkh), - Typecode::P2sh => Err(data), - Typecode::Sapling => data.try_into().map(Ivk::Sapling), - Typecode::Orchard => data.try_into().map(Ivk::Orchard), - Typecode::Unknown(_) => Ok(Ivk::Unknown { typecode, data }), + match typecode { + DataTypecode::P2pkh => data.try_into().map(Ivk::P2pkh), + DataTypecode::P2sh => Err(data), + DataTypecode::Sapling => data.try_into().map(Ivk::Sapling), + DataTypecode::Orchard => data.try_into().map(Ivk::Orchard), + DataTypecode::Unknown(typecode) => Ok(Ivk::Unknown { typecode, data }), } .map_err(|e| { - ParseError::InvalidEncoding(format!("Invalid ivk for typecode {}: {:?}", typecode, e)) + ParseError::InvalidEncoding(format!("Invalid ivk for typecode {:?}: {:?}", typecode, e)) }) } -} -impl SealedItem for Ivk { - fn typecode(&self) -> Typecode { + fn typecode(&self) -> DataTypecode { match self { - Ivk::P2pkh(_) => Typecode::P2pkh, - Ivk::Sapling(_) => Typecode::Sapling, - Ivk::Orchard(_) => Typecode::Orchard, - Ivk::Unknown { typecode, .. } => Typecode::Unknown(*typecode), + Ivk::P2pkh(_) => DataTypecode::P2pkh, + Ivk::Sapling(_) => DataTypecode::Sapling, + Ivk::Orchard(_) => DataTypecode::Orchard, + Ivk::Unknown { typecode, .. } => DataTypecode::Unknown(*typecode), } } @@ -89,7 +85,7 @@ impl SealedItem for Ivk { /// # Examples /// /// ``` -/// use zcash_address::unified::{self, Container, Encoding}; +/// use zcash_address::unified::{self, Container, Encoding, Item}; /// /// # #[cfg(not(feature = "std"))] /// # fn main() {} @@ -100,28 +96,22 @@ impl SealedItem for Ivk { /// /// let (network, uivk) = unified::Uivk::decode(example_uivk)?; /// -/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK in -/// // preference order (the order in which wallets should prefer to use their -/// // corresponding address receivers): -/// let ivks: Vec = uivk.items(); +/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK. +/// let ivks: &[Item] = uivk.items_as_parsed(); /// -/// // And we can create the UIVK from a list of IVKs: -/// let new_uivk = unified::Uivk::try_from_items(ivks)?; +/// // And we can create the UIVK from a vector of IVKs: +/// let new_uivk = unified::Uivk::try_from_items(ivks.to_vec())?; /// assert_eq!(new_uivk, uivk); /// # Ok(()) /// # } /// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Uivk(pub(crate) Vec); +pub struct Uivk(pub(crate) Vec>); impl Container for Uivk { - type Item = Ivk; + type DataItem = Ivk; - /// Returns the IVKs contained within this UIVK, in the order they were - /// parsed from the string encoding. - /// - /// This API is for advanced usage; in most cases you should use `Uivk::items`. - fn items_as_parsed(&self) -> &[Ivk] { + fn items_as_parsed(&self) -> &[Item] { &self.0 } } @@ -146,7 +136,7 @@ impl SealedContainer for Uivk { /// The HRP for a Bech32m-encoded regtest Unified IVK. const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_IVK; - fn from_inner(ivks: Vec) -> Self { + fn from_inner(ivks: Vec>) -> Self { Self(ivks) } } @@ -164,10 +154,10 @@ mod tests { sample::select, }; - use super::{Ivk, ParseError, Typecode, Uivk}; - use crate::kind::unified::{ - private::{SealedContainer, SealedItem}, - Container, Encoding, + use super::{Ivk, ParseError, Uivk}; + use crate::{ + kind::unified::{private::SealedContainer, Encoding as _}, + unified::{Item, Typecode}, }; use zcash_protocol::consensus::NetworkType; @@ -209,8 +199,8 @@ mod tests { shielded in arb_shielded_ivk(), transparent in prop::option::of(arb_transparent_ivk()), ) -> Uivk { - let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect(); - items.sort_unstable_by(Ivk::encoding_order); + let mut items: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect(); + items.sort_unstable_by(Item::encoding_order); Uivk(items) } } @@ -306,11 +296,14 @@ mod tests { #[test] fn duplicate_typecode() { // Construct and serialize an invalid UIVK. - let uivk = Uivk(vec![Ivk::Sapling([1; 64]), Ivk::Sapling([2; 64])]); + let uivk = Uivk(vec![ + Item::Data(Ivk::Sapling([1; 64])), + Item::Data(Ivk::Sapling([2; 64])), + ]); let encoded = uivk.encode(&NetworkType::Main); assert_eq!( Uivk::decode(&encoded), - Err(ParseError::DuplicateTypecode(Typecode::Sapling)) + Err(ParseError::DuplicateTypecode(Typecode::SAPLING)) ); } @@ -331,32 +324,4 @@ mod tests { Err(ParseError::OnlyTransparent) ); } - - #[test] - fn ivks_are_sorted() { - // Construct a UIVK with ivks in an unsorted order. - let uivk = Uivk(vec![ - Ivk::P2pkh([0; 65]), - Ivk::Orchard([0; 64]), - Ivk::Unknown { - typecode: 0xff, - data: vec![], - }, - Ivk::Sapling([0; 64]), - ]); - - // `Uivk::items` sorts the ivks in priority order. - assert_eq!( - uivk.items(), - vec![ - Ivk::Orchard([0; 64]), - Ivk::Sapling([0; 64]), - Ivk::P2pkh([0; 65]), - Ivk::Unknown { - typecode: 0xff, - data: vec![], - }, - ] - ) - } } diff --git a/components/zcash_address/src/test_vectors.rs b/components/zcash_address/src/test_vectors.rs index a7278734e..0c3aff46b 100644 --- a/components/zcash_address/src/test_vectors.rs +++ b/components/zcash_address/src/test_vectors.rs @@ -8,6 +8,7 @@ use { unified::{ self, address::{test_vectors::TEST_VECTORS, Receiver}, + Item, }, ToAddress, ZcashAddress, }, @@ -38,6 +39,7 @@ fn unified() { data: data.to_vec(), }) })) + .map(Item::Data) .collect(); let expected_addr = From fe6b1fc2fb3b61f7204edd5ef50979663ec41296 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 25 Jan 2024 14:12:19 -0700 Subject: [PATCH 03/10] zcash_keys: Update key and address types to include ZIP-316 metadata items. --- Cargo.lock | 3 + components/zip321/src/lib.rs | 1 + devtools/Cargo.toml | 1 + devtools/src/bin/inspect/address.rs | 8 +- devtools/src/bin/inspect/keys.rs | 4 +- devtools/src/bin/inspect/keys/view.rs | 129 +++-- devtools/src/bin/inspect/transaction.rs | 4 +- .../src/data_api/testing/orchard.rs | 5 +- zcash_client_sqlite/src/wallet/init.rs | 6 +- .../init/migrations/add_transaction_views.rs | 2 +- .../wallet/init/migrations/addresses_table.rs | 4 +- .../migrations/ensure_orchard_ua_receiver.rs | 11 +- .../wallet/init/migrations/ufvk_support.rs | 3 +- zcash_client_sqlite/src/wallet/transparent.rs | 8 +- zcash_keys/CHANGELOG.md | 27 + zcash_keys/src/address.rs | 236 +++++--- zcash_keys/src/keys.rs | 546 +++++++++++++----- 17 files changed, 693 insertions(+), 305 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4beeea797..d51a7eaa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,8 +684,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -1224,6 +1226,7 @@ dependencies = [ "bellman", "bip0039", "blake2b_simd", + "chrono", "ed25519-zebra", "equihash", "group", diff --git a/components/zip321/src/lib.rs b/components/zip321/src/lib.rs index 42b81fb78..17fd8d974 100644 --- a/components/zip321/src/lib.rs +++ b/components/zip321/src/lib.rs @@ -799,6 +799,7 @@ pub mod testing { use zcash_protocol::{consensus::NetworkType, value::testing::arb_zatoshis}; use super::{MemoBytes, Payment, TransactionRequest}; + pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*"; prop_compose! { diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 3f0095b30..10a4c6573 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -43,6 +43,7 @@ orchard.workspace = true # zcash-inspect tool anyhow = "1" +chrono = "0.4" hex.workspace = true lazy_static.workspace = true secrecy.workspace = true diff --git a/devtools/src/bin/inspect/address.rs b/devtools/src/bin/inspect/address.rs index 5f47d4e0d..028ff3b3a 100644 --- a/devtools/src/bin/inspect/address.rs +++ b/devtools/src/bin/inspect/address.rs @@ -1,5 +1,5 @@ use zcash_address::{ - unified::{self, Container, Encoding}, + unified::{self, Encoding}, ConversionError, ToAddress, ZcashAddress, }; use zcash_protocol::consensus::NetworkType; @@ -114,14 +114,14 @@ pub(crate) fn inspect(addr: ZcashAddress) { match addr.kind { AddressKind::Unified(ua) => { eprintln!(" - Receivers:"); - for receiver in ua.items() { + for receiver in ua.receivers() { match receiver { unified::Receiver::Orchard(data) => { eprintln!( " - Orchard ({})", - unified::Address::try_from_items(vec![ + unified::Address::try_from_items(vec![unified::Item::Data( unified::Receiver::Orchard(data) - ]) + )]) .unwrap() .encode(&addr.net) ); diff --git a/devtools/src/bin/inspect/keys.rs b/devtools/src/bin/inspect/keys.rs index 969adf19d..7b6e6ca9e 100644 --- a/devtools/src/bin/inspect/keys.rs +++ b/devtools/src/bin/inspect/keys.rs @@ -150,7 +150,9 @@ pub(crate) fn inspect_mnemonic(mnemonic: bip0039::Mnemonic, context: Option { + eprintln!(" - Expiry Height: {}", h); + } + unified::MetadataItem::ExpiryTime(t) => { + eprintln!( + " - Expiry Time: {}", + i64::try_from(*t) + .ok() + .and_then(|secs| DateTime::from_timestamp(secs, 0)) + .map_or(format!("Invalid expiry timestamp: {}", t), |t| t + .to_rfc3339()) + ); + } + unified::MetadataItem::Unknown { typecode, data } => { + eprintln!(" - Unknown Metadata Item"); + eprintln!(" - Typecode: {}", typecode); + eprintln!(" - Payload: {}", hex::encode(data)); + } + } +} + pub(crate) fn inspect_ufvk(ufvk: unified::Ufvk, network: NetworkType) { eprintln!("Unified full viewing key"); eprintln!( @@ -17,34 +42,41 @@ pub(crate) fn inspect_ufvk(ufvk: unified::Ufvk, network: NetworkType) { } ); eprintln!(" - Items:"); - for item in ufvk.items() { + for item in ufvk.items_as_parsed() { match item { - unified::Fvk::Orchard(data) => { - eprintln!( - " - Orchard ({})", - unified::Ufvk::try_from_items(vec![unified::Fvk::Orchard(data)]) + unified::Item::Data(d) => match d { + unified::Fvk::Orchard(data) => { + eprintln!( + " - Orchard ({})", + unified::Ufvk::try_from_items(vec![unified::Item::Data( + unified::Fvk::Orchard(*data) + )]) .unwrap() .encode(&network) - ); - } - unified::Fvk::Sapling(data) => { - eprintln!( - " - Sapling ({})", - bech32::encode::( - Hrp::parse_unchecked(network.hrp_sapling_extended_full_viewing_key()), - &data - ) - .unwrap(), - ); - } - unified::Fvk::P2pkh(data) => { - eprintln!(" - Transparent P2PKH"); - eprintln!(" - Payload: {}", hex::encode(data)); - } - unified::Fvk::Unknown { typecode, data } => { - eprintln!(" - Unknown"); - eprintln!(" - Typecode: {}", typecode); - eprintln!(" - Payload: {}", hex::encode(data)); + ); + } + unified::Fvk::Sapling(data) => { + eprintln!( + " - Sapling ({})", + bech32::encode::( + Hrp::parse_unchecked(network.hrp_sapling_extended_full_viewing_key()), + data + ) + .unwrap(), + ); + } + unified::Fvk::P2pkh(data) => { + eprintln!(" - Transparent P2PKH"); + eprintln!(" - Payload: {}", hex::encode(data)); + } + unified::Fvk::Unknown { typecode, data } => { + eprintln!(" - Unknown"); + eprintln!(" - Typecode: {}", typecode); + eprintln!(" - Payload: {}", hex::encode(data)); + } + }, + unified::Item::Metadata(m) => { + inspect_metadata_item(m); } } } @@ -61,29 +93,34 @@ pub(crate) fn inspect_uivk(uivk: unified::Uivk, network: NetworkType) { } ); eprintln!(" - Items:"); - for item in uivk.items() { + for item in uivk.items_as_parsed() { match item { - unified::Ivk::Orchard(data) => { - eprintln!( - " - Orchard ({})", - unified::Uivk::try_from_items(vec![unified::Ivk::Orchard(data)]) + unified::Item::Data(d) => match d { + unified::Ivk::Orchard(data) => { + eprintln!( + " - Orchard ({})", + unified::Uivk::try_from_items(vec![unified::Item::Data( + unified::Ivk::Orchard(*data) + )]) .unwrap() .encode(&network) - ); - } - unified::Ivk::Sapling(data) => { - eprintln!(" - Sapling"); - eprintln!(" - Payload: {}", hex::encode(data)); - } - unified::Ivk::P2pkh(data) => { - eprintln!(" - Transparent P2PKH"); - eprintln!(" - Payload: {}", hex::encode(data)); - } - unified::Ivk::Unknown { typecode, data } => { - eprintln!(" - Unknown"); - eprintln!(" - Typecode: {}", typecode); - eprintln!(" - Payload: {}", hex::encode(data)); - } + ); + } + unified::Ivk::Sapling(data) => { + eprintln!(" - Sapling"); + eprintln!(" - Payload: {}", hex::encode(data)); + } + unified::Ivk::P2pkh(data) => { + eprintln!(" - Transparent P2PKH"); + eprintln!(" - Payload: {}", hex::encode(data)); + } + unified::Ivk::Unknown { typecode, data } => { + eprintln!(" - Unknown Data Item"); + eprintln!(" - Typecode: {}", typecode); + eprintln!(" - Payload: {}", hex::encode(data)); + } + }, + unified::Item::Metadata(m) => inspect_metadata_item(m), } } } diff --git a/devtools/src/bin/inspect/transaction.rs b/devtools/src/bin/inspect/transaction.rs index e259a17b0..12ab874ea 100644 --- a/devtools/src/bin/inspect/transaction.rs +++ b/devtools/src/bin/inspect/transaction.rs @@ -540,8 +540,8 @@ pub(crate) fn inspect( // Construct a single-receiver UA. let zaddr = ZcashAddress::from_unified( net, - unified::Address::try_from_items(vec![unified::Receiver::Orchard( - addr.to_raw_address_bytes(), + unified::Address::try_from_items(vec![unified::Item::Data( + unified::Receiver::Orchard(addr.to_raw_address_bytes()), )]) .unwrap(), ); diff --git a/zcash_client_backend/src/data_api/testing/orchard.rs b/zcash_client_backend/src/data_api/testing/orchard.rs index d07f14ba4..4ca673eb2 100644 --- a/zcash_client_backend/src/data_api/testing/orchard.rs +++ b/zcash_client_backend/src/data_api/testing/orchard.rs @@ -75,7 +75,6 @@ impl ShieldedPoolTester for OrchardPoolTester { None, None, ) - .unwrap() .into() } @@ -155,9 +154,7 @@ impl ShieldedPoolTester for OrchardPoolTester { return result.map(|(note, addr, memo)| { ( Note::Orchard(note), - UnifiedAddress::from_receivers(Some(addr), None, None) - .unwrap() - .into(), + UnifiedAddress::from_receivers(Some(addr), None, None).into(), MemoBytes::from_bytes(&memo).expect("correct length"), ) }); diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index d9d25c2b9..fe2e24a40 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -987,7 +987,8 @@ mod tests { // Unified addresses at the time of the addition of migrations did not contain an // Orchard component. - let ua_request = UnifiedAddressRequest::unsafe_new(Omit, Require, UA_TRANSPARENT); + let ua_request = + UnifiedAddressRequest::unsafe_new_without_expiry(Omit, Require, UA_TRANSPARENT); let address_str = Address::Unified( ufvk.default_address(Some(ua_request)) .expect("A valid default address exists for the UFVK") @@ -1114,7 +1115,8 @@ mod tests { assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork)); // hardcoded with knowledge of what's coming next - let ua_request = UnifiedAddressRequest::unsafe_new(Omit, Require, Require); + let ua_request = + UnifiedAddressRequest::unsafe_new_without_expiry(Omit, Require, Require); db_data .get_next_available_address(account_id, Some(ua_request)) .unwrap() diff --git a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs index a7403c76d..59d3b340c 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs @@ -441,7 +441,7 @@ mod tests { let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap(); let ufvk = usk.to_unified_full_viewing_key(); let (ua, _) = ufvk - .default_address(Some(UnifiedAddressRequest::unsafe_new( + .default_address(Some(UnifiedAddressRequest::unsafe_new_without_expiry( Omit, Require, UA_TRANSPARENT, diff --git a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs index 64b057a47..4468fe628 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs @@ -91,7 +91,7 @@ impl RusqliteMigration for Migration

{ )); }; let (expected_address, idx) = ufvk.default_address(Some( - UnifiedAddressRequest::unsafe_new(Omit, Require, UA_TRANSPARENT), + UnifiedAddressRequest::unsafe_new_without_expiry(Omit, Require, UA_TRANSPARENT), ))?; if decoded_address != expected_address { return Err(WalletMigrationError::CorruptedData(format!( @@ -163,7 +163,7 @@ impl RusqliteMigration for Migration

{ )?; let (address, d_idx) = ufvk.default_address(Some( - UnifiedAddressRequest::unsafe_new(Omit, Require, UA_TRANSPARENT), + UnifiedAddressRequest::unsafe_new_without_expiry(Omit, Require, UA_TRANSPARENT), ))?; insert_address(transaction, &self.params, account, d_idx, &address)?; } diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs b/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs index 633fc37ae..6bc87eff2 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/ensure_orchard_ua_receiver.rs @@ -65,9 +65,12 @@ impl RusqliteMigration for Migration

{ })? }; - let (default_addr, diversifier_index) = uivk.default_address(Some( - UnifiedAddressRequest::unsafe_new(UA_ORCHARD, Require, UA_TRANSPARENT), - ))?; + let (default_addr, diversifier_index) = + uivk.default_address(Some(UnifiedAddressRequest::unsafe_new_without_expiry( + UA_ORCHARD, + Require, + UA_TRANSPARENT, + )))?; let mut di_be = *diversifier_index.as_bytes(); di_be.reverse(); @@ -142,7 +145,7 @@ mod tests { .unwrap(); let (addr, diversifier_index) = ufvk - .default_address(Some(UnifiedAddressRequest::unsafe_new( + .default_address(Some(UnifiedAddressRequest::unsafe_new_without_expiry( Omit, Require, UA_TRANSPARENT, diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs index 305b0f5eb..defacaaf6 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs @@ -85,7 +85,8 @@ impl RusqliteMigration for Migration

{ // our second assumption above, and we report this as corrupted data. let mut seed_is_relevant = false; - let ua_request = UnifiedAddressRequest::unsafe_new(Omit, Require, UA_TRANSPARENT); + let ua_request = + UnifiedAddressRequest::unsafe_new_without_expiry(Omit, Require, UA_TRANSPARENT); let mut rows = stmt_fetch_accounts.query([])?; while let Some(row) = rows.next()? { // We only need to check for the presence of the seed if we have keys that diff --git a/zcash_client_sqlite/src/wallet/transparent.rs b/zcash_client_sqlite/src/wallet/transparent.rs index 55844cb71..639c9193d 100644 --- a/zcash_client_sqlite/src/wallet/transparent.rs +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -9,7 +9,7 @@ use ::transparent::{ bundle::{OutPoint, TxOut}, keys::{IncomingViewingKey, NonHardenedChildIndex}, }; -use zcash_address::unified::{Ivk, Uivk}; +use zcash_address::unified::{Item, Ivk, Uivk}; use zcash_client_backend::{ data_api::{AccountBalance, TransactionDataRequest}, wallet::{TransparentAddressMetadata, WalletTransparentOutput}, @@ -152,9 +152,9 @@ pub(crate) fn uivk_legacy_transparent_address( } // Derive the default transparent address (if it wasn't already part of a derived UA). - for item in uivk.items() { - if let Ivk::P2pkh(tivk_bytes) = item { - let tivk = ExternalIvk::deserialize(&tivk_bytes)?; + for item in uivk.items_as_parsed() { + if let Item::Data(Ivk::P2pkh(tivk_bytes)) = item { + let tivk = ExternalIvk::deserialize(tivk_bytes)?; return Ok(Some(tivk.default_address())); } } diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index ea7d673a4..65d5c668b 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -23,11 +23,38 @@ and this library adheres to Rust's notion of accordingly. In addition, request construction methods that previously returned `None` to indicate an attempt to generate an invalid request now return `Err(())` +- `zcash_keys::address::UnifiedAddress::{ + expiry_height, set_expiry_height, unset_expiry_height, + expiry_time, set_expiry_time, unset_expiry_time, + unknown_data, unknown_metadata + }` +- `zcash_keys::keys::UnifiedFullViewingKey::{ + expiry_height, set_expiry_height, unset_expiry_height, + expiry_time, set_expiry_time, unset_expiry_time, + unknown_data, unknown_metadata + }` +- `zcash_keys::keys::UnifiedIncomingViewingKey::{ + expiry_height, set_expiry_height, unset_expiry_height, + expiry_time, set_expiry_time, unset_expiry_time, + unknown_data, unknown_metadata + }` +- `zcash_keys::address::UnifiedAddressRequest::unsafe_new_without_expiry` +- `zcash_keys::address::UnifiedAddressRequest::new` now takes additional + optional `expiry_height` and `expiry_time` arguments. +- `zcash_keys::address::UnifiedAddress::from_receivers` is now only available + under the `test-dependencies` feature flag. It should not be used for + non-test purposes as it can potentially generate addresses that contain no + receivers. +- `zcash_keys::keys::UnifiedSpendingKey::default_address` is now failable, and + now returns a `Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError>`. ### Removed - `zcash_keys::keys::UnifiedAddressRequest::all` (use `UnifiedAddressRequest::ALLOW_ALL` or `UnifiedFullViewingKey::to_address_request` instead) +- `zcash_keys::address::UnifiedAddress::unknown` (use `unknown_data` instead.) +- `zcash_keys::address::UnifiedAddressRequest::unsafe_new` (use + `UnifiedAddressRequest::unsafe_new_without_expiry` instead) ## [0.6.0] - 2024-12-16 diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index 3a196ecf9..83955e597 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -5,14 +5,16 @@ use alloc::vec::Vec; use transparent::address::TransparentAddress; use zcash_address::{ - unified::{self, Container, Encoding, Typecode}, + unified::{self, Container, DataTypecode, Encoding, Item, Typecode}, ConversionError, ToAddress, TryFromRawAddress, ZcashAddress, }; -use zcash_protocol::consensus::{self, NetworkType}; +use zcash_protocol::{ + consensus::{self, BlockHeight, NetworkType}, + PoolType, ShieldedProtocol, +}; #[cfg(feature = "sapling")] use sapling::PaymentAddress; -use zcash_protocol::{PoolType, ShieldedProtocol}; /// A Unified Address. #[derive(Clone, Debug, PartialEq, Eq)] @@ -22,7 +24,10 @@ pub struct UnifiedAddress { #[cfg(feature = "sapling")] sapling: Option, transparent: Option, - unknown: Vec<(u32, Vec)>, + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, } impl TryFrom for UnifiedAddress { @@ -34,14 +39,16 @@ impl TryFrom for UnifiedAddress { #[cfg(feature = "sapling")] let mut sapling = None; let mut transparent = None; - - let mut unknown: Vec<(u32, Vec)> = vec![]; + let mut unknown_data = 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 // receivers we support from the unknown receivers. for item in ua.items_as_parsed() { match item { - unified::Receiver::Orchard(data) => { + Item::Data(unified::Receiver::Orchard(data)) => { #[cfg(feature = "orchard")] { orchard = Some( @@ -51,11 +58,11 @@ impl TryFrom for UnifiedAddress { } #[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")] { sapling = Some( @@ -65,20 +72,26 @@ impl TryFrom for UnifiedAddress { } #[cfg(not(feature = "sapling"))] { - unknown.push((unified::Typecode::Sapling.into(), data.to_vec())); + unknown_data.push((unified::Typecode::SAPLING.into(), data.to_vec())); } } - - unified::Receiver::P2pkh(data) => { + Item::Data(unified::Receiver::P2pkh(data)) => { transparent = Some(TransparentAddress::PublicKeyHash(*data)); } - - unified::Receiver::P2sh(data) => { + Item::Data(unified::Receiver::P2sh(data)) => { transparent = Some(TransparentAddress::ScriptHash(*data)); } - - unified::Receiver::Unknown { typecode, data } => { - unknown.push((*typecode, data.clone())); + Item::Data(unified::Receiver::Unknown { typecode, data }) => { + unknown_data.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())); } } } @@ -89,7 +102,10 @@ impl TryFrom for UnifiedAddress { #[cfg(feature = "sapling")] sapling, transparent, - unknown, + unknown_data, + expiry_height, + expiry_time, + unknown_metadata, }) } } @@ -97,36 +113,43 @@ impl TryFrom for UnifiedAddress { impl UnifiedAddress { /// Constructs a Unified Address from a given set of receivers. /// - /// Returns `None` if the receivers would produce an invalid Unified Address (namely, - /// if no shielded receiver is provided). + /// This method is only available when the `test-dependencies` feature is enabled, as + /// derivation from the UFVK or UIVK, or deserialization from the serialized form should be + /// used instead. This method may generate invalid addresses that contain no receivers. + #[cfg(any(test, feature = "test-dependencies"))] pub fn from_receivers( #[cfg(feature = "orchard")] orchard: Option, #[cfg(feature = "sapling")] sapling: Option, transparent: Option, - // TODO: Add handling for address metadata items. - ) -> Option { - #[cfg(feature = "orchard")] - let has_orchard = orchard.is_some(); - #[cfg(not(feature = "orchard"))] - let has_orchard = false; + ) -> Self { + Self::new_internal( + #[cfg(feature = "orchard")] + orchard, + #[cfg(feature = "sapling")] + sapling, + transparent, + None, + None, + ) + } - #[cfg(feature = "sapling")] - let has_sapling = sapling.is_some(); - #[cfg(not(feature = "sapling"))] - let has_sapling = false; - - if has_orchard || has_sapling { - Some(Self { - #[cfg(feature = "orchard")] - orchard, - #[cfg(feature = "sapling")] - sapling, - transparent, - unknown: vec![], - }) - } else { - // UAs require at least one shielded receiver. - None + pub(crate) fn new_internal( + #[cfg(feature = "orchard")] orchard: Option, + #[cfg(feature = "sapling")] sapling: Option, + transparent: Option, + expiry_height: Option, + expiry_time: Option, + ) -> Self { + Self { + #[cfg(feature = "orchard")] + orchard, + #[cfg(feature = "sapling")] + sapling, + transparent, + unknown_data: vec![], + expiry_height, + expiry_time, + unknown_metadata: vec![], } } @@ -171,22 +194,66 @@ impl UnifiedAddress { self.transparent.as_ref() } - /// Returns the set of unknown receivers of the unified address. - pub fn unknown(&self) -> &[(u32, Vec)] { - &self.unknown + /// Returns any unknown data items parsed from the encoded form of the address. + pub fn unknown_data(&self) -> &[(u32, Vec)] { + self.unknown_data.as_ref() + } + + /// Returns the expiration height for this address. + pub fn expiry_height(&self) -> Option { + 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. + /// + /// The returned value is an integer representing a UTC time in seconds relative to the Unix + /// Epoch of 1970-01-01T00:00:00Z. + pub fn expiry_time(&self) -> Option { + self.expiry_time + } + + /// Sets the expiry time of this address. + /// + /// The argument should be an integer representing a UTC time in seconds relative to the Unix + /// Epoch of 1970-01-01T00:00:00Z. + 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)] { + self.unknown_metadata.as_ref() } fn to_address(&self, net: NetworkType) -> ZcashAddress { - let items = self - .unknown - .iter() - .map(|(typecode, data)| unified::Receiver::Unknown { - typecode: *typecode, - data: data.clone(), - }); + let data_items = + self.unknown_data + .iter() + .map(|(typecode, data)| unified::Receiver::Unknown { + typecode: *typecode, + data: data.clone(), + }); #[cfg(feature = "orchard")] - let items = items.chain( + let data_items = data_items.chain( self.orchard .as_ref() .map(|addr| addr.to_raw_address_bytes()) @@ -194,20 +261,38 @@ impl UnifiedAddress { ); #[cfg(feature = "sapling")] - let items = items.chain( + let data_items = data_items.chain( self.sapling .as_ref() .map(|pa| pa.to_bytes()) .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::ScriptHash(data) => unified::Receiver::P2sh(*data), })); - let ua = unified::Address::try_from_items(items.collect()) - .expect("UnifiedAddress should only be constructed safely"); + let meta_items = self + .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( + data_items + .map(Item::Data) + .chain(meta_items.map(Item::Metadata)) + .collect(), + ) + .expect("UnifiedAddress should only be constructed safely"); ZcashAddress::from_unified(net, ua) } @@ -220,17 +305,17 @@ impl UnifiedAddress { pub fn receiver_types(&self) -> Vec { let result = core::iter::empty(); #[cfg(feature = "orchard")] - let result = result.chain(self.orchard.map(|_| Typecode::Orchard)); + let result = result.chain(self.orchard.map(|_| Typecode::ORCHARD)); #[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 { - TransparentAddress::PublicKeyHash(_) => Typecode::P2pkh, - TransparentAddress::ScriptHash(_) => Typecode::P2sh, + TransparentAddress::PublicKeyHash(_) => Typecode::P2PKH, + TransparentAddress::ScriptHash(_) => Typecode::P2SH, })); let result = result.chain( - self.unknown() + self.unknown_data() .iter() - .map(|(typecode, _)| Typecode::Unknown(*typecode)), + .map(|(typecode, _)| Typecode::Data(DataTypecode::Unknown(*typecode))), ); result.collect() } @@ -259,7 +344,8 @@ impl Receiver { match self { #[cfg(feature = "orchard")] Receiver::Orchard(addr) => { - let receiver = unified::Receiver::Orchard(addr.to_raw_address_bytes()); + let receiver = + unified::Item::Data(unified::Receiver::Orchard(addr.to_raw_address_bytes())); let ua = unified::Address::try_from_items(vec![receiver]) .expect("A unified address may contain a single Orchard receiver."); ZcashAddress::from_unified(net, ua) @@ -462,7 +548,8 @@ pub mod testing { params: Network, request: UnifiedAddressRequest, ) -> impl Strategy { - arb_unified_spending_key(params).prop_map(move |k| k.default_address(Some(request)).0) + arb_unified_spending_key(params) + .prop_map(move |k| k.default_address(Some(request)).unwrap().0) } #[cfg(feature = "sapling")] @@ -490,13 +577,13 @@ mod tests { use zcash_address::test_vectors; use zcash_protocol::consensus::MAIN_NETWORK; - use super::{Address, UnifiedAddress}; + use super::Address; #[cfg(feature = "sapling")] use crate::keys::sapling; #[cfg(any(feature = "orchard", feature = "sapling"))] - use zip32::AccountId; + use {super::UnifiedAddress, zip32::AccountId}; #[test] #[cfg(any(feature = "orchard", feature = "sapling"))] @@ -519,26 +606,19 @@ mod tests { let transparent = None; #[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"))] - let ua = UnifiedAddress::from_receivers(sapling, transparent).unwrap(); + let ua = UnifiedAddress::new_internal(sapling, transparent, None, None); #[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_str = addr.encode(&MAIN_NETWORK); assert_eq!(Address::decode(&MAIN_NETWORK, &addr_str), Some(addr)); } - #[test] - #[cfg(not(any(feature = "orchard", feature = "sapling")))] - fn ua_round_trip() { - let transparent = None; - assert_eq!(UnifiedAddress::from_receivers(transparent), None) - } - #[test] fn ua_parsing() { for tv in test_vectors::UNIFIED { diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs index dc6a04daa..e59bd2ea9 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -3,8 +3,8 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::fmt::{self, Display}; -use zcash_address::unified::{self, Container, Encoding, Typecode, Ufvk, Uivk}; -use zcash_protocol::consensus; +use zcash_address::unified::{self, Container, Encoding, Item, MetadataItem, Typecode}; +use zcash_protocol::consensus::{self, BlockHeight}; use zip32::{AccountId, DiversifierIndex}; use crate::address::UnifiedAddress; @@ -266,7 +266,10 @@ impl UnifiedSpendingKey { sapling: Some(self.sapling.to_diversifiable_full_viewing_key()), #[cfg(feature = "orchard")] orchard: Some((&self.orchard).into()), - unknown: vec![], + unknown_data: vec![], + expiry_height: None, + expiry_time: None, + unknown_metadata: vec![], } } @@ -306,7 +309,7 @@ impl UnifiedSpendingKey { #[cfg(feature = "orchard")] { let orchard_key = self.orchard(); - CompactSize::write(&mut result, usize::try_from(Typecode::Orchard).unwrap()).unwrap(); + CompactSize::write(&mut result, usize::try_from(Typecode::ORCHARD).unwrap()).unwrap(); let orchard_key_bytes = orchard_key.to_bytes(); CompactSize::write(&mut result, orchard_key_bytes.len()).unwrap(); @@ -316,7 +319,7 @@ impl UnifiedSpendingKey { #[cfg(feature = "sapling")] { let sapling_key = self.sapling(); - CompactSize::write(&mut result, usize::try_from(Typecode::Sapling).unwrap()).unwrap(); + CompactSize::write(&mut result, usize::try_from(Typecode::SAPLING).unwrap()).unwrap(); let sapling_key_bytes = sapling_key.to_bytes(); CompactSize::write(&mut result, sapling_key_bytes.len()).unwrap(); @@ -326,7 +329,7 @@ impl UnifiedSpendingKey { #[cfg(feature = "transparent-inputs")] { let account_tkey = self.transparent(); - CompactSize::write(&mut result, usize::try_from(Typecode::P2pkh).unwrap()).unwrap(); + CompactSize::write(&mut result, usize::try_from(Typecode::P2PKH).unwrap()).unwrap(); let account_tkey_bytes = account_tkey.to_bytes(); CompactSize::write(&mut result, account_tkey_bytes.len()).unwrap(); @@ -342,6 +345,7 @@ impl UnifiedSpendingKey { #[allow(clippy::unnecessary_unwrap)] #[cfg(feature = "unstable")] pub fn from_bytes(era: Era, encoded: &[u8]) -> Result { + use zcash_address::unified::DataTypecode; let mut source = core2::io::Cursor::new(encoded); let decoded_era = source .read_u32::() @@ -361,21 +365,23 @@ impl UnifiedSpendingKey { loop { let tc = CompactSize::read_t::<_, u32>(&mut source) .map_err(|_| DecodingError::ReadError("typecode")) - .and_then(|v| Typecode::try_from(v).map_err(|_| DecodingError::TypecodeInvalid))?; + .and_then(|v| { + DataTypecode::try_from(v).map_err(|_| DecodingError::TypecodeInvalid) + })?; let len = CompactSize::read_t::<_, u32>(&mut source) .map_err(|_| DecodingError::ReadError("key length"))?; match tc { - Typecode::Orchard => { + DataTypecode::Orchard => { if len != 32 { - return Err(DecodingError::LengthMismatch(Typecode::Orchard, len)); + return Err(DecodingError::LengthMismatch(Typecode::ORCHARD, len)); } let mut key = [0u8; 32]; source .read_exact(&mut key) - .map_err(|_| DecodingError::InsufficientData(Typecode::Orchard))?; + .map_err(|_| DecodingError::InsufficientData(Typecode::ORCHARD))?; #[cfg(feature = "orchard")] { @@ -383,43 +389,43 @@ impl UnifiedSpendingKey { Option::::from( orchard::keys::SpendingKey::from_bytes(key), ) - .ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?, + .ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?, ); } } - Typecode::Sapling => { + DataTypecode::Sapling => { if len != 169 { - return Err(DecodingError::LengthMismatch(Typecode::Sapling, len)); + return Err(DecodingError::LengthMismatch(Typecode::SAPLING, len)); } let mut key = [0u8; 169]; source .read_exact(&mut key) - .map_err(|_| DecodingError::InsufficientData(Typecode::Sapling))?; + .map_err(|_| DecodingError::InsufficientData(Typecode::SAPLING))?; #[cfg(feature = "sapling")] { sapling = Some( sapling::ExtendedSpendingKey::from_bytes(&key) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::Sapling))?, + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::SAPLING))?, ); } } - Typecode::P2pkh => { + DataTypecode::P2pkh => { if len != 74 { - return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len)); + return Err(DecodingError::LengthMismatch(Typecode::P2PKH, len)); } let mut key = [0u8; 74]; source .read_exact(&mut key) - .map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?; + .map_err(|_| DecodingError::InsufficientData(Typecode::P2PKH))?; #[cfg(feature = "transparent-inputs")] { transparent = Some( transparent::keys::AccountPrivKey::from_bytes(&key) - .ok_or(DecodingError::KeyDataInvalid(Typecode::P2pkh))?, + .ok_or(DecodingError::KeyDataInvalid(Typecode::P2PKH))?, ); } } @@ -452,7 +458,7 @@ impl UnifiedSpendingKey { #[cfg(feature = "orchard")] orchard.unwrap(), ) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)); + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH)); } } } @@ -461,10 +467,8 @@ impl UnifiedSpendingKey { pub fn default_address( &self, request: Option, - ) -> (UnifiedAddress, DiversifierIndex) { - self.to_unified_full_viewing_key() - .default_address(request) - .unwrap() + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + self.to_unified_full_viewing_key().default_address(request) } #[cfg(all( @@ -597,6 +601,8 @@ pub struct UnifiedAddressRequest { orchard: ReceiverRequirement, sapling: ReceiverRequirement, p2pkh: ReceiverRequirement, + expiry_height: Option, + expiry_time: Option, } impl UnifiedAddressRequest { @@ -607,6 +613,8 @@ impl UnifiedAddressRequest { orchard: ReceiverRequirement, sapling: ReceiverRequirement, p2pkh: ReceiverRequirement, + expiry_height: Option, + expiry_time: Option, ) -> Result { use ReceiverRequirement::*; if orchard == Omit && sapling == Omit { @@ -616,6 +624,8 @@ impl UnifiedAddressRequest { orchard, sapling, p2pkh, + expiry_height, + expiry_time, }) } } @@ -623,7 +633,7 @@ impl UnifiedAddressRequest { /// Constructs a new unified address request that allows a receiver of each type. pub const ALLOW_ALL: UnifiedAddressRequest = { use ReceiverRequirement::*; - Self::unsafe_new(Allow, Allow, Allow) + Self::unsafe_new_without_expiry(Allow, Allow, Allow) }; /// Constructs a new unified address request that includes only the receivers that are allowed @@ -633,13 +643,27 @@ impl UnifiedAddressRequest { let orchard = self.orchard.intersect(other.orchard)?; let sapling = self.sapling.intersect(other.sapling)?; let p2pkh = self.p2pkh.intersect(other.p2pkh)?; - Self::new(orchard, sapling, p2pkh) + Self::new( + orchard, + sapling, + p2pkh, + self.expiry_height + .zip(other.expiry_height) + .map(|(s, o)| std::cmp::min(s, o)) + .or(self.expiry_height) + .or(other.expiry_height), + self.expiry_time + .zip(other.expiry_time) + .map(|(s, o)| std::cmp::min(s, o)) + .or(self.expiry_time) + .or(other.expiry_time), + ) } /// Construct a new unified address request from its constituent parts. /// /// Panics: at least one of `orchard` or `sapling` must be allowed. - pub const fn unsafe_new( + pub const fn unsafe_new_without_expiry( orchard: ReceiverRequirement, sapling: ReceiverRequirement, p2pkh: ReceiverRequirement, @@ -653,6 +677,8 @@ impl UnifiedAddressRequest { orchard, sapling, p2pkh, + expiry_height: None, + expiry_time: None, } } } @@ -673,7 +699,10 @@ pub struct UnifiedFullViewingKey { sapling: Option, #[cfg(feature = "orchard")] orchard: Option, - unknown: Vec<(u32, Vec)>, + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, } impl UnifiedFullViewingKey { @@ -701,6 +730,9 @@ impl UnifiedFullViewingKey { // We don't currently allow constructing new UFVKs with unknown items, but we store // this to allow parsing such UFVKs. vec![], + None, + None, + vec![], ) } @@ -718,6 +750,9 @@ impl UnifiedFullViewingKey { // We don't currently allow constructing new UFVKs with unknown items, but we store // this to allow parsing such UFVKs. vec![], + None, + None, + vec![], ) } @@ -735,6 +770,9 @@ impl UnifiedFullViewingKey { // We don't currently allow constructing new UFVKs with unknown items, but we store // this to allow parsing such UFVKs. vec![], + None, + None, + vec![], ) } @@ -746,7 +784,10 @@ impl UnifiedFullViewingKey { >, #[cfg(feature = "sapling")] sapling: Option, #[cfg(feature = "orchard")] orchard: Option, - unknown: Vec<(u32, Vec)>, + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, ) -> Result { // Verify that IVK derivation succeeds; we don't want to construct a UFVK // that can't derive transparent addresses. @@ -763,7 +804,10 @@ impl UnifiedFullViewingKey { sapling, #[cfg(feature = "orchard")] orchard, - unknown, + unknown_data, + expiry_height, + expiry_time, + unknown_metadata, }) } @@ -771,7 +815,8 @@ impl UnifiedFullViewingKey { /// /// [ZIP 316]: https://zips.z.cash/zip-0316 pub fn decode(params: &P, encoding: &str) -> Result { - let (net, ufvk) = unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?; + let (net, ufvk) = + zcash_address::unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?; let expected_net = params.network_type(); if net != expected_net { return Err(format!( @@ -786,64 +831,71 @@ impl UnifiedFullViewingKey { /// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding. /// /// [ZIP 316]: https://zips.z.cash/zip-0316 - pub fn parse(ufvk: &Ufvk) -> Result { + pub fn parse(ufvk: &zcash_address::unified::Ufvk) -> Result { #[cfg(feature = "orchard")] let mut orchard = None; #[cfg(feature = "sapling")] let mut sapling = None; #[cfg(feature = "transparent-inputs")] let mut transparent = None; + let mut unknown_data = 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 // receivers we support from the unknown receivers. - let unknown = ufvk - .items_as_parsed() - .iter() - .filter_map(|receiver| match receiver { - #[cfg(feature = "orchard")] - unified::Fvk::Orchard(data) => orchard::keys::FullViewingKey::from_bytes(data) - .ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard)) - .map(|addr| { - orchard = Some(addr); - None - }) - .transpose(), - #[cfg(not(feature = "orchard"))] - unified::Fvk::Orchard(data) => Some(Ok::<_, DecodingError>(( - u32::from(unified::Typecode::Orchard), - data.to_vec(), - ))), - #[cfg(feature = "sapling")] - unified::Fvk::Sapling(data) => { - sapling::DiversifiableFullViewingKey::from_bytes(data) - .ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling)) - .map(|pa| { - sapling = Some(pa); - None - }) - .transpose() + for item in ufvk.items_as_parsed() { + match item { + Item::Data(unified::Fvk::Orchard(data)) => { + #[cfg(feature = "orchard")] + { + orchard = Some( + orchard::keys::FullViewingKey::from_bytes(data) + .ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?, + ); + } + + #[cfg(not(feature = "orchard"))] + unknown_data.push((unified::DataTypecode::Orchard.into(), data.to_vec())); } - #[cfg(not(feature = "sapling"))] - unified::Fvk::Sapling(data) => Some(Ok::<_, DecodingError>(( - u32::from(unified::Typecode::Sapling), - data.to_vec(), - ))), - #[cfg(feature = "transparent-inputs")] - unified::Fvk::P2pkh(data) => transparent::keys::AccountPubKey::deserialize(data) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)) - .map(|tfvk| { - transparent = Some(tfvk); - None - }) - .transpose(), - #[cfg(not(feature = "transparent-inputs"))] - unified::Fvk::P2pkh(data) => Some(Ok::<_, DecodingError>(( - u32::from(unified::Typecode::P2pkh), - data.to_vec(), - ))), - unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))), - }) - .collect::>()?; + Item::Data(unified::Fvk::Sapling(data)) => { + #[cfg(feature = "sapling")] + { + sapling = Some( + sapling::DiversifiableFullViewingKey::from_bytes(data) + .ok_or(DecodingError::KeyDataInvalid(Typecode::SAPLING))?, + ); + } + #[cfg(not(feature = "sapling"))] + unknown_data.push((unified::Typecode::SAPLING.into(), data.to_vec())); + } + Item::Data(unified::Fvk::P2pkh(data)) => { + #[cfg(feature = "transparent-inputs")] + { + transparent = Some( + transparent::keys::AccountPubKey::deserialize(data) + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))?, + ); + } + + #[cfg(not(feature = "transparent-inputs"))] + unknown_data.push((unified::DataTypecode::P2pkh.into(), data.to_vec())); + } + Item::Data(unified::Fvk::Unknown { typecode, data }) => { + unknown_data.push((*typecode, data.clone())); + } + Item::Metadata(MetadataItem::ExpiryHeight(h)) => { + expiry_height = Some(BlockHeight::from(*h)); + } + Item::Metadata(MetadataItem::ExpiryTime(t)) => { + expiry_time = Some(*t); + } + Item::Metadata(MetadataItem::Unknown { typecode, data }) => { + unknown_metadata.push((*typecode, data.clone())); + } + } + } Self::from_checked_parts( #[cfg(feature = "transparent-inputs")] @@ -852,9 +904,12 @@ impl UnifiedFullViewingKey { sapling, #[cfg(feature = "orchard")] orchard, - unknown, + unknown_data, + expiry_height, + expiry_time, + unknown_metadata, ) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh)) + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH)) } /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. @@ -863,37 +918,56 @@ impl UnifiedFullViewingKey { } /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. - fn to_ufvk(&self) -> Ufvk { - let items = core::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| { - unified::Fvk::Unknown { - typecode: *typecode, - data: data.clone(), - } - })); + fn to_ufvk(&self) -> zcash_address::unified::Ufvk { + let data_items = + std::iter::empty().chain(self.unknown_data.iter().map(|(typecode, data)| { + unified::Fvk::Unknown { + typecode: *typecode, + data: data.clone(), + } + })); #[cfg(feature = "orchard")] - let items = items.chain( + let data_items = data_items.chain( self.orchard .as_ref() .map(|fvk| fvk.to_bytes()) .map(unified::Fvk::Orchard), ); #[cfg(feature = "sapling")] - let items = items.chain( + let data_items = data_items.chain( self.sapling .as_ref() .map(|dfvk| dfvk.to_bytes()) .map(unified::Fvk::Sapling), ); #[cfg(feature = "transparent-inputs")] - let items = items.chain( + let data_items = data_items.chain( self.transparent .as_ref() .map(|tfvk| tfvk.serialize().try_into().unwrap()) .map(unified::Fvk::P2pkh), ); - unified::Ufvk::try_from_items(items.collect()) - .expect("UnifiedFullViewingKey should only be constructed safely") + let meta_items = std::iter::empty() + .chain(self.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)); + + zcash_address::unified::Ufvk::try_from_items( + data_items + .map(Item::Data) + .chain(meta_items.map(Item::Metadata)) + .collect(), + ) + .expect("UnifiedFullViewingKey should only be constructed safely") } /// Derives a Unified Incoming Viewing Key from this Unified Full Viewing Key. @@ -908,7 +982,11 @@ impl UnifiedFullViewingKey { sapling: self.sapling.as_ref().map(|s| s.to_external_ivk()), #[cfg(feature = "orchard")] orchard: self.orchard.as_ref().map(|o| o.to_ivk(Scope::External)), - unknown: Vec::new(), + expiry_height: self.expiry_height, + expiry_time: self.expiry_time, + // We cannot translate unknown data or metadata items, as they may not be relevant to the IVK + unknown_data: vec![], + unknown_metadata: vec![], } } @@ -931,6 +1009,52 @@ impl UnifiedFullViewingKey { self.orchard.as_ref() } + /// Returns any unknown data items parsed from the encoded form of the key. + pub fn unknown_data(&self) -> &[(u32, Vec)] { + self.unknown_data.as_ref() + } + + /// Returns the expiration height that will be used in addresses derived from this key. + pub fn expiry_height(&self) -> Option { + self.expiry_height + } + + /// Sets the expiration height that will be used in addresses derived from this key. + pub fn set_expiry_height(&mut self, height: BlockHeight) { + self.expiry_height = Some(height); + } + + /// Removes the expiration height from this key. + pub fn unset_expiry_height(&mut self) { + self.expiry_height = None; + } + + /// Returns the expiration time that will be used in addresses derived from this key. + /// + /// The returned value is an integer representing a UTC time in seconds relative to the Unix + /// Epoch of 1970-01-01T00:00:00Z. + pub fn expiry_time(&self) -> Option { + self.expiry_time + } + + /// Sets the expiration time that will be used in addresses derived from this key. + /// + /// The argument should be an integer representing a UTC time in seconds relative to the Unix + /// Epoch of 1970-01-01T00:00:00Z. + pub fn set_expiry_time(&mut self, time: u64) { + self.expiry_time = Some(time); + } + + /// Removes the expiration time from this key. + pub fn unset_expiry_time(&mut self) { + self.expiry_time = None; + } + + /// Returns any unknown metadata items parsed from the encoded form of the key. + pub fn unknown_metadata(&self) -> &[(u32, Vec)] { + self.unknown_metadata.as_ref() + } + /// Attempts to derive the Unified Address for the given diversifier index and receiver types. /// If `request` is None, the address should be derived to contain a receiver for each item in /// this UFVK. @@ -983,8 +1107,10 @@ pub struct UnifiedIncomingViewingKey { sapling: Option<::sapling::zip32::IncomingViewingKey>, #[cfg(feature = "orchard")] orchard: Option, - /// Stores the unrecognized elements of the unified encoding. - unknown: Vec<(u32, Vec)>, + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, } impl UnifiedIncomingViewingKey { @@ -998,7 +1124,10 @@ impl UnifiedIncomingViewingKey { #[cfg(feature = "transparent-inputs")] transparent: Option, #[cfg(feature = "sapling")] sapling: Option<::sapling::zip32::IncomingViewingKey>, #[cfg(feature = "orchard")] orchard: Option, - // TODO: Implement construction of UIVKs with metadata items. + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, ) -> UnifiedIncomingViewingKey { UnifiedIncomingViewingKey { #[cfg(feature = "transparent-inputs")] @@ -1007,9 +1136,10 @@ impl UnifiedIncomingViewingKey { sapling, #[cfg(feature = "orchard")] orchard, - // We don't allow constructing new UFVKs with unknown items, but we store - // this to allow parsing such UFVKs. - unknown: vec![], + unknown_data, + expiry_height, + expiry_time, + unknown_metadata, } } @@ -1017,7 +1147,7 @@ impl UnifiedIncomingViewingKey { /// /// [ZIP 316]: https://zips.z.cash/zip-0316 pub fn decode(params: &P, encoding: &str) -> Result { - let (net, ufvk) = unified::Uivk::decode(encoding).map_err(|e| e.to_string())?; + let (net, uivk) = unified::Uivk::decode(encoding).map_err(|e| e.to_string())?; let expected_net = params.network_type(); if net != expected_net { return Err(format!( @@ -1026,62 +1156,73 @@ impl UnifiedIncomingViewingKey { )); } - Self::parse(&ufvk).map_err(|e| e.to_string()) + Self::parse(&uivk).map_err(|e| e.to_string()) } /// Constructs a unified incoming viewing key from a parsed unified encoding. - fn parse(uivk: &Uivk) -> Result { + fn parse(uivk: &zcash_address::unified::Uivk) -> Result { #[cfg(feature = "orchard")] let mut orchard = None; #[cfg(feature = "sapling")] let mut sapling = None; #[cfg(feature = "transparent-inputs")] let mut transparent = None; - - let mut unknown = vec![]; + let mut unknown_data = 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 // receivers we support from the unknown receivers. for receiver in uivk.items_as_parsed() { match receiver { - unified::Ivk::Orchard(data) => { + Item::Data(unified::Ivk::Orchard(data)) => { #[cfg(feature = "orchard")] { orchard = Some( Option::from(orchard::keys::IncomingViewingKey::from_bytes(data)) - .ok_or(DecodingError::KeyDataInvalid(Typecode::Orchard))?, + .ok_or(DecodingError::KeyDataInvalid(Typecode::ORCHARD))?, ); } #[cfg(not(feature = "orchard"))] - unknown.push((u32::from(unified::Typecode::Orchard), data.to_vec())); + unknown_data.push((u32::from(unified::Typecode::ORCHARD), data.to_vec())); } - unified::Ivk::Sapling(data) => { + Item::Data(unified::Ivk::Sapling(data)) => { #[cfg(feature = "sapling")] { sapling = Some( Option::from(::sapling::zip32::IncomingViewingKey::from_bytes(data)) - .ok_or(DecodingError::KeyDataInvalid(Typecode::Sapling))?, + .ok_or(DecodingError::KeyDataInvalid(Typecode::SAPLING))?, ); } #[cfg(not(feature = "sapling"))] - unknown.push((u32::from(unified::Typecode::Sapling), data.to_vec())); + unknown_data.push((u32::from(unified::Typecode::SAPLING), data.to_vec())); } - unified::Ivk::P2pkh(data) => { + Item::Data(unified::Ivk::P2pkh(data)) => { #[cfg(feature = "transparent-inputs")] { transparent = Some( transparent::keys::ExternalIvk::deserialize(data) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh))?, + .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH))?, ); } #[cfg(not(feature = "transparent-inputs"))] - unknown.push((u32::from(unified::Typecode::P2pkh), data.to_vec())); + unknown_data.push((u32::from(unified::Typecode::P2PKH), data.to_vec())); + } + Item::Data(unified::Ivk::Unknown { typecode, data }) => { + unknown_data.push((*typecode, data.clone())); + } + Item::Metadata(MetadataItem::ExpiryHeight(h)) => { + expiry_height = Some(BlockHeight::from(*h)); } - unified::Ivk::Unknown { typecode, data } => { - unknown.push((*typecode, data.clone())); + Item::Metadata(MetadataItem::ExpiryTime(t)) => { + expiry_time = Some(*t); + } + Item::Metadata(MetadataItem::Unknown { typecode, data }) => { + unknown_metadata.push((*typecode, data.clone())); } } } @@ -1093,7 +1234,10 @@ impl UnifiedIncomingViewingKey { sapling, #[cfg(feature = "orchard")] orchard, - unknown, + unknown_data, + expiry_height, + expiry_time, + unknown_metadata, }) } @@ -1103,37 +1247,56 @@ impl UnifiedIncomingViewingKey { } /// Converts this unified incoming viewing key to a unified encoding. - fn render(&self) -> Uivk { - let items = core::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| { - unified::Ivk::Unknown { - typecode: *typecode, - data: data.clone(), - } - })); + fn render(&self) -> zcash_address::unified::Uivk { + let data_items = + std::iter::empty().chain(self.unknown_data.iter().map(|(typecode, data)| { + unified::Ivk::Unknown { + typecode: *typecode, + data: data.clone(), + } + })); #[cfg(feature = "orchard")] - let items = items.chain( + let data_items = data_items.chain( self.orchard .as_ref() .map(|ivk| ivk.to_bytes()) .map(unified::Ivk::Orchard), ); #[cfg(feature = "sapling")] - let items = items.chain( + let data_items = data_items.chain( self.sapling .as_ref() .map(|divk| divk.to_bytes()) .map(unified::Ivk::Sapling), ); #[cfg(feature = "transparent-inputs")] - let items = items.chain( + let data_items = data_items.chain( self.transparent .as_ref() .map(|tivk| tivk.serialize().try_into().unwrap()) .map(unified::Ivk::P2pkh), ); - unified::Uivk::try_from_items(items.collect()) - .expect("UnifiedIncomingViewingKey should only be constructed safely.") + let meta_items = std::iter::empty() + .chain(self.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)); + + zcash_address::unified::Uivk::try_from_items( + data_items + .map(Item::Data) + .chain(meta_items.map(Item::Metadata)) + .collect(), + ) + .expect("UnifiedIncomingViewingKey should only be constructed safely.") } /// Returns the Transparent external IVK, if present. @@ -1154,6 +1317,52 @@ impl UnifiedIncomingViewingKey { &self.orchard } + /// Returns any unknown data items parsed from the encoded form of the key. + pub fn unknown_data(&self) -> &[(u32, Vec)] { + self.unknown_data.as_ref() + } + + /// Returns the expiration height that will be used in addresses derived from this key. + pub fn expiry_height(&self) -> Option { + self.expiry_height + } + + /// Sets the expiration height that will be used in addresses derived from this key. + pub fn set_expiry_height(&mut self, height: BlockHeight) { + self.expiry_height = Some(height); + } + + /// Removes the expiration height from this key. + pub fn unset_expiry_height(&mut self) { + self.expiry_height = None; + } + + /// Returns the expiration time that will be used in addresses derived from this key. + /// + /// The returned value is an integer representing a UTC time in seconds relative to the Unix + /// Epoch of 1970-01-01T00:00:00Z. + pub fn expiry_time(&self) -> Option { + self.expiry_time + } + + /// Sets the expiration time that will be used in addresses derived from this key. + /// + /// The argument should be an integer representing a UTC time in seconds relative to the Unix + /// Epoch of 1970-01-01T00:00:00Z. + pub fn set_expiry_time(&mut self, time: u64) { + self.expiry_time = Some(time); + } + + /// Removes the expiration time from this key. + pub fn unset_expiry_time(&mut self) { + self.expiry_time = None; + } + + /// Returns any unknown metadata items parsed from the encoded form of the key. + pub fn unknown_metadata(&self) -> &[(u32, Vec)] { + self.unknown_metadata.as_ref() + } + /// Attempts to derive the Unified Address for the given diversifier index and receiver types. /// If `request` is None, the address will be derived to contain a receiver for each item in /// this UFVK. @@ -1177,7 +1386,7 @@ impl UnifiedIncomingViewingKey { #[cfg(not(feature = "orchard"))] if request.orchard == Require { return Err(AddressGenerationError::ReceiverTypeNotSupported( - Typecode::Orchard, + Typecode::ORCHARD, )); } @@ -1186,7 +1395,7 @@ impl UnifiedIncomingViewingKey { let orchard_j = orchard::keys::DiversifierIndex::from(*_j.as_bytes()); orchard = Some(oivk.address_at(orchard_j)) } else if request.orchard == Require { - return Err(AddressGenerationError::KeyNotAvailable(Typecode::Orchard)); + return Err(AddressGenerationError::KeyNotAvailable(Typecode::ORCHARD)); } } @@ -1196,7 +1405,7 @@ impl UnifiedIncomingViewingKey { #[cfg(not(feature = "sapling"))] if request.sapling == Require { return Err(AddressGenerationError::ReceiverTypeNotSupported( - Typecode::Sapling, + Typecode::SAPLING, )); } @@ -1213,7 +1422,7 @@ impl UnifiedIncomingViewingKey { _ => Ok(None), }?; } else if request.sapling == Require { - return Err(AddressGenerationError::KeyNotAvailable(Typecode::Sapling)); + return Err(AddressGenerationError::KeyNotAvailable(Typecode::SAPLING)); } } @@ -1223,7 +1432,7 @@ impl UnifiedIncomingViewingKey { #[cfg(not(feature = "transparent-inputs"))] if request.p2pkh == Require { return Err(AddressGenerationError::ReceiverTypeNotSupported( - Typecode::P2pkh, + Typecode::P2PKH, )); } @@ -1242,20 +1451,29 @@ impl UnifiedIncomingViewingKey { _ => Ok(None), }?; } else if request.p2pkh == Require { - return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2pkh)); + return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2PKH)); } } #[cfg(not(feature = "transparent-inputs"))] let transparent = None; - UnifiedAddress::from_receivers( + Ok(UnifiedAddress::new_internal( #[cfg(feature = "orchard")] orchard, #[cfg(feature = "sapling")] sapling, transparent, - ) - .ok_or(AddressGenerationError::ShieldedReceiverRequired) + self.expiry_height + .zip(request.expiry_height) + .map(|(l, r)| std::cmp::min(l, r)) + .or(self.expiry_height) + .or(request.expiry_height), + self.expiry_time + .zip(request.expiry_time) + .map(|(l, r)| std::cmp::min(l, r)) + .or(self.expiry_time) + .or(request.expiry_time), + )) } /// Searches the diversifier space starting at diversifier index `j` for one which will produce @@ -1302,6 +1520,8 @@ impl UnifiedIncomingViewingKey { Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => { if j.increment().is_err() { return Err(AddressGenerationError::DiversifierSpaceExhausted); + } else { + continue; } } Err(other) => { @@ -1349,7 +1569,13 @@ impl UnifiedIncomingViewingKey { p2pkh = Require; } - UnifiedAddressRequest::new(orchard, sapling, p2pkh) + UnifiedAddressRequest::new( + orchard, + sapling, + p2pkh, + self.expiry_height, + self.expiry_time, + ) } } @@ -1387,7 +1613,7 @@ mod tests { #[cfg(any(feature = "sapling", feature = "orchard"))] use { super::{UnifiedFullViewingKey, UnifiedIncomingViewingKey}, - zcash_address::unified::{Encoding, Uivk}, + zcash_address::unified::Encoding, }; #[cfg(feature = "orchard")] @@ -1525,13 +1751,13 @@ mod tests { feature = "sapling", feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 0); + assert_eq!(decoded_with_t.unknown_data.len(), 0); #[cfg(all( feature = "orchard", feature = "sapling", not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); // Orchard enabled #[cfg(all( @@ -1539,13 +1765,13 @@ mod tests { not(feature = "sapling"), feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); #[cfg(all( feature = "orchard", not(feature = "sapling"), not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 2); + assert_eq!(decoded_with_t.unknown_data.len(), 2); // Sapling enabled #[cfg(all( @@ -1553,13 +1779,13 @@ mod tests { feature = "sapling", feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); #[cfg(all( not(feature = "orchard"), feature = "sapling", not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 2); + assert_eq!(decoded_with_t.unknown_data.len(), 2); } #[test] @@ -1590,7 +1816,9 @@ mod tests { let ua = ufvk .address( d_idx, - Some(UnifiedAddressRequest::unsafe_new(Omit, Require, Require)), + Some(UnifiedAddressRequest::unsafe_new_without_expiry( + Omit, Require, Require, + )), ) .unwrap_or_else(|err| { panic!( @@ -1653,6 +1881,10 @@ mod tests { sapling, #[cfg(feature = "orchard")] orchard, + vec![], + None, + None, + vec![], ); let encoded = uivk.render().encode(&NetworkType::Main); @@ -1673,7 +1905,7 @@ mod tests { assert_eq!(encoded, _encoded_no_t); } - let decoded = UnifiedIncomingViewingKey::parse(&Uivk::decode(&encoded).unwrap().1).unwrap(); + let decoded = UnifiedIncomingViewingKey::decode(&MAIN_NETWORK, &encoded).unwrap(); let reencoded = decoded.render().encode(&NetworkType::Main); assert_eq!(encoded, reencoded); @@ -1694,7 +1926,7 @@ mod tests { ); let decoded_with_t = - UnifiedIncomingViewingKey::parse(&Uivk::decode(encoded_with_t).unwrap().1).unwrap(); + UnifiedIncomingViewingKey::decode(&MAIN_NETWORK, encoded_with_t).unwrap(); #[cfg(feature = "transparent-inputs")] assert_eq!( decoded_with_t.transparent.map(|t| t.serialize()), @@ -1707,13 +1939,13 @@ mod tests { feature = "sapling", feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 0); + assert_eq!(decoded_with_t.unknown_data.len(), 0); #[cfg(all( feature = "orchard", feature = "sapling", not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); // Orchard enabled #[cfg(all( @@ -1721,13 +1953,13 @@ mod tests { not(feature = "sapling"), feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); #[cfg(all( feature = "orchard", not(feature = "sapling"), not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 2); + assert_eq!(decoded_with_t.unknown_data.len(), 2); // Sapling enabled #[cfg(all( @@ -1735,13 +1967,13 @@ mod tests { feature = "sapling", feature = "transparent-inputs" ))] - assert_eq!(decoded_with_t.unknown.len(), 1); + assert_eq!(decoded_with_t.unknown_data.len(), 1); #[cfg(all( not(feature = "orchard"), feature = "sapling", not(feature = "transparent-inputs") ))] - assert_eq!(decoded_with_t.unknown.len(), 2); + assert_eq!(decoded_with_t.unknown_data.len(), 2); } #[test] @@ -1774,7 +2006,9 @@ mod tests { let ua = uivk .address( d_idx, - Some(UnifiedAddressRequest::unsafe_new(Omit, Require, Require)), + Some(UnifiedAddressRequest::unsafe_new_without_expiry( + Omit, Require, Require, + )), ) .unwrap_or_else(|err| { panic!( From 7da700a7388952401d27849501aa8e432f32253b Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 25 Jan 2024 17:23:05 -0700 Subject: [PATCH 04/10] zcash_keys: Box `Address` elements to avoid large variations in enum variant sizes --- zcash_client_backend/CHANGELOG.md | 2 ++ .../src/data_api/testing/pool.rs | 2 +- .../src/data_api/wallet/input_selection.rs | 22 ++++++++++-------- zcash_client_sqlite/src/wallet.rs | 2 +- zcash_client_sqlite/src/wallet/init.rs | 4 ++-- .../wallet/init/migrations/addresses_table.rs | 4 ++-- .../wallet/init/migrations/ufvk_support.rs | 6 ++--- zcash_keys/CHANGELOG.md | 2 ++ zcash_keys/src/address.rs | 23 ++++++++++--------- 9 files changed, 38 insertions(+), 29 deletions(-) diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 1d3860dcd..ba11e4f4e 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -21,6 +21,8 @@ and this library adheres to Rust's notion of - `zcash_client_backend::data_api::WalletRead::get_known_ephemeral_addresses` now takes a `Range` as its argument instead of a `Range` +- `zcash_client_backend::data_api::wallet::input_selection::GreedyInputSelectorError::UnsupportedAddress` + now contains a `ZcashAddress` instead of a `UnifiedAddress`. ### Deprecated - `zcash_client_backend::address` (use `zcash_keys::address` instead) diff --git a/zcash_client_backend/src/data_api/testing/pool.rs b/zcash_client_backend/src/data_api/testing/pool.rs index a58e73b78..697e9b739 100644 --- a/zcash_client_backend/src/data_api/testing/pool.rs +++ b/zcash_client_backend/src/data_api/testing/pool.rs @@ -673,7 +673,7 @@ pub fn send_multi_step_proposed_transfer( assert_matches!( ephemeral0, - Address::Transparent(TransparentAddress::PublicKeyHash(_)) + Address::Transparent(ref b) if matches!(*b, TransparentAddress::PublicKeyHash(_)) ); // Attempting to pay to an ephemeral address should cause an error. diff --git a/zcash_client_backend/src/data_api/wallet/input_selection.rs b/zcash_client_backend/src/data_api/wallet/input_selection.rs index 0e5debcde..e54707b41 100644 --- a/zcash_client_backend/src/data_api/wallet/input_selection.rs +++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs @@ -9,8 +9,8 @@ use std::{ use ::transparent::bundle::TxOut; use nonempty::NonEmpty; -use zcash_address::ConversionError; -use zcash_keys::address::{Address, UnifiedAddress}; +use zcash_address::{ConversionError, ZcashAddress}; +use zcash_keys::address::Address; use zcash_protocol::{ consensus::{self, BlockHeight}, value::{BalanceError, Zatoshis}, @@ -253,7 +253,7 @@ pub enum GreedyInputSelectorError { /// An intermediate value overflowed or underflowed the valid monetary range. Balance(BalanceError), /// A unified address did not contain a supported receiver. - UnsupportedAddress(Box), + UnsupportedAddress(ZcashAddress), /// Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature. UnsupportedTexAddress, } @@ -266,10 +266,12 @@ impl fmt::Display for GreedyInputSelectorError { "A balance calculation violated amount validity bounds: {:?}.", e ), - GreedyInputSelectorError::UnsupportedAddress(_) => { - // we can't encode the UA to its string representation because we - // don't have network parameters here - write!(f, "Unified address contains no supported receivers.") + GreedyInputSelectorError::UnsupportedAddress(addr) => { + write!( + f, + "Unified address {} contains no supported receivers.", + addr.encode() + ) } GreedyInputSelectorError::UnsupportedTexAddress => { write!(f, "Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature.") @@ -438,7 +440,7 @@ impl InputSelector for GreedyInputSelector { .expect("cannot fail because memo is None"), ); total_ephemeral = (total_ephemeral + payment.amount()) - .ok_or_else(|| GreedyInputSelectorError::Balance(BalanceError::Overflow))?; + .ok_or(GreedyInputSelectorError::Balance(BalanceError::Overflow))?; } #[cfg(not(feature = "transparent-inputs"))] Address::Tex(_) => { @@ -474,7 +476,9 @@ impl InputSelector for GreedyInputSelector { } return Err(InputSelectorError::Selection( - GreedyInputSelectorError::UnsupportedAddress(Box::new(addr)), + GreedyInputSelectorError::UnsupportedAddress( + payment.recipient_address().clone(), + ), )); } } diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 99db16e47..363285cea 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -656,7 +656,7 @@ pub(crate) fn get_current_address( addr_str, ))), }) - .map(|addr| (addr, DiversifierIndex::from(di_be))) + .map(|addr| (*addr, DiversifierIndex::from(di_be))) }) .transpose() } diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index fe2e24a40..943ad2f90 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -989,7 +989,7 @@ mod tests { // Orchard component. let ua_request = UnifiedAddressRequest::unsafe_new_without_expiry(Omit, Require, UA_TRANSPARENT); - let address_str = Address::Unified( + let address_str = Address::from( ufvk.default_address(Some(ua_request)) .expect("A valid default address exists for the UFVK") .0, @@ -1008,7 +1008,7 @@ mod tests { // add a transparent "sent note" #[cfg(feature = "transparent-inputs")] { - let taddr = Address::Transparent( + let taddr = Address::from( *ufvk .default_address(Some(ua_request)) .expect("A valid default address exists for the UFVK") diff --git a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs index 4468fe628..e75cfdb5c 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs @@ -93,11 +93,11 @@ impl RusqliteMigration for Migration

{ let (expected_address, idx) = ufvk.default_address(Some( UnifiedAddressRequest::unsafe_new_without_expiry(Omit, Require, UA_TRANSPARENT), ))?; - if decoded_address != expected_address { + if *decoded_address != expected_address { return Err(WalletMigrationError::CorruptedData(format!( "Decoded UA {} does not match the UFVK's default address {} at {:?}.", address, - Address::Unified(expected_address).encode(&self.params), + Address::from(expected_address).encode(&self.params), idx, ))); } diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs index defacaaf6..09d15371f 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs @@ -127,7 +127,7 @@ impl RusqliteMigration for Migration

{ WalletMigrationError::CorruptedData( format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.", address, - Address::Sapling(expected_address).encode(&self.params), + Address::from(expected_address).encode(&self.params), idx)) } else { WalletMigrationError::SeedNotRelevant @@ -140,12 +140,12 @@ impl RusqliteMigration for Migration

{ } Address::Unified(decoded_address) => { let (expected_address, idx) = ufvk.default_address(Some(ua_request))?; - if decoded_address != expected_address { + if *decoded_address != expected_address { return Err(if seed_is_relevant { WalletMigrationError::CorruptedData( format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.", address, - Address::Unified(expected_address).encode(&self.params), + Address::from(expected_address).encode(&self.params), idx)) } else { WalletMigrationError::SeedNotRelevant diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index 65d5c668b..a040ac4e3 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -47,6 +47,8 @@ and this library adheres to Rust's notion of receivers. - `zcash_keys::keys::UnifiedSpendingKey::default_address` is now failable, and now returns a `Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError>`. +- `zcash_keys::address::Address` variants now `Box` their contents to + avoid large discrepancies in enum variant sizing. ### Removed - `zcash_keys::keys::UnifiedAddressRequest::all` (use diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index 83955e597..8c5f13cae 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -1,5 +1,6 @@ //! Structs for handling supported address types. +use alloc::boxed::Box; use alloc::string::{String, ToString}; use alloc::vec::Vec; @@ -392,7 +393,7 @@ pub enum Address { /// A [ZIP 316] Unified Address. /// /// [ZIP 316]: https://zips.z.cash/zip-0316 - Unified(UnifiedAddress), + Unified(Box), /// A [ZIP 320] transparent-source-only P2PKH address, or "TEX address". /// @@ -415,7 +416,7 @@ impl From for Address { impl From for Address { fn from(addr: UnifiedAddress) -> Self { - Address::Unified(addr) + Address::Unified(Box::new(addr)) } } @@ -475,12 +476,12 @@ impl Address { match self { #[cfg(feature = "sapling")] Address::Sapling(pa) => ZcashAddress::from_sapling(net, pa.to_bytes()), - Address::Transparent(addr) => match addr { + Address::Transparent(addr) => match *addr { TransparentAddress::PublicKeyHash(data) => { - ZcashAddress::from_transparent_p2pkh(net, *data) + ZcashAddress::from_transparent_p2pkh(net, data) } TransparentAddress::ScriptHash(data) => { - ZcashAddress::from_transparent_p2sh(net, *data) + ZcashAddress::from_transparent_p2sh(net, data) } }, Address::Unified(ua) => ua.to_address(net), @@ -555,9 +556,9 @@ pub mod testing { #[cfg(feature = "sapling")] pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy { prop_oneof![ - arb_payment_address().prop_map(Address::Sapling), - arb_transparent_addr().prop_map(Address::Transparent), - arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified), + arb_payment_address().prop_map(Address::from), + arb_transparent_addr().prop_map(Address::from), + arb_unified_addr(Network::TestNetwork, request).prop_map(Address::from), proptest::array::uniform20(any::()).prop_map(Address::Tex), ] } @@ -565,8 +566,8 @@ pub mod testing { #[cfg(not(feature = "sapling"))] pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy { return prop_oneof![ - arb_transparent_addr().prop_map(Address::Transparent), - arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified), + arb_transparent_addr().prop_map(Address::from), + arb_unified_addr(Network::TestNetwork, request).prop_map(Address::from), proptest::array::uniform20(any::()).prop_map(Address::Tex), ]; } @@ -614,7 +615,7 @@ mod tests { #[cfg(all(feature = "orchard", not(feature = "sapling")))] 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); assert_eq!(Address::decode(&MAIN_NETWORK, &addr_str), Some(addr)); } From 7557665d98d247b35a579490dd6ca6f1225bdfd5 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 16 Feb 2024 14:49:47 -0700 Subject: [PATCH 05/10] `zcash_address`: Add support for ZIP 316, Revision 1 --- components/zcash_address/CHANGELOG.md | 4 +- components/zcash_address/src/encoding.rs | 17 +- components/zcash_address/src/kind/unified.rs | 123 +++++++++----- .../zcash_address/src/kind/unified/address.rs | 157 ++++++++++++------ .../zcash_address/src/kind/unified/fvk.rs | 82 ++++++--- .../zcash_address/src/kind/unified/ivk.rs | 79 ++++++--- components/zcash_address/src/test_vectors.rs | 11 +- components/zcash_protocol/CHANGELOG.md | 19 +++ components/zcash_protocol/src/address.rs | 12 ++ components/zcash_protocol/src/consensus.rs | 58 ++++--- .../zcash_protocol/src/constants/mainnet.rs | 25 ++- .../zcash_protocol/src/constants/regtest.rs | 25 ++- .../zcash_protocol/src/constants/testnet.rs | 25 ++- components/zcash_protocol/src/lib.rs | 1 + devtools/src/bin/inspect/address.rs | 9 +- devtools/src/bin/inspect/keys.rs | 9 +- devtools/src/bin/inspect/keys/view.rs | 14 +- devtools/src/bin/inspect/transaction.rs | 10 +- zcash_keys/src/address.rs | 8 +- zcash_keys/src/keys.rs | 15 +- 20 files changed, 488 insertions(+), 215 deletions(-) create mode 100644 components/zcash_protocol/src/address.rs diff --git a/components/zcash_address/CHANGELOG.md b/components/zcash_address/CHANGELOG.md index 14831efa1..40158dde5 100644 --- a/components/zcash_address/CHANGELOG.md +++ b/components/zcash_address/CHANGELOG.md @@ -11,9 +11,10 @@ and this library adheres to Rust's notion of - `zcash_address::unified`: - `Address::receivers` - `DataTypecode` - - `MetadataTypecode` - `Item` - `MetadataItem` + - `MetadataTypecode` + - `Revision` - `impl serde::{Serialize, Deserialize} for zcash_address::ZcashAddress` behind the `serde` feature flag. @@ -23,6 +24,7 @@ and this library adheres to Rust's notion of it now has two variants, `Typecode::Data` and `Typecode::Metadata`. - `Encoding::try_from_items` arguments have changed. - The result type of `Container::items_as_parsed` has changed. + - The `Container` trait has an added `revision` accessor method. - `ParseError::InvalidTypecodeValue` now wraps a `u32` instead of a `u64`. - `ParseError` has added variant `NotUnderstood`. diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index 943ba209a..b017ae849 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -190,7 +190,7 @@ mod tests { kind::unified, unified::{Item, Receiver}, }; - use zcash_protocol::consensus::NetworkType; + use zcash_protocol::{address::Revision, consensus::NetworkType}; fn encoding(encoded: &str, decoded: ZcashAddress) { assert_eq!(decoded.to_string(), encoded); @@ -240,21 +240,30 @@ mod tests { "u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl", ZcashAddress { net: NetworkType::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: NetworkType::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: NetworkType::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 dba9654fc..94d9ebba0 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -17,7 +17,7 @@ use std::error::Error; use bech32::{primitives::decode::CheckedHrpstring, Bech32m, Checksum, Hrp}; use zcash_encoding::MAX_COMPACT_SIZE; -use zcash_protocol::consensus::NetworkType; +use zcash_protocol::{address::Revision, consensus::NetworkType}; pub(crate) mod address; pub(crate) mod fvk; @@ -127,7 +127,7 @@ impl DataTypecode { 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) + /// Expiration time 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) @@ -235,9 +235,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) @@ -246,7 +252,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) @@ -255,11 +261,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())), } } @@ -371,15 +378,14 @@ impl fmt::Display for ParseError { impl Error for ParseError {} pub(crate) mod private { - use alloc::borrow::ToOwned; - use alloc::vec::Vec; + use alloc::{borrow::ToOwned, string::ToString, vec::Vec}; use core::convert::{TryFrom, TryInto}; use core2::io::Write; use super::{DataTypecode, ParseError, Typecode, PADDING_LEN}; use crate::unified::{Item, MetadataItem}; use zcash_encoding::CompactSize; - use zcash_protocol::consensus::NetworkType; + use zcash_protocol::{address::Revision, consensus::NetworkType}; /// A raw address or viewing key. pub trait SealedDataItem: Clone { @@ -395,33 +401,50 @@ pub(crate) mod private { /// A Unified Container containing addresses or viewing keys. pub trait SealedContainer: super::Container + core::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 network_hrp(network: &NetworkType) -> &'static str { - match network { - NetworkType::Main => Self::MAINNET, - NetworkType::Test => Self::TESTNET, - NetworkType::Regtest => Self::REGTEST, + fn from_inner(revision: Revision, items: Vec>) -> Self; + + fn network_hrp(revision: Revision, network: &NetworkType) -> &'static str { + match (revision, network) { + (Revision::R0, NetworkType::Main) => Self::MAINNET_R0, + (Revision::R0, NetworkType::Test) => Self::TESTNET_R0, + (Revision::R0, NetworkType::Regtest) => Self::REGTEST_R0, + (Revision::R1, NetworkType::Main) => Self::MAINNET_R1, + (Revision::R1, NetworkType::Test) => Self::TESTNET_R1, + (Revision::R1, NetworkType::Regtest) => Self::REGTEST_R1, } } + fn hrp_revision(hrp: &str) -> Option { + (hrp == Self::MAINNET_R0 || hrp == Self::TESTNET_R0 || hrp == Self::REGTEST_R0) + .then_some(Revision::R0) + .or_else(|| { + (hrp == Self::MAINNET_R1 || hrp == Self::TESTNET_R1 || hrp == Self::REGTEST_R1) + .then_some(Revision::R1) + }) + } + fn hrp_network(hrp: &str) -> Option { - if hrp == Self::MAINNET { - Some(NetworkType::Main) - } else if hrp == Self::TESTNET { - Some(NetworkType::Test) - } else if hrp == Self::REGTEST { - Some(NetworkType::Regtest) - } else { - None - } + (hrp == Self::MAINNET_R0 || hrp == Self::MAINNET_R1) + .then_some(NetworkType::Main) + .or_else(|| { + (hrp == Self::TESTNET_R0 || hrp == Self::TESTNET_R1) + .then_some(NetworkType::Test) + }) + .or_else(|| { + (hrp == Self::REGTEST_R0 || hrp == Self::REGTEST_R1) + .then_some(NetworkType::Regtest) + }) } fn write_raw_encoding(&self, mut writer: W) { @@ -453,11 +476,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 core2::io::Cursor<&[u8]>, ) -> Result, ParseError> { let typecode = CompactSize::read(&mut cursor) @@ -490,7 +515,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) @@ -517,19 +544,25 @@ pub(crate) mod private { )), }?; + let revision = Self::hrp_revision(hrp) + .ok_or_else(|| ParseError::UnknownPrefix(hrp.to_string()))?; + let mut cursor = core2::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 { let mut prev_code = None; // less than any Some let mut only_transparent = true; for item in &items { @@ -555,12 +588,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)) } } } @@ -596,9 +630,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 @@ -625,7 +662,7 @@ pub trait Encoding: private::SealedContainer { /// ordering of the contained items such that it correctly obeys round-trip /// serialization invariants. fn encode(&self, network: &NetworkType) -> String { - let hrp = Self::network_hrp(network); + let hrp = Self::network_hrp(self.revision(), network); bech32::encode::(Hrp::parse_unchecked(hrp), &self.to_jumbled_bytes(hrp)) .expect("F4Jumble ensures length is short enough by construction") } @@ -638,4 +675,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 d1352f3b5..b53967e00 100644 --- a/components/zcash_address/src/kind/unified/address.rs +++ b/components/zcash_address/src/kind/unified/address.rs @@ -1,4 +1,4 @@ -use zcash_protocol::{constants, PoolType}; +use zcash_protocol::{address::Revision, constants, PoolType}; use super::{private::SealedDataItem, DataTypecode, Item, ParseError}; @@ -72,6 +72,7 @@ impl SealedDataItem for Receiver { /// unified::{self, Container, Encoding, Item}, /// ConversionError, TryFromRawAddress, ZcashAddress, /// }; +/// use zcash_protocol::address::Revision; /// /// # #[cfg(not(feature = "std"))] /// # fn main() {} @@ -103,13 +104,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. @@ -118,7 +122,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()), @@ -133,7 +137,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::ORCHARD, Item::Data(Receiver::Sapling(_)) => pool_type == PoolType::SAPLING, Item::Data(Receiver::P2pkh(_)) | Item::Data(Receiver::P2sh(_)) => { @@ -146,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(_)) @@ -163,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 = constants::mainnet::HRP_UNIFIED_ADDRESS; + const MAINNET_R0: &'static str = constants::mainnet::HRP_UNIFIED_ADDRESS_R0; - /// 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 = constants::testnet::HRP_UNIFIED_ADDRESS; + const TESTNET_R0: &'static str = constants::testnet::HRP_UNIFIED_ADDRESS_R0; /// The HRP for a Bech32m-encoded regtest Unified Address. - const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_ADDRESS; + const REGTEST_R0: &'static str = constants::regtest::HRP_UNIFIED_ADDRESS_R0; + + /// 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 = constants::mainnet::HRP_UNIFIED_ADDRESS_R1; + + /// 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 = constants::testnet::HRP_UNIFIED_ADDRESS_R1; - fn from_inner(receivers: Vec>) -> Self { - Self(receivers) + /// The HRP for a Bech32m-encoded regtest Revision 1 Unified Address. + const REGTEST_R1: &'static str = constants::regtest::HRP_UNIFIED_ADDRESS_R1; + + fn from_inner(revision: Revision, receivers: Vec>) -> Self { + Self { + revision, + receivers, + } } } @@ -190,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 } } @@ -209,6 +237,7 @@ pub mod testing { use super::{Address, Receiver}; use crate::unified::{DataTypecode, Item}; use zcash_encoding::MAX_COMPACT_SIZE; + use zcash_protocol::address::Revision; prop_compose! { fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] { @@ -224,6 +253,11 @@ pub mod testing { select(vec![DataTypecode::P2pkh, DataTypecode::P2sh]) } + /// A strategy to generate an arbitrary transparent typecode. + pub(crate) fn arb_revision() -> impl Strategy { + select(vec![Revision::R0, Revision::R1]) + } + /// A strategy to generate an arbitrary shielded (Sapling, Orchard, or unknown) typecode. fn arb_shielded_typecode() -> impl Strategy { prop_oneof![ @@ -269,13 +303,16 @@ pub mod testing { /// protocol rules; this generator is only intended for use in testing parsing and /// serialization. pub fn arb_unified_address() -> impl Strategy { - arb_typecodes() - .prop_flat_map(arb_unified_address_receivers) - .prop_map(|rs| { - let mut items = rs.into_iter().map(Item::Data).collect::>(); - items.sort_unstable_by(Item::encoding_order); - Address(items) + (arb_typecodes(), arb_revision()).prop_flat_map(|(tc, revision)| { + arb_unified_address_receivers(tc).prop_map(move |rs| { + let mut receivers = rs.into_iter().map(Item::Data).collect::>(); + receivers.sort_unstable_by(Item::encoding_order); + Address { + revision, + receivers: receivers.clone(), + } }) + }) } } @@ -294,7 +331,7 @@ mod tests { use super::{Address, ParseError, Receiver}; use crate::{ kind::unified::{private::SealedContainer, Encoding as _}, - unified::{address::testing::arb_unified_address, Item, Typecode}, + unified::{address::testing::arb_unified_address, Item, Revision, Typecode}, }; proptest! { @@ -324,7 +361,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() )) @@ -339,7 +376,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() )) @@ -364,7 +401,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(_)) ); @@ -377,7 +414,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(_)) ); } @@ -386,13 +423,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)) ); } @@ -401,14 +441,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) ); } @@ -417,14 +460,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) ); } @@ -443,7 +489,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(_)) ); } @@ -451,15 +497,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 ad558d85e..4be6549d6 100644 --- a/components/zcash_address/src/kind/unified/fvk.rs +++ b/components/zcash_address/src/kind/unified/fvk.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; use core::convert::TryInto; -use zcash_protocol::constants; +use zcash_protocol::{address::Revision, constants}; use super::{ private::{SealedContainer, SealedDataItem}, @@ -81,6 +81,7 @@ impl SealedDataItem for Fvk { /// /// ``` /// use zcash_address::unified::{self, Container, Encoding, Item}; +/// use zcash_protocol::address::Revision; /// /// # #[cfg(not(feature = "std"))] /// # fn main() {} @@ -95,48 +96,71 @@ 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 = constants::mainnet::HRP_UNIFIED_FVK; + const MAINNET_R0: &'static str = constants::mainnet::HRP_UNIFIED_FVK_R0; - /// 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 = constants::testnet::HRP_UNIFIED_FVK; + const TESTNET_R0: &'static str = constants::testnet::HRP_UNIFIED_FVK_R0; /// The HRP for a Bech32m-encoded regtest Unified FVK. /// /// Defined in [ZIP 316][zip-0316]. /// /// [zip-0316]: https://zips.z.cash/zip-0316 - const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_FVK; + const REGTEST_R0: &'static str = constants::regtest::HRP_UNIFIED_FVK_R0; + + /// 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 = constants::mainnet::HRP_UNIFIED_FVK_R1; + /// 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 = constants::testnet::HRP_UNIFIED_FVK_R1; + + /// The HRP for a Bech32m-encoded regtest Revision 1 Unified FVK. + const REGTEST_R1: &'static str = constants::regtest::HRP_UNIFIED_FVK_R1; - fn from_inner(fvks: Vec>) -> Self { - Self(fvks) + fn from_inner(revision: Revision, fvks: Vec>) -> Self { + Self { revision, fvks } } } @@ -152,9 +176,9 @@ mod tests { use super::{Fvk, ParseError, Ufvk}; use crate::{ kind::unified::{private::SealedContainer, Encoding as _}, - unified::{Item, Typecode}, + unified::{address::testing::arb_revision, Item, Typecode}, }; - use zcash_protocol::consensus::NetworkType; + use zcash_protocol::{address::Revision, consensus::NetworkType}; prop_compose! { fn uniform128()(a in uniform96(), b in uniform32(0u8..)) -> [u8; 128] { @@ -207,12 +231,13 @@ mod tests { prop_compose! { fn arb_unified_fvk()( + revision in arb_revision(), 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, fvks } } } @@ -244,7 +269,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() )) @@ -262,7 +287,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() )) @@ -294,7 +319,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(_)) ); @@ -309,7 +334,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(_)) ); } @@ -318,13 +343,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)) ); } @@ -342,7 +370,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 3c909aacf..1da276923 100644 --- a/components/zcash_address/src/kind/unified/ivk.rs +++ b/components/zcash_address/src/kind/unified/ivk.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; use core::convert::TryInto; -use zcash_protocol::constants; +use zcash_protocol::{address::Revision, constants}; use super::{ private::{SealedContainer, SealedDataItem}, @@ -86,6 +86,7 @@ impl SealedDataItem for Ivk { /// /// ``` /// use zcash_address::unified::{self, Container, Encoding, Item}; +/// use zcash_protocol::address::Revision; /// /// # #[cfg(not(feature = "std"))] /// # fn main() {} @@ -100,44 +101,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 = constants::mainnet::HRP_UNIFIED_IVK; + const MAINNET_R0: &'static str = constants::mainnet::HRP_UNIFIED_IVK_R0; - /// 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 = constants::testnet::HRP_UNIFIED_IVK; + const TESTNET_R0: &'static str = constants::testnet::HRP_UNIFIED_IVK_R0; /// The HRP for a Bech32m-encoded regtest Unified IVK. - const REGTEST: &'static str = constants::regtest::HRP_UNIFIED_IVK; + const REGTEST_R0: &'static str = constants::regtest::HRP_UNIFIED_IVK_R0; + + /// 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 = constants::mainnet::HRP_UNIFIED_IVK_R1; - fn from_inner(ivks: Vec>) -> Self { - Self(ivks) + /// 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 = constants::testnet::HRP_UNIFIED_IVK_R1; + + /// The HRP for a Bech32m-encoded regtest Revision 1 Unified IVK. + const REGTEST_R1: &'static str = constants::regtest::HRP_UNIFIED_IVK_R1; + + fn from_inner(revision: Revision, ivks: Vec>) -> Self { + Self { revision, ivks } } } @@ -157,9 +182,9 @@ mod tests { use super::{Ivk, ParseError, Uivk}; use crate::{ kind::unified::{private::SealedContainer, Encoding as _}, - unified::{Item, Typecode}, + unified::{address::testing::arb_revision, Item, Typecode}, }; - use zcash_protocol::consensus::NetworkType; + use zcash_protocol::{address::Revision, consensus::NetworkType}; prop_compose! { fn uniform64()(a in uniform32(0u8..), b in uniform32(0u8..)) -> [u8; 64] { @@ -196,12 +221,13 @@ mod tests { prop_compose! { fn arb_unified_ivk()( + revision in arb_revision(), 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, ivks } } } @@ -231,7 +257,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() )) @@ -247,7 +273,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() )) @@ -275,7 +301,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(_)) ); @@ -288,7 +314,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(_)) ); } @@ -296,10 +322,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(&NetworkType::Main); assert_eq!( Uivk::decode(&encoded), @@ -320,7 +349,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 0c3aff46b..6170addf5 100644 --- a/components/zcash_address/src/test_vectors.rs +++ b/components/zcash_address/src/test_vectors.rs @@ -14,7 +14,7 @@ use { }, alloc::string::ToString, core::iter, - zcash_protocol::consensus::NetworkType, + zcash_protocol::{address::Revision, consensus::NetworkType}, }; #[test] @@ -42,8 +42,13 @@ fn unified() { .map(Item::Data) .collect(); - let expected_addr = - ZcashAddress::from_unified(NetworkType::Main, unified::Address(receivers)); + let expected_addr = ZcashAddress::from_unified( + NetworkType::Main, + unified::Address { + revision: Revision::R0, + receivers, + }, + ); // Test parsing let addr: ZcashAddress = tv.unified_addr.parse().unwrap(); diff --git a/components/zcash_protocol/CHANGELOG.md b/components/zcash_protocol/CHANGELOG.md index 8ebbfd656..433676fd3 100644 --- a/components/zcash_protocol/CHANGELOG.md +++ b/components/zcash_protocol/CHANGELOG.md @@ -7,12 +7,31 @@ and this library adheres to Rust's notion of ## [Unreleased] +### Added +- `zcash_protocol::address::Revision` +- `zcash_protocol::constants::` + - `{mainnet|testnet|regtest}::HRP_UNIFIED_ADDRESS_R0` + - `{mainnet|testnet|regtest}::HRP_UNIFIED_IVK_R0` + - `{mainnet|testnet|regtest}::HRP_UNIFIED_FVK_R0` + - `{mainnet|testnet|regtest}::HRP_UNIFIED_ADDRESS_R1` + - `{mainnet|testnet|regtest}::HRP_UNIFIED_IVK_R1` + - `{mainnet|testnet|regtest}::HRP_UNIFIED_FVK_R1` + ### Changed - `zcash_protocol::consensus::NetworkConstants` has added methods: - `hrp_unified_address` - `hrp_unified_fvk` - `hrp_unified_ivk` +### Removed +- `zcash_protocol::constants::` + - `{mainnet|testnet|regtest}::HRP_UNIFIED_ADDRESS` have been replaced by + `{mainnet|testnet|regtest}::HRP_UNIFIED_ADDRESS_R0` respectively. + - `{mainnet|testnet|regtest}::HRP_UNIFIED_IVK` have been replaced by + `{mainnet|testnet|regtest}::HRP_UNIFIED_IVK_R0` respectively. + - `{mainnet|testnet|regtest}::HRP_UNIFIED_FVK` have been replaced by + `{mainnet|testnet|regtest}::HRP_UNIFIED_FVK_R0` respectively. + ## [0.4.3] - 2024-12-16 ### Added - `zcash_protocol::TxId` (moved from `zcash_primitives::transaction`). diff --git a/components/zcash_protocol/src/address.rs b/components/zcash_protocol/src/address.rs new file mode 100644 index 000000000..2950ce2c1 --- /dev/null +++ b/components/zcash_protocol/src/address.rs @@ -0,0 +1,12 @@ +//! Types related to Zcash address parsing & encoding. + +/// The [revision] of the Unified Address standard that an address was parsed under. +/// +/// [revision]: https://zips.z.cash/zip-0316#revisions +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Revision { + /// Identifier for ZIP 316 Revision 0 + R0, + /// Identifier for ZIP 316 Revision 1 + R1, +} diff --git a/components/zcash_protocol/src/consensus.rs b/components/zcash_protocol/src/consensus.rs index 6648089e9..aff24fb47 100644 --- a/components/zcash_protocol/src/consensus.rs +++ b/components/zcash_protocol/src/consensus.rs @@ -8,6 +8,7 @@ use core::ops::{Add, Bound, RangeBounds, Sub}; #[cfg(feature = "std")] use memuse::DynamicUsage; +use crate::address::Revision; use crate::constants::{mainnet, regtest, testnet}; /// A wrapper type representing blockchain heights. @@ -195,21 +196,21 @@ pub trait NetworkConstants: Clone { /// Defined in [ZIP 316][zip-0316]. /// /// [zip-0316]: https://zips.z.cash/zip-0316 - fn hrp_unified_address(&self) -> &'static str; + fn hrp_unified_address(&self, revision: Revision) -> &'static str; /// The HRP for a Bech32m-encoded mainnet Unified FVK. /// /// Defined in [ZIP 316][zip-0316]. /// /// [zip-0316]: https://zips.z.cash/zip-0316 - fn hrp_unified_fvk(&self) -> &'static str; + fn hrp_unified_fvk(&self, revision: Revision) -> &'static str; /// The HRP for a Bech32m-encoded mainnet Unified IVK. /// /// Defined in [ZIP 316][zip-0316]. /// /// [zip-0316]: https://zips.z.cash/zip-0316 - fn hrp_unified_ivk(&self) -> &'static str; + fn hrp_unified_ivk(&self, revision: Revision) -> &'static str; } /// The enumeration of known Zcash network types. @@ -294,27 +295,36 @@ impl NetworkConstants for NetworkType { } } - fn hrp_unified_address(&self) -> &'static str { - match self { - NetworkType::Main => mainnet::HRP_UNIFIED_ADDRESS, - NetworkType::Test => testnet::HRP_UNIFIED_ADDRESS, - NetworkType::Regtest => regtest::HRP_UNIFIED_ADDRESS, + fn hrp_unified_address(&self, revision: Revision) -> &'static str { + match (revision, self) { + (Revision::R0, NetworkType::Main) => mainnet::HRP_UNIFIED_ADDRESS_R0, + (Revision::R0, NetworkType::Test) => testnet::HRP_UNIFIED_ADDRESS_R0, + (Revision::R0, NetworkType::Regtest) => regtest::HRP_UNIFIED_ADDRESS_R0, + (Revision::R1, NetworkType::Main) => mainnet::HRP_UNIFIED_ADDRESS_R1, + (Revision::R1, NetworkType::Test) => testnet::HRP_UNIFIED_ADDRESS_R1, + (Revision::R1, NetworkType::Regtest) => regtest::HRP_UNIFIED_ADDRESS_R1, } } - fn hrp_unified_fvk(&self) -> &'static str { - match self { - NetworkType::Main => mainnet::HRP_UNIFIED_FVK, - NetworkType::Test => testnet::HRP_UNIFIED_FVK, - NetworkType::Regtest => regtest::HRP_UNIFIED_FVK, + fn hrp_unified_fvk(&self, revision: Revision) -> &'static str { + match (revision, self) { + (Revision::R0, NetworkType::Main) => mainnet::HRP_UNIFIED_FVK_R0, + (Revision::R0, NetworkType::Test) => testnet::HRP_UNIFIED_FVK_R0, + (Revision::R0, NetworkType::Regtest) => regtest::HRP_UNIFIED_FVK_R0, + (Revision::R1, NetworkType::Main) => mainnet::HRP_UNIFIED_FVK_R1, + (Revision::R1, NetworkType::Test) => testnet::HRP_UNIFIED_FVK_R1, + (Revision::R1, NetworkType::Regtest) => regtest::HRP_UNIFIED_FVK_R1, } } - fn hrp_unified_ivk(&self) -> &'static str { - match self { - NetworkType::Main => mainnet::HRP_UNIFIED_IVK, - NetworkType::Test => testnet::HRP_UNIFIED_IVK, - NetworkType::Regtest => regtest::HRP_UNIFIED_IVK, + fn hrp_unified_ivk(&self, revision: Revision) -> &'static str { + match (revision, self) { + (Revision::R0, NetworkType::Main) => mainnet::HRP_UNIFIED_IVK_R0, + (Revision::R0, NetworkType::Test) => testnet::HRP_UNIFIED_IVK_R0, + (Revision::R0, NetworkType::Regtest) => regtest::HRP_UNIFIED_IVK_R0, + (Revision::R1, NetworkType::Main) => mainnet::HRP_UNIFIED_IVK_R1, + (Revision::R1, NetworkType::Test) => testnet::HRP_UNIFIED_IVK_R1, + (Revision::R1, NetworkType::Regtest) => regtest::HRP_UNIFIED_IVK_R1, } } } @@ -368,16 +378,16 @@ impl NetworkConstants for P { self.network_type().hrp_tex_address() } - fn hrp_unified_address(&self) -> &'static str { - self.network_type().hrp_unified_address() + fn hrp_unified_address(&self, revision: Revision) -> &'static str { + self.network_type().hrp_unified_address(revision) } - fn hrp_unified_fvk(&self) -> &'static str { - self.network_type().hrp_unified_fvk() + fn hrp_unified_fvk(&self, revision: Revision) -> &'static str { + self.network_type().hrp_unified_fvk(revision) } - fn hrp_unified_ivk(&self) -> &'static str { - self.network_type().hrp_unified_ivk() + fn hrp_unified_ivk(&self, revision: Revision) -> &'static str { + self.network_type().hrp_unified_ivk(revision) } } diff --git a/components/zcash_protocol/src/constants/mainnet.rs b/components/zcash_protocol/src/constants/mainnet.rs index ada1a42d1..327cb4ee2 100644 --- a/components/zcash_protocol/src/constants/mainnet.rs +++ b/components/zcash_protocol/src/constants/mainnet.rs @@ -51,23 +51,36 @@ pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xbd]; /// [ZIP 320]: https://zips.z.cash/zip-0320 pub const HRP_TEX_ADDRESS: &str = "tex"; -/// 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 -pub const HRP_UNIFIED_ADDRESS: &str = "u"; +pub const HRP_UNIFIED_ADDRESS_R0: &str = "u"; -/// 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 -pub const HRP_UNIFIED_FVK: &str = "uview"; +pub const HRP_UNIFIED_FVK_R0: &str = "uview"; -/// 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 -pub const HRP_UNIFIED_IVK: &str = "uivk"; +pub const HRP_UNIFIED_IVK_R0: &str = "uivk"; + +/// The HRP for a Bech32m-encoded regtest Revision 1 Unified Address. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub const HRP_UNIFIED_ADDRESS_R1: &str = "ur"; + +/// The HRP for a Bech32m-encoded regtest Revision 1 Unified FVK. +pub const HRP_UNIFIED_FVK_R1: &str = "urview"; + +/// The HRP for a Bech32m-encoded regtest Revision 1 Unified IVK. +pub const HRP_UNIFIED_IVK_R1: &str = "urivk"; diff --git a/components/zcash_protocol/src/constants/regtest.rs b/components/zcash_protocol/src/constants/regtest.rs index c78f9a295..1cb98fb62 100644 --- a/components/zcash_protocol/src/constants/regtest.rs +++ b/components/zcash_protocol/src/constants/regtest.rs @@ -58,15 +58,28 @@ pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; /// [ZIP 320]: https://zips.z.cash/zip-0320 pub const HRP_TEX_ADDRESS: &str = "texregtest"; -/// The HRP for a Bech32m-encoded regtest Unified Address. +/// The HRP for a Bech32m-encoded regtest Revision 0 Unified Address. /// /// Defined in [ZIP 316][zip-0316]. /// /// [zip-0316]: https://zips.z.cash/zip-0316 -pub const HRP_UNIFIED_ADDRESS: &str = "uregtest"; +pub const HRP_UNIFIED_ADDRESS_R0: &str = "uregtest"; -/// The HRP for a Bech32m-encoded regtest Unified FVK. -pub const HRP_UNIFIED_FVK: &str = "uviewregtest"; +/// The HRP for a Bech32m-encoded regtest Revision 0 Unified FVK. +pub const HRP_UNIFIED_FVK_R0: &str = "uviewregtest"; -/// The HRP for a Bech32m-encoded regtest Unified IVK. -pub const HRP_UNIFIED_IVK: &str = "uivkregtest"; +/// The HRP for a Bech32m-encoded regtest Revision 0 Unified IVK. +pub const HRP_UNIFIED_IVK_R0: &str = "uivkregtest"; + +/// The HRP for a Bech32m-encoded regtest Revision 1 Unified Address. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub const HRP_UNIFIED_ADDRESS_R1: &str = "urregtest"; + +/// The HRP for a Bech32m-encoded regtest Revision 1 Unified FVK. +pub const HRP_UNIFIED_FVK_R1: &str = "urviewregtest"; + +/// The HRP for a Bech32m-encoded regtest Revision 1 Unified IVK. +pub const HRP_UNIFIED_IVK_R1: &str = "urivkregtest"; diff --git a/components/zcash_protocol/src/constants/testnet.rs b/components/zcash_protocol/src/constants/testnet.rs index 52e90a256..f5b6e8648 100644 --- a/components/zcash_protocol/src/constants/testnet.rs +++ b/components/zcash_protocol/src/constants/testnet.rs @@ -51,23 +51,36 @@ pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; /// [ZIP 320]: https://zips.z.cash/zip-0320 pub const HRP_TEX_ADDRESS: &str = "textest"; -/// 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 -pub const HRP_UNIFIED_ADDRESS: &str = "utest"; +pub const HRP_UNIFIED_ADDRESS_R0: &str = "utest"; -/// 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 -pub const HRP_UNIFIED_FVK: &str = "uviewtest"; +pub const HRP_UNIFIED_FVK_R0: &str = "uviewtest"; -/// 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 -pub const HRP_UNIFIED_IVK: &str = "uivktest"; +pub const HRP_UNIFIED_IVK_R0: &str = "uivktest"; + +/// The HRP for a Bech32m-encoded regtest Revision 1 Unified Address. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub const HRP_UNIFIED_ADDRESS_R1: &str = "urtest"; + +/// The HRP for a Bech32m-encoded regtest Revision 1 Unified FVK. +pub const HRP_UNIFIED_FVK_R1: &str = "urviewtest"; + +/// The HRP for a Bech32m-encoded regtest Revision 1 Unified IVK. +pub const HRP_UNIFIED_IVK_R1: &str = "urivktest"; diff --git a/components/zcash_protocol/src/lib.rs b/components/zcash_protocol/src/lib.rs index ae25f66d5..b60ef75b8 100644 --- a/components/zcash_protocol/src/lib.rs +++ b/components/zcash_protocol/src/lib.rs @@ -24,6 +24,7 @@ extern crate std; use core::fmt; +pub mod address; pub mod consensus; pub mod constants; #[cfg(feature = "local-consensus")] diff --git a/devtools/src/bin/inspect/address.rs b/devtools/src/bin/inspect/address.rs index 028ff3b3a..e8cde8571 100644 --- a/devtools/src/bin/inspect/address.rs +++ b/devtools/src/bin/inspect/address.rs @@ -1,5 +1,5 @@ use zcash_address::{ - unified::{self, Encoding}, + unified::{self, Container as _, Encoding}, ConversionError, ToAddress, ZcashAddress, }; use zcash_protocol::consensus::NetworkType; @@ -119,9 +119,10 @@ pub(crate) fn inspect(addr: ZcashAddress) { unified::Receiver::Orchard(data) => { eprintln!( " - Orchard ({})", - unified::Address::try_from_items(vec![unified::Item::Data( - unified::Receiver::Orchard(data) - )]) + unified::Address::try_from_items( + ua.revision(), + vec![unified::Item::Data(unified::Receiver::Orchard(data))] + ) .unwrap() .encode(&addr.net) ); diff --git a/devtools/src/bin/inspect/keys.rs b/devtools/src/bin/inspect/keys.rs index 7b6e6ca9e..e93b89128 100644 --- a/devtools/src/bin/inspect/keys.rs +++ b/devtools/src/bin/inspect/keys.rs @@ -14,6 +14,7 @@ use zcash_address::{ }; use zcash_keys::keys::UnifiedFullViewingKey; use zcash_protocol::{ + address::Revision, consensus::{Network, NetworkConstants, NetworkType}, local_consensus::LocalNetwork, }; @@ -150,9 +151,11 @@ pub(crate) fn inspect_mnemonic(mnemonic: bip0039::Mnemonic, context: Option { eprintln!( " - Orchard ({})", - unified::Ufvk::try_from_items(vec![unified::Item::Data( - unified::Fvk::Orchard(*data) - )]) + unified::Ufvk::try_from_items( + ufvk.revision(), + vec![unified::Item::Data(unified::Fvk::Orchard(*data))] + ) .unwrap() .encode(&network) ); @@ -99,9 +100,10 @@ pub(crate) fn inspect_uivk(uivk: unified::Uivk, network: NetworkType) { unified::Ivk::Orchard(data) => { eprintln!( " - Orchard ({})", - unified::Uivk::try_from_items(vec![unified::Item::Data( - unified::Ivk::Orchard(*data) - )]) + unified::Uivk::try_from_items( + uivk.revision(), + vec![unified::Item::Data(unified::Ivk::Orchard(*data))] + ) .unwrap() .encode(&network) ); diff --git a/devtools/src/bin/inspect/transaction.rs b/devtools/src/bin/inspect/transaction.rs index 12ab874ea..9cc84d19b 100644 --- a/devtools/src/bin/inspect/transaction.rs +++ b/devtools/src/bin/inspect/transaction.rs @@ -28,6 +28,7 @@ use zcash_primitives::transaction::{ Authorization, Transaction, TransactionData, TxId, TxVersion, }; use zcash_protocol::{ + address::Revision, consensus::BlockHeight, memo::{Memo, MemoBytes}, value::Zatoshis, @@ -540,9 +541,12 @@ pub(crate) fn inspect( // Construct a single-receiver UA. let zaddr = ZcashAddress::from_unified( net, - unified::Address::try_from_items(vec![unified::Item::Data( - unified::Receiver::Orchard(addr.to_raw_address_bytes()), - )]) + unified::Address::try_from_items( + Revision::R0, + vec![unified::Item::Data(unified::Receiver::Orchard( + addr.to_raw_address_bytes(), + ))], + ) .unwrap(), ); eprintln!(" - {}", zaddr); diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index 8c5f13cae..86f582c59 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -10,6 +10,7 @@ use zcash_address::{ ConversionError, ToAddress, TryFromRawAddress, ZcashAddress, }; use zcash_protocol::{ + address::Revision, consensus::{self, BlockHeight, NetworkType}, PoolType, ShieldedProtocol, }; @@ -288,6 +289,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)) @@ -347,7 +353,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 e59bd2ea9..0c625764f 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -4,7 +4,10 @@ use alloc::vec::Vec; use core::fmt::{self, Display}; use zcash_address::unified::{self, Container, Encoding, Item, MetadataItem, Typecode}; -use zcash_protocol::consensus::{self, BlockHeight}; +use zcash_protocol::{ + address::Revision, + consensus::{self, BlockHeight}, +}; use zip32::{AccountId, DiversifierIndex}; use crate::address::UnifiedAddress; @@ -962,6 +965,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)) @@ -1291,6 +1299,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)) From a047e6968447e8fa375735eb446376f0dd728b99 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 1 Mar 2024 13:25:49 -0700 Subject: [PATCH 06/10] Apply suggestions from code review Co-authored-by: Daira-Emma Hopwood --- components/zcash_address/src/kind/unified.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index 94d9ebba0..08560b624 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -261,12 +261,15 @@ impl MetadataItem { "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(), }), - (R0, ExpiryHeight | ExpiryTime) => Err(ParseError::NotUnderstood(typecode.into())), } } @@ -512,6 +515,9 @@ pub(crate) mod private { length ))); } + // The "as usize" casts cannot change the values, because both + // cursor.position() and addr_end are u64 values <= buf.len() + // 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)?), @@ -572,9 +578,7 @@ pub(crate) mod private { return Err(ParseError::InvalidTypecodeOrder); } else if t_code == prev_code { return Err(ParseError::DuplicateTypecode(t)); - } else if t == Typecode::Data(DataTypecode::P2sh) - && prev_code == Some(u32::from(DataTypecode::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, // otherwise we would detect an out-of-order or duplicate typecode. return Err(ParseError::BothP2phkAndP2sh); @@ -628,7 +632,6 @@ pub trait Encoding: private::SealedContainer { /// invariants concerning the composition of a unified container are /// violated: /// * 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( revision: Revision, From c252209491f7a013e07a1e67eba1e473f1319859 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 22 Apr 2024 12:27:31 -0600 Subject: [PATCH 07/10] Allow transparent-only unified addresses and viewing keys. --- components/f4jumble/src/lib.rs | 2 +- components/zcash_address/src/kind/unified.rs | 9 +++++++- .../zcash_address/src/kind/unified/fvk.rs | 5 +---- .../zcash_address/src/kind/unified/ivk.rs | 5 +---- zcash_keys/CHANGELOG.md | 1 + zcash_keys/src/address.rs | 5 ++++- zcash_keys/src/keys.rs | 21 ++++++++++++++++++- 7 files changed, 36 insertions(+), 12 deletions(-) diff --git a/components/f4jumble/src/lib.rs b/components/f4jumble/src/lib.rs index 982e9d64e..9ee6ca23e 100644 --- a/components/f4jumble/src/lib.rs +++ b/components/f4jumble/src/lib.rs @@ -71,7 +71,7 @@ mod test_vectors_long; /// Length of F4Jumbled message must lie in the range VALID_LENGTH. /// /// VALID_LENGTH = 48..=4194368 -pub const VALID_LENGTH: RangeInclusive = 48..=4194368; +pub const VALID_LENGTH: RangeInclusive = 38..=4194368; /// Errors produced by F4Jumble. #[derive(Debug)] diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index 08560b624..7d26569cc 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -345,6 +345,8 @@ pub enum ParseError { InvalidTypecodeOrder, /// The unified container only contains transparent items. OnlyTransparent, + /// The unified container contains no data items. + NoDataItems, /// The string is not Bech32m encoded, and so cannot be a unified address. NotUnified, /// The Bech32m string has an unrecognized human-readable prefix. @@ -362,6 +364,7 @@ impl fmt::Display for ParseError { ParseError::InvalidEncoding(msg) => write!(f, "Invalid encoding: {}", msg), ParseError::InvalidTypecodeOrder => write!(f, "Items are out of order."), ParseError::OnlyTransparent => write!(f, "UA only contains transparent items"), + ParseError::NoDataItems => write!(f, "UA contains no data items"), ParseError::NotUnified => write!(f, "Address is not Bech32m encoded"), ParseError::UnknownPrefix(s) => { write!(f, "Unrecognized Bech32m human-readable prefix: {}", s) @@ -569,6 +572,10 @@ pub(crate) mod private { revision: Revision, items: Vec>, ) -> Result { + if items.is_empty() { + return Err(ParseError::NoDataItems); + } + let mut prev_code = None; // less than any Some let mut only_transparent = true; for item in &items { @@ -588,7 +595,7 @@ pub(crate) mod private { } } - if only_transparent { + if only_transparent && revision == Revision::R0 { Err(ParseError::OnlyTransparent) } else { // All checks pass! diff --git a/components/zcash_address/src/kind/unified/fvk.rs b/components/zcash_address/src/kind/unified/fvk.rs index 4be6549d6..89db88fc8 100644 --- a/components/zcash_address/src/kind/unified/fvk.rs +++ b/components/zcash_address/src/kind/unified/fvk.rs @@ -369,9 +369,6 @@ mod tests { 0xf4, 0xf5, 0x16, 0xef, 0x5c, 0xe0, 0x26, 0xbc, 0x23, 0x73, 0x76, 0x3f, 0x4b, ]; - assert_eq!( - Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]), - Err(ParseError::OnlyTransparent) - ); + assert_matches!(Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]), Ok(_)); } } diff --git a/components/zcash_address/src/kind/unified/ivk.rs b/components/zcash_address/src/kind/unified/ivk.rs index 1da276923..900f29b99 100644 --- a/components/zcash_address/src/kind/unified/ivk.rs +++ b/components/zcash_address/src/kind/unified/ivk.rs @@ -348,9 +348,6 @@ mod tests { 0xbd, 0xfe, 0xa4, 0xb7, 0x47, 0x20, 0x92, 0x6, 0xf0, 0x0, 0xf9, 0x64, ]; - assert_eq!( - Uivk::parse_internal(Uivk::MAINNET_R0, &encoded[..]), - Err(ParseError::OnlyTransparent) - ); + assert_matches!(Uivk::parse_internal(Uivk::MAINNET_R0, &encoded[..]), Ok(_)); } } diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index a040ac4e3..cdc545536 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -31,6 +31,7 @@ and this library adheres to Rust's notion of - `zcash_keys::keys::UnifiedFullViewingKey::{ expiry_height, set_expiry_height, unset_expiry_height, expiry_time, set_expiry_time, unset_expiry_time, + has_sapling, has_orchard, unknown_data, unknown_metadata }` - `zcash_keys::keys::UnifiedIncomingViewingKey::{ diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index 86f582c59..10041f162 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -289,7 +289,10 @@ 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() { + if self.expiry_height().is_some() + || self.expiry_time().is_some() + || !(self.has_orchard() || self.has_sapling()) + { Revision::R1 } else { Revision::R0 diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs index 0c625764f..9f93df741 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -965,7 +965,10 @@ 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() { + if self.expiry_height().is_some() + || self.expiry_time().is_some() + || !(self.has_sapling() || self.has_orchard()) + { Revision::R1 } else { Revision::R0 @@ -1011,12 +1014,28 @@ impl UnifiedFullViewingKey { self.sapling.as_ref() } + /// Returns whether this UFVK contains a Sapling item. + pub fn has_sapling(&self) -> bool { + #[cfg(feature = "sapling")] + return self.sapling.is_some(); + #[cfg(not(feature = "sapling"))] + return false; + } + /// Returns the Orchard full viewing key component of this unified key. #[cfg(feature = "orchard")] pub fn orchard(&self) -> Option<&orchard::keys::FullViewingKey> { self.orchard.as_ref() } + /// Returns whether this UFVK contains an Orchard item. + pub fn has_orchard(&self) -> bool { + #[cfg(feature = "orchard")] + return self.orchard.is_some(); + #[cfg(not(feature = "orchard"))] + return false; + } + /// Returns any unknown data items parsed from the encoded form of the key. pub fn unknown_data(&self) -> &[(u32, Vec)] { self.unknown_data.as_ref() From 7aecb82bef65caf873ed48f7be12c678f4589575 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 22 Apr 2024 12:27:31 -0600 Subject: [PATCH 08/10] Only allow construction of Unified keys & addresses with at least one data item. --- .../src/data_api/testing/orchard.rs | 5 +- zcash_keys/CHANGELOG.md | 16 +- zcash_keys/src/address.rs | 58 +++-- zcash_keys/src/keys.rs | 201 ++++++++++++++---- 4 files changed, 218 insertions(+), 62 deletions(-) diff --git a/zcash_client_backend/src/data_api/testing/orchard.rs b/zcash_client_backend/src/data_api/testing/orchard.rs index 4ca673eb2..0c0ce03d8 100644 --- a/zcash_client_backend/src/data_api/testing/orchard.rs +++ b/zcash_client_backend/src/data_api/testing/orchard.rs @@ -75,6 +75,7 @@ impl ShieldedPoolTester for OrchardPoolTester { None, None, ) + .expect("Address construction is valid") .into() } @@ -154,7 +155,9 @@ impl ShieldedPoolTester for OrchardPoolTester { return result.map(|(note, addr, memo)| { ( Note::Orchard(note), - UnifiedAddress::from_receivers(Some(addr), None, None).into(), + UnifiedAddress::from_receivers(Some(addr), None, None) + .expect("Address construction is valid") + .into(), MemoBytes::from_bytes(&memo).expect("correct length"), ) }); diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index cdc545536..742e8c96c 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -40,16 +40,27 @@ and this library adheres to Rust's notion of unknown_data, unknown_metadata }` - `zcash_keys::address::UnifiedAddressRequest::unsafe_new_without_expiry` +- `zcash_keys::keys::UnifiedKeyError` - `zcash_keys::address::UnifiedAddressRequest::new` now takes additional optional `expiry_height` and `expiry_time` arguments. - `zcash_keys::address::UnifiedAddress::from_receivers` is now only available under the `test-dependencies` feature flag. It should not be used for - non-test purposes as it can potentially generate addresses that contain no - receivers. + non-test purposes. +- `zcash_keys::address::UnifiedAddress` can now represent ZIP 316 Revision 1 + unified addresses, which may lack any shielded receivers; addresses are now + constrained to include at least one data item, but are not otherwise + restricted in their receiver composition. - `zcash_keys::keys::UnifiedSpendingKey::default_address` is now failable, and now returns a `Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError>`. - `zcash_keys::address::Address` variants now `Box` their contents to avoid large discrepancies in enum variant sizing. +- `zcash_keys::keys::DecodingError` has added variant `ConstraintViolation`. +- The following methods now return `Result<_, UnifiedKeyError>` instead of + `Result<_, DerivationError>` + - `zcash_keys::keys::UnifiedSpendingKey::from_seed` + - `zcash_keys::keys::UnifiedFullViewingKey::new` + - `zcash_keys::keys::UnifiedFullViewingKey::from_orchard_fvk` + - `zcash_keys::keys::UnifiedFullViewingKey::from_sapling_extended_full_viewing_key` ### Removed - `zcash_keys::keys::UnifiedAddressRequest::all` (use @@ -58,6 +69,7 @@ and this library adheres to Rust's notion of - `zcash_keys::address::UnifiedAddress::unknown` (use `unknown_data` instead.) - `zcash_keys::address::UnifiedAddressRequest::unsafe_new` (use `UnifiedAddressRequest::unsafe_new_without_expiry` instead) +- `zcash_keys::keys::DerivationError` (use `UnifiedKeyError` instead). ## [0.6.0] - 2024-12-16 diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index 10041f162..463a4577d 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -98,7 +98,7 @@ impl TryFrom for UnifiedAddress { } } - Ok(Self { + Self::from_checked_parts( #[cfg(feature = "orchard")] orchard, #[cfg(feature = "sapling")] @@ -108,7 +108,8 @@ impl TryFrom for UnifiedAddress { expiry_height, expiry_time, unknown_metadata, - }) + ) + .ok_or("Unified addresses without data fields are not permitted.") } } @@ -123,36 +124,55 @@ impl UnifiedAddress { #[cfg(feature = "orchard")] orchard: Option, #[cfg(feature = "sapling")] sapling: Option, transparent: Option, - ) -> Self { - Self::new_internal( + ) -> Option { + Self::from_checked_parts( #[cfg(feature = "orchard")] orchard, #[cfg(feature = "sapling")] sapling, transparent, + vec![], None, None, + vec![], ) } - pub(crate) fn new_internal( + pub(crate) fn from_checked_parts( #[cfg(feature = "orchard")] orchard: Option, #[cfg(feature = "sapling")] sapling: Option, transparent: Option, + unknown_data: Vec<(u32, Vec)>, expiry_height: Option, expiry_time: Option, - ) -> Self { - Self { + unknown_metadata: Vec<(u32, Vec)>, + ) -> Option { + let has_transparent = transparent.is_some(); + + #[allow(unused_mut)] + let mut has_shielded = false; + #[cfg(feature = "sapling")] + { + has_shielded = has_shielded || sapling.is_some(); + } + #[cfg(feature = "orchard")] + { + has_shielded = has_shielded || orchard.is_some(); + } + + let has_unknown = !unknown_data.is_empty(); + + (has_transparent || has_shielded || has_unknown).then_some(Self { #[cfg(feature = "orchard")] orchard, #[cfg(feature = "sapling")] sapling, transparent, - unknown_data: vec![], + unknown_data, expiry_height, expiry_time, - unknown_metadata: vec![], - } + unknown_metadata, + }) } /// Returns whether this address has an Orchard receiver. @@ -616,15 +636,25 @@ mod tests { let transparent = None; #[cfg(all(feature = "orchard", feature = "sapling"))] - let ua = UnifiedAddress::new_internal(orchard, sapling, transparent, None, None); + let ua = UnifiedAddress::from_checked_parts( + orchard, + sapling, + transparent, + vec![], + None, + None, + vec![], + ); #[cfg(all(not(feature = "orchard"), feature = "sapling"))] - let ua = UnifiedAddress::new_internal(sapling, transparent, None, None); + let ua = + UnifiedAddress::from_checked_parts(sapling, transparent, vec![], None, None, vec![]); #[cfg(all(feature = "orchard", not(feature = "sapling")))] - let ua = UnifiedAddress::new_internal(orchard, transparent, None, None); + let ua = + UnifiedAddress::from_checked_parts(orchard, transparent, vec![], None, None, vec![]); - let addr = Address::from(ua); + let addr = Address::from(ua.expect("test UAs are constructed in valid configurations")); let addr_str = addr.encode(&MAIN_NETWORK); assert_eq!(Address::decode(&MAIN_NETWORK, &addr_str), Some(addr)); } diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs index 9f93df741..4af5d4a57 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -1,4 +1,5 @@ //! Helper functions for managing light client key material. +use alloc::borrow::ToOwned; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::fmt::{self, Display}; @@ -93,31 +94,44 @@ fn to_transparent_child_index(j: DiversifierIndex) -> Option) -> fmt::Result { match self { + UnifiedKeyError::DataItemRequired => write!( + _f, + "Unified keys must contain at least one non-metadata item." + ), #[cfg(feature = "orchard")] - DerivationError::Orchard(e) => write!(_f, "Orchard error: {}", e), + UnifiedKeyError::Orchard(e) => write!(_f, "Orchard key derivation error: {}", e), #[cfg(feature = "transparent-inputs")] - DerivationError::Transparent(e) => write!(_f, "Transparent error: {}", e), + UnifiedKeyError::Transparent(e) => { + write!(_f, "Transparent key derivation error: {}", e) + } #[cfg(not(any(feature = "orchard", feature = "transparent-inputs")))] other => { - unreachable!("Unhandled DerivationError variant {:?}", other) + unreachable!("Unhandled UnifiedKeyError variant {:?}", other) } } } } #[cfg(feature = "std")] -impl std::error::Error for DerivationError {} +impl std::error::Error for UnifiedKeyError {} /// A version identifier for the encoding of unified spending keys. /// @@ -150,8 +164,11 @@ pub enum DecodingError { LengthMismatch(Typecode, u32), #[cfg(feature = "unstable")] InsufficientData(Typecode), - /// The key data could not be decoded from its string representation to a valid key. + /// The key data for the given key type could not be decoded from its string representation to + /// a valid key. KeyDataInvalid(Typecode), + /// Decoding resulted in a value that would violate validity constraints. + ConstraintViolation(String), } impl core::fmt::Display for DecodingError { @@ -180,6 +197,9 @@ impl core::fmt::Display for DecodingError { write!(f, "Insufficient data for typecode {:?}", t) } DecodingError::KeyDataInvalid(t) => write!(f, "Invalid key data for key type {:?}", t), + DecodingError::ConstraintViolation(s) => { + write!(f, "Decoding produced an invalid value: {}", s) + } } } } @@ -187,6 +207,24 @@ impl core::fmt::Display for DecodingError { #[cfg(feature = "std")] impl std::error::Error for DecodingError {} +impl From for DecodingError { + fn from(value: UnifiedKeyError) -> Self { + match value { + UnifiedKeyError::DataItemRequired => { + Self::ConstraintViolation("At least one Data Item must be present.".to_owned()) + } + #[cfg(feature = "orchard")] + UnifiedKeyError::Orchard(_) => { + Self::KeyDataInvalid(Typecode::Data(unified::DataTypecode::Orchard)) + } + #[cfg(feature = "transparent-inputs")] + UnifiedKeyError::Transparent(_) => { + Self::KeyDataInvalid(Typecode::Data(unified::DataTypecode::P2pkh)) + } + } + } +} + #[cfg(feature = "unstable")] impl Era { /// Returns the unique identifier for the era. @@ -222,7 +260,7 @@ impl UnifiedSpendingKey { _params: &P, seed: &[u8], _account: AccountId, - ) -> Result { + ) -> Result { if seed.len() < 32 { panic!("ZIP 32 seeds MUST be at least 32 bytes"); } @@ -230,12 +268,12 @@ impl UnifiedSpendingKey { UnifiedSpendingKey::from_checked_parts( #[cfg(feature = "transparent-inputs")] transparent::keys::AccountPrivKey::from_seed(_params, seed, _account) - .map_err(DerivationError::Transparent)?, + .map_err(UnifiedKeyError::Transparent)?, #[cfg(feature = "sapling")] sapling::spending_key(seed, _params.coin_type(), _account), #[cfg(feature = "orchard")] orchard::keys::SpendingKey::from_zip32_seed(seed, _params.coin_type(), _account) - .map_err(DerivationError::Orchard)?, + .map_err(UnifiedKeyError::Orchard)?, ) } @@ -245,7 +283,7 @@ impl UnifiedSpendingKey { #[cfg(feature = "transparent-inputs")] transparent: transparent::keys::AccountPrivKey, #[cfg(feature = "sapling")] sapling: sapling::ExtendedSpendingKey, #[cfg(feature = "orchard")] orchard: orchard::keys::SpendingKey, - ) -> Result { + ) -> Result { // Verify that FVK and IVK derivation succeed; we don't want to construct a USK // that can't derive transparent addresses. #[cfg(feature = "transparent-inputs")] @@ -461,7 +499,7 @@ impl UnifiedSpendingKey { #[cfg(feature = "orchard")] orchard.unwrap(), ) - .map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH)); + .map_err(DecodingError::from); } } } @@ -672,8 +710,8 @@ impl UnifiedAddressRequest { p2pkh: ReceiverRequirement, ) -> Self { use ReceiverRequirement::*; - if matches!(orchard, Omit) && matches!(sapling, Omit) { - panic!("At least one shielded receiver must be allowed.") + if matches!(orchard, Omit) && matches!(sapling, Omit) && matches!(p2pkh, Omit) { + panic!("At least one receiver type must be allowed.") } Self { @@ -687,9 +725,9 @@ impl UnifiedAddressRequest { } #[cfg(feature = "transparent-inputs")] -impl From for DerivationError { +impl From for UnifiedKeyError { fn from(e: bip32::Error) -> Self { - DerivationError::Transparent(e) + UnifiedKeyError::Transparent(e) } } @@ -722,7 +760,7 @@ impl UnifiedFullViewingKey { #[cfg(feature = "sapling")] sapling: Option, #[cfg(feature = "orchard")] orchard: Option, // TODO: Implement construction of UFVKs with metadata items. - ) -> Result { + ) -> Result { Self::from_checked_parts( #[cfg(feature = "transparent-inputs")] transparent, @@ -742,7 +780,7 @@ impl UnifiedFullViewingKey { #[cfg(feature = "unstable-frost")] pub fn from_orchard_fvk( orchard: orchard::keys::FullViewingKey, - ) -> Result { + ) -> Result { Self::from_checked_parts( #[cfg(feature = "transparent-inputs")] None, @@ -762,7 +800,7 @@ impl UnifiedFullViewingKey { #[cfg(all(feature = "sapling", feature = "unstable"))] pub fn from_sapling_extended_full_viewing_key( sapling: ExtendedFullViewingKey, - ) -> Result { + ) -> Result { Self::from_checked_parts( #[cfg(feature = "transparent-inputs")] None, @@ -791,7 +829,7 @@ impl UnifiedFullViewingKey { expiry_height: Option, expiry_time: Option, unknown_metadata: Vec<(u32, Vec)>, - ) -> Result { + ) -> Result { // Verify that IVK derivation succeeds; we don't want to construct a UFVK // that can't derive transparent addresses. #[cfg(feature = "transparent-inputs")] @@ -800,18 +838,37 @@ impl UnifiedFullViewingKey { .map(|t| t.derive_external_ivk()) .transpose()?; - Ok(UnifiedFullViewingKey { - #[cfg(feature = "transparent-inputs")] - transparent, - #[cfg(feature = "sapling")] - sapling, - #[cfg(feature = "orchard")] - orchard, - unknown_data, - expiry_height, - expiry_time, - unknown_metadata, - }) + #[allow(unused_mut)] + let mut has_data = !unknown_data.is_empty(); + #[cfg(feature = "transparent-inputs")] + { + has_data = has_data || transparent.is_some(); + } + #[cfg(feature = "sapling")] + { + has_data = has_data || sapling.is_some(); + } + #[cfg(feature = "orchard")] + { + has_data = has_data || orchard.is_some(); + } + + if has_data { + Ok(Self { + #[cfg(feature = "transparent-inputs")] + transparent, + #[cfg(feature = "sapling")] + sapling, + #[cfg(feature = "orchard")] + orchard, + unknown_data, + expiry_height, + expiry_time, + unknown_metadata, + }) + } else { + Err(UnifiedKeyError::DataItemRequired) + } } /// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding. @@ -1170,6 +1227,48 @@ impl UnifiedIncomingViewingKey { } } + fn from_checked_parts( + #[cfg(feature = "transparent-inputs")] transparent: Option, + #[cfg(feature = "sapling")] sapling: Option<::sapling::zip32::IncomingViewingKey>, + #[cfg(feature = "orchard")] orchard: Option, + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, + ) -> Result { + #[allow(unused_mut)] + let mut has_data = !unknown_data.is_empty(); + #[cfg(feature = "transparent-inputs")] + { + has_data = has_data || transparent.is_some(); + } + #[cfg(feature = "sapling")] + { + has_data = has_data || sapling.is_some(); + } + #[cfg(feature = "orchard")] + { + has_data = has_data || orchard.is_some(); + } + + if has_data { + Ok(Self { + #[cfg(feature = "transparent-inputs")] + transparent, + #[cfg(feature = "sapling")] + sapling, + #[cfg(feature = "orchard")] + orchard, + unknown_data, + expiry_height, + expiry_time, + unknown_metadata, + }) + } else { + Err(UnifiedKeyError::DataItemRequired) + } + } + /// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding. /// /// [ZIP 316]: https://zips.z.cash/zip-0316 @@ -1254,7 +1353,7 @@ impl UnifiedIncomingViewingKey { } } - Ok(Self { + Ok(Self::from_checked_parts( #[cfg(feature = "transparent-inputs")] transparent, #[cfg(feature = "sapling")] @@ -1265,7 +1364,7 @@ impl UnifiedIncomingViewingKey { expiry_height, expiry_time, unknown_metadata, - }) + )?) } /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. @@ -1489,12 +1588,13 @@ impl UnifiedIncomingViewingKey { #[cfg(not(feature = "transparent-inputs"))] let transparent = None; - Ok(UnifiedAddress::new_internal( + Ok(UnifiedAddress::from_checked_parts( #[cfg(feature = "orchard")] orchard, #[cfg(feature = "sapling")] sapling, transparent, + self.unknown_data.clone(), self.expiry_height .zip(request.expiry_height) .map(|(l, r)| std::cmp::min(l, r)) @@ -1505,7 +1605,9 @@ impl UnifiedIncomingViewingKey { .map(|(l, r)| std::cmp::min(l, r)) .or(self.expiry_time) .or(request.expiry_time), - )) + self.unknown_metadata.clone(), + ) + .expect("UIVK validity constraints are checked at construction.")) } /// Searches the diversifier space starting at diversifier index `j` for one which will produce @@ -1656,15 +1758,13 @@ mod tests { #[cfg(feature = "transparent-inputs")] use { - crate::{address::Address, encoding::AddressCodec}, + crate::encoding::AddressCodec, alloc::string::ToString, alloc::vec::Vec, transparent::keys::{AccountPrivKey, IncomingViewingKey}, - zcash_address::test_vectors, - zip32::DiversifierIndex, }; - #[cfg(feature = "unstable")] + #[cfg(all(feature = "unstable", any(feature = "sapling", feature = "orchard")))] use super::{testing::arb_unified_spending_key, Era, UnifiedSpendingKey}; #[cfg(all(feature = "orchard", feature = "unstable"))] @@ -1821,9 +1921,14 @@ mod tests { } #[test] - #[cfg(feature = "transparent-inputs")] + #[cfg(all( + feature = "transparent-inputs", + any(feature = "orchard", feature = "sapling") + ))] fn ufvk_derivation() { - use crate::keys::UnifiedAddressRequest; + use crate::{address::Address, keys::UnifiedAddressRequest}; + use zcash_address::test_vectors; + use zip32::DiversifierIndex; use super::{ReceiverRequirement::*, UnifiedSpendingKey}; @@ -2009,9 +2114,14 @@ mod tests { } #[test] - #[cfg(feature = "transparent-inputs")] + #[cfg(all( + feature = "transparent-inputs", + any(feature = "sapling", feature = "orchard") + ))] fn uivk_derivation() { - use crate::keys::UnifiedAddressRequest; + use crate::{address::Address, keys::UnifiedAddressRequest}; + use zcash_address::test_vectors; + use zip32::DiversifierIndex; use super::{ReceiverRequirement::*, UnifiedSpendingKey}; @@ -2073,7 +2183,7 @@ mod tests { proptest! { #[test] - #[cfg(feature = "unstable")] + #[cfg(all(feature = "unstable", any(feature = "orchard", feature = "sapling")))] fn prop_usk_roundtrip(usk in arb_unified_spending_key(zcash_protocol::consensus::Network::MainNetwork)) { let encoded = usk.to_bytes(Era::Orchard); @@ -2102,6 +2212,7 @@ mod tests { #[cfg(feature = "orchard")] assert!(bool::from(decoded.orchard().ct_eq(usk.orchard()))); + #[cfg(feature = "sapling")] assert_eq!(decoded.sapling(), usk.sapling()); #[cfg(feature = "transparent-inputs")] From fb88ebde1dfe303e081e67d14f34d796d834dd7b Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 10 Dec 2024 16:35:21 -0700 Subject: [PATCH 09/10] zcash_keys: Make unified key construction APIs consistent with one another. --- zcash_client_sqlite/src/lib.rs | 8 ++++++++ zcash_keys/CHANGELOG.md | 5 +++++ zcash_keys/src/keys.rs | 28 +++++++++++++++++----------- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index bcf277197..f7e707ae8 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -2068,6 +2068,10 @@ mod tests { ufvk.sapling().cloned(), #[cfg(feature = "orchard")] ufvk.orchard().cloned(), + vec![], + None, + None, + vec![], ) .unwrap(); assert_matches!( @@ -2092,6 +2096,10 @@ mod tests { ufvk.transparent().cloned(), ufvk.sapling().cloned(), None, + vec![], + None, + None, + vec![], ) .unwrap(); assert_matches!( diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index 742e8c96c..c5b832e3a 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -61,6 +61,11 @@ and this library adheres to Rust's notion of - `zcash_keys::keys::UnifiedFullViewingKey::new` - `zcash_keys::keys::UnifiedFullViewingKey::from_orchard_fvk` - `zcash_keys::keys::UnifiedFullViewingKey::from_sapling_extended_full_viewing_key` +- `zcash_keys::keys::UnifiedFullViewingKey::new` has been modified to allow + construction that includes unknown data as well as metadata fields. +- `zcash_keys::keys::UnifiedIncomingViewingKey::new` now returns a + `Result` instead of a + potentially invalid value. ### Removed - `zcash_keys::keys::UnifiedAddressRequest::all` (use diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs index 4af5d4a57..440b7a20d 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -759,7 +759,10 @@ impl UnifiedFullViewingKey { >, #[cfg(feature = "sapling")] sapling: Option, #[cfg(feature = "orchard")] orchard: Option, - // TODO: Implement construction of UFVKs with metadata items. + unknown_data: Vec<(u32, Vec)>, + expiry_height: Option, + expiry_time: Option, + unknown_metadata: Vec<(u32, Vec)>, ) -> Result { Self::from_checked_parts( #[cfg(feature = "transparent-inputs")] @@ -768,12 +771,10 @@ impl UnifiedFullViewingKey { sapling, #[cfg(feature = "orchard")] orchard, - // We don't currently allow constructing new UFVKs with unknown items, but we store - // this to allow parsing such UFVKs. - vec![], - None, - None, - vec![], + unknown_data, + expiry_height, + expiry_time, + unknown_metadata, ) } @@ -1212,8 +1213,8 @@ impl UnifiedIncomingViewingKey { expiry_height: Option, expiry_time: Option, unknown_metadata: Vec<(u32, Vec)>, - ) -> UnifiedIncomingViewingKey { - UnifiedIncomingViewingKey { + ) -> Result { + Self::from_checked_parts( #[cfg(feature = "transparent-inputs")] transparent, #[cfg(feature = "sapling")] @@ -1224,7 +1225,7 @@ impl UnifiedIncomingViewingKey { expiry_height, expiry_time, unknown_metadata, - } + ) } fn from_checked_parts( @@ -1829,6 +1830,10 @@ mod tests { sapling, #[cfg(feature = "orchard")] orchard, + vec![], + None, + None, + vec![], ); let ufvk = ufvk.expect("Orchard or Sapling fvk is present."); @@ -2022,7 +2027,8 @@ mod tests { None, None, vec![], - ); + ) + .unwrap(); let encoded = uivk.render().encode(&NetworkType::Main); From e16de948dec43e9faf5f42115cf30f153162f129 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 11 Dec 2024 11:05:15 -0700 Subject: [PATCH 10/10] Fix feature-dependent clippy complaints. --- zcash_keys/src/keys.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs index 440b7a20d..b55574e72 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -122,10 +122,6 @@ impl Display for UnifiedKeyError { UnifiedKeyError::Transparent(e) => { write!(_f, "Transparent key derivation error: {}", e) } - #[cfg(not(any(feature = "orchard", feature = "transparent-inputs")))] - other => { - unreachable!("Unhandled UnifiedKeyError variant {:?}", other) - } } } } @@ -658,7 +654,7 @@ impl UnifiedAddressRequest { expiry_time: Option, ) -> Result { use ReceiverRequirement::*; - if orchard == Omit && sapling == Omit { + if orchard == Omit && sapling == Omit && p2pkh == Omit { Err(()) } else { Ok(Self {