Compare commits

...

7 Commits

Author SHA1 Message Date
Kris Nuttycombe 078b4ffa45 Allow transparent-only unified addresses. 2024-04-22 12:29:13 -06:00
Kris Nuttycombe 478f2e39b2 Apply suggestions from code review
Co-authored-by: Daira-Emma Hopwood <daira@jacaranda.org>
2024-04-01 16:08:51 -06:00
Kris Nuttycombe c842a7c8df `zcash_address`: Add support for ZIP 316, Revision 1 2024-04-01 16:08:50 -06:00
Kris Nuttycombe c82a06ccc5 zcash_keys: Box `Address` elements to avoid large variations in enum variant sizes 2024-04-01 16:08:49 -06:00
Kris Nuttycombe dd43246bb8 zcash_keys: Update key and address types to include ZIP-316 metadata items. 2024-04-01 15:49:44 -06:00
Kris Nuttycombe f7b635a7f2 zcash_address: Add handling for Unified Metadata Items 2024-04-01 15:30:59 -06:00
Kris Nuttycombe babc925d4e zcash_keys: Add missing entries to `zcash_keys-0.1.0` CHANGELOG 2024-04-01 15:30:59 -06:00
25 changed files with 1338 additions and 686 deletions

View File

@ -7,6 +7,27 @@ and this library adheres to Rust's notion of
## [Unreleased]
### Added
- `zcash_address::unified`:
- `Address::receivers`
- `Container::revision`
- `DataTypecode`
- `Item`
- `MetadataItem`
- `MetadataTypecode`
- `Revision`
### Changed
- `zcash_address::unified`:
- `Typecode` has changed. Instead of having a variant for each receiver type,
it now has two variants, `Typecode::Data` and `Typecode::Metadata`.
- `Encoding::try_from_items` now takes an additional `Revision` argument.
### Removed
- `zcash_address::unified::Container::items` Preference order is only
significant when considering unified address receivers; use
`Address::receivers` instead.
## [0.3.2] - 2024-03-06
### Added
- `zcash_address::convert`:

View File

@ -180,7 +180,11 @@ mod tests {
use assert_matches::assert_matches;
use super::*;
use crate::{kind::unified, Network};
use crate::{
kind::unified,
unified::{Item, Receiver, Revision},
Network,
};
fn encoding(encoded: &str, decoded: ZcashAddress) {
assert_eq!(decoded.to_string(), encoded);
@ -230,21 +234,30 @@ mod tests {
"u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl",
ZcashAddress {
net: Network::Main,
kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);
encoding(
"utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s",
ZcashAddress {
net: Network::Test,
kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);
encoding(
"uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3",
ZcashAddress {
net: Network::Regtest,
kind: AddressKind::Unified(unified::Address(vec![unified::address::Receiver::Sapling([0; 43])])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);

View File

@ -6,6 +6,7 @@ use std::convert::{TryFrom, TryInto};
use std::error::Error;
use std::fmt;
use std::num::TryFromIntError;
use zcash_encoding::MAX_COMPACT_SIZE;
use crate::Network;
@ -22,9 +23,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.
@ -39,7 +40,37 @@ pub enum Typecode {
Unknown(u32),
}
impl Typecode {
impl TryFrom<u32> for DataTypecode {
type Error = ();
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
match typecode {
0x00 => Ok(DataTypecode::P2pkh),
0x01 => Ok(DataTypecode::P2sh),
0x02 => Ok(DataTypecode::Sapling),
0x03 => Ok(DataTypecode::Orchard),
0x04..=0xBF | 0xFD..=MAX_COMPACT_SIZE => Ok(DataTypecode::Unknown(typecode)),
_ => Err(()),
}
}
}
impl From<DataTypecode> for u32 {
fn from(t: DataTypecode) -> Self {
match t {
DataTypecode::P2pkh => 0x00,
DataTypecode::P2sh => 0x01,
DataTypecode::Sapling => 0x02,
DataTypecode::Orchard => 0x03,
DataTypecode::Unknown(typecode) => typecode,
}
}
}
impl DataTypecode {
/// A total ordering over the data typecodes that can be used to sort
/// receivers and/or key items in order of decreasing priority,
/// as specified in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses)
pub fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
match (a, b) {
// Trivial equality checks.
@ -69,51 +100,213 @@ 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<u32> for MetadataTypecode {
type Error = ();
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
match typecode {
0xC0..=0xDF => Ok(MetadataTypecode::Unknown(typecode)),
0xE0 => Ok(MetadataTypecode::ExpiryHeight),
0xE1 => Ok(MetadataTypecode::ExpiryTime),
0xE2..=0xFC => Ok(MetadataTypecode::MustUnderstand(typecode)),
_ => Err(()),
}
}
}
impl From<MetadataTypecode> for u32 {
fn from(t: MetadataTypecode) -> Self {
match t {
MetadataTypecode::ExpiryHeight => 0xE0,
MetadataTypecode::ExpiryTime => 0xE1,
MetadataTypecode::MustUnderstand(value) => value,
MetadataTypecode::Unknown(value) => value,
}
}
}
/// An enumeration of the Unified Container Item Typecodes.
///
/// Unified Address Items are partitioned into two sets: data items, which include
/// receivers and viewing keys, and metadata items.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Typecode {
/// A data (receiver or viewing key) typecode.
Data(DataTypecode),
/// A metadata typecode.
Metadata(MetadataTypecode),
}
impl Typecode {
/// The typecode for p2pkh data items.
pub const P2PKH: Typecode = Typecode::Data(DataTypecode::P2pkh);
/// The typecode for p2sh data items.
pub const P2SH: Typecode = Typecode::Data(DataTypecode::P2sh);
/// The typecode for Sapling data items.
pub const SAPLING: Typecode = Typecode::Data(DataTypecode::Sapling);
/// The typecode for Orchard data items.
pub const ORCHARD: Typecode = Typecode::Data(DataTypecode::Orchard);
}
impl TryFrom<u32> for Typecode {
type Error = ParseError;
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
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<Typecode> 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<Typecode> for usize {
type Error = TryFromIntError;
fn try_from(t: Typecode) -> Result<Self, Self::Error> {
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<u8> },
}
impl MetadataItem {
/// Parse a metadata item for the specified metadata typecode from the provided bytes.
pub fn parse(
revision: Revision,
typecode: MetadataTypecode,
data: &[u8],
) -> Result<Self, ParseError> {
use MetadataTypecode::*;
use Revision::*;
match (revision, typecode) {
(R1, ExpiryHeight) => data
.try_into()
.map(u32::from_le_bytes)
.map(MetadataItem::ExpiryHeight)
.map_err(|_| {
ParseError::InvalidEncoding(
"Expiry height must be a 32-bit little-endian value.".to_string(),
)
}),
(R1, ExpiryTime) => data
.try_into()
.map(u64::from_le_bytes)
.map(MetadataItem::ExpiryTime)
.map_err(|_| {
ParseError::InvalidEncoding(
"Expiry time must be a 64-bit little-endian value.".to_string(),
)
}),
(R0, ExpiryHeight | ExpiryTime) => Err(ParseError::NotUnderstood(typecode.into())),
(R0 | R1, MustUnderstand(tc)) => Err(ParseError::NotUnderstood(tc)),
// This implementation treats the 0xC0..OxFD range as unknown metadata for both R0 and
// R1, as no typecodes were specified in this range for R0 and were "reclaimed" as
// metadata codes by ZIP 316 at the time R1 was introduced.
(R0 | R1, Unknown(typecode)) => Ok(MetadataItem::Unknown {
typecode,
data: data.to_vec(),
}),
}
}
/// Returns the typecode for this metadata item.
pub fn typecode(&self) -> MetadataTypecode {
match self {
MetadataItem::ExpiryHeight(_) => MetadataTypecode::ExpiryHeight,
MetadataItem::ExpiryTime(_) => MetadataTypecode::ExpiryTime,
MetadataItem::Unknown { typecode, .. } => MetadataTypecode::Unknown(*typecode),
}
}
/// Returns the raw bytes of this metadata item.
pub fn data(&self) -> Vec<u8> {
match self {
MetadataItem::ExpiryHeight(h) => h.to_le_bytes().to_vec(),
MetadataItem::ExpiryTime(t) => t.to_le_bytes().to_vec(),
MetadataItem::Unknown { data, .. } => data.clone(),
}
}
}
/// A Unified Encoding Item.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Item<T> {
/// A data item; either a receiver (for Unified Addresses) or a key (for Unified Viewing Keys)
Data(T),
/// A metadata item.
Metadata(MetadataItem),
}
impl<T: private::SealedDataItem> Item<T> {
/// Returns the typecode for this item.
pub fn typecode(&self) -> Typecode {
match self {
Item::Data(d) => Typecode::Data(d.typecode()),
Item::Metadata(m) => Typecode::Metadata(m.typecode()),
}
}
/// The total ordering over items by their typecodes, used for encoding as specified
/// in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses)
pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering {
u32::from(a.typecode()).cmp(&u32::from(b.typecode()))
}
/// Returns the raw binary representation of the data for this item.
pub fn data(&self) -> Vec<u8> {
match self {
Item::Data(d) => d.data().to_vec(),
Item::Metadata(m) => m.data(),
}
}
/// Returns whether this item is a transparent receiver or key.
pub fn is_transparent_data_item(&self) -> bool {
self.typecode() == Typecode::P2PKH || self.typecode() == Typecode::P2SH
}
}
@ -125,7 +318,7 @@ pub enum ParseError {
/// The unified container contains a duplicated typecode.
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.
@ -136,6 +329,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 {
@ -151,67 +346,93 @@ 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
)
}
}
}
}
impl Error for ParseError {}
/// The revision of the Unified Address standard that an address was parsed under.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Revision {
R0,
R1,
}
pub(crate) mod private {
use super::{ParseError, Typecode, PADDING_LEN};
use crate::Network;
use super::{DataTypecode, ParseError, Revision, Typecode, PADDING_LEN};
use crate::{
unified::{Item, MetadataItem},
Network,
};
use std::{
cmp,
convert::{TryFrom, TryInto},
io::Write,
};
use zcash_encoding::CompactSize;
/// A raw address or viewing key.
pub trait SealedItem: for<'a> TryFrom<(u32, &'a [u8]), Error = ParseError> + Clone {
fn typecode(&self) -> Typecode;
pub trait SealedDataItem: Clone {
/// Parse a data item for the specified data typecode from the provided bytes.
fn parse(tc: DataTypecode, value: &[u8]) -> Result<Self, ParseError>;
/// Returns the typecode of this data item.
fn typecode(&self) -> DataTypecode;
/// Returns the raw bytes of this data item.
fn data(&self) -> &[u8];
fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
match Typecode::preference_order(&a.typecode(), &b.typecode()) {
cmp::Ordering::Equal => a.data().cmp(b.data()),
res => res,
}
}
fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering {
match Typecode::encoding_order(&a.typecode(), &b.typecode()) {
cmp::Ordering::Equal => a.data().cmp(b.data()),
res => res,
}
}
}
/// A Unified Container containing addresses or viewing keys.
pub trait SealedContainer: super::Container + std::marker::Sized {
const MAINNET: &'static str;
const TESTNET: &'static str;
const REGTEST: &'static str;
const MAINNET_R0: &'static str;
const TESTNET_R0: &'static str;
const REGTEST_R0: &'static str;
const MAINNET_R1: &'static str;
const TESTNET_R1: &'static str;
const REGTEST_R1: &'static str;
/// Implementations of this method should act as unchecked constructors
/// of the container type; the caller is guaranteed to check the
/// general invariants that apply to all unified containers.
fn from_inner(items: Vec<Self::Item>) -> Self;
fn from_inner(revision: Revision, items: Vec<Item<Self::DataItem>>) -> Self;
fn network_hrp(network: &Network) -> &'static str {
match network {
Network::Main => Self::MAINNET,
Network::Test => Self::TESTNET,
Network::Regtest => Self::REGTEST,
fn network_hrp(revision: Revision, network: &Network) -> &'static str {
match (revision, network) {
(Revision::R0, Network::Main) => Self::MAINNET_R0,
(Revision::R0, Network::Test) => Self::TESTNET_R0,
(Revision::R0, Network::Regtest) => Self::REGTEST_R0,
(Revision::R1, Network::Main) => Self::MAINNET_R1,
(Revision::R1, Network::Test) => Self::TESTNET_R1,
(Revision::R1, Network::Regtest) => Self::REGTEST_R1,
}
}
fn hrp_revision(hrp: &str) -> Option<Revision> {
if hrp == Self::MAINNET_R0 || hrp == Self::TESTNET_R0 || hrp == Self::REGTEST_R0 {
Some(Revision::R0)
} else if hrp == Self::MAINNET_R1 || hrp == Self::TESTNET_R1 || hrp == Self::REGTEST_R1
{
Some(Revision::R1)
} else {
None
}
}
fn hrp_network(hrp: &str) -> Option<Network> {
if hrp == Self::MAINNET {
if hrp == Self::MAINNET_R0 || hrp == Self::MAINNET_R1 {
Some(Network::Main)
} else if hrp == Self::TESTNET {
} else if hrp == Self::TESTNET_R0 || hrp == Self::TESTNET_R1 {
Some(Network::Test)
} else if hrp == Self::REGTEST {
} else if hrp == Self::REGTEST_R0 || hrp == Self::REGTEST_R1 {
Some(Network::Regtest)
} else {
None
@ -227,7 +448,7 @@ pub(crate) mod private {
)
.unwrap();
CompactSize::write(&mut writer, data.len()).unwrap();
writer.write_all(data).unwrap();
writer.write_all(&data).unwrap();
}
}
@ -248,10 +469,15 @@ pub(crate) mod private {
}
/// Parse the items of the unified container.
fn parse_items<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Vec<Self::Item>, ParseError> {
fn read_receiver<R: SealedItem>(
#[allow(clippy::type_complexity)]
fn parse_items<T: Into<Vec<u8>>>(
hrp: &str,
buf: T,
) -> Result<(Revision, Vec<Item<Self::DataItem>>), ParseError> {
fn read_item<R: SealedDataItem>(
revision: Revision,
mut cursor: &mut std::io::Cursor<&[u8]>,
) -> Result<R, ParseError> {
) -> Result<Item<R>, ParseError> {
let typecode = CompactSize::read(&mut cursor)
.map(|v| u32::try_from(v).expect("CompactSize::read enforces MAX_SIZE limit"))
.map_err(|e| {
@ -279,12 +505,18 @@ pub(crate) mod private {
length
)));
}
let result = R::try_from((
typecode,
&buf[cursor.position() as usize..addr_end as usize],
));
// 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)?),
Typecode::Metadata(tc) => {
Item::Metadata(MetadataItem::parse(revision, tc, data)?)
}
};
cursor.set_position(addr_end);
result
Ok(result)
}
// Here we allocate if necessary to get a mutable Vec<u8> to unjumble.
@ -308,20 +540,26 @@ pub(crate) mod private {
)),
}?;
let revision = Self::hrp_revision(hrp)
.ok_or_else(|| ParseError::UnknownPrefix(hrp.to_string()))?;
let mut cursor = std::io::Cursor::new(encoded);
let mut result = vec![];
while cursor.position() < encoded.len().try_into().unwrap() {
result.push(read_receiver(&mut cursor)?);
result.push(read_item(revision, &mut cursor)?);
}
assert_eq!(cursor.position(), encoded.len().try_into().unwrap());
Ok(result)
Ok((revision, result))
}
/// A private function that constructs a unified container with the
/// specified items, which must be in ascending typecode order.
fn try_from_items_internal(items: Vec<Self::Item>) -> Result<Self, ParseError> {
assert!(u32::from(Typecode::P2sh) == u32::from(Typecode::P2pkh) + 1);
fn try_from_items_internal(
revision: Revision,
items: Vec<Item<Self::DataItem>>,
) -> Result<Self, ParseError> {
assert!(u32::from(Typecode::P2SH) == u32::from(Typecode::P2PKH) + 1);
let mut only_transparent = true;
let mut prev_code = None; // less than any Some
@ -332,13 +570,13 @@ 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::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();
}
}
@ -346,33 +584,37 @@ pub(crate) mod private {
Err(ParseError::OnlyTransparent)
} else {
// All checks pass!
Ok(Self::from_inner(items))
Ok(Self::from_inner(revision, items))
}
}
fn parse_internal<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Self, ParseError> {
Self::parse_items(hrp, buf).and_then(Self::try_from_items_internal)
Self::parse_items(hrp, buf)
.and_then(|(revision, items)| Self::try_from_items_internal(revision, items))
}
}
}
use private::SealedItem;
use private::SealedDataItem;
/// 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
/// 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(mut items: Vec<Self::Item>) -> Result<Self, ParseError> {
items.sort_unstable_by(Self::Item::encoding_order);
Self::try_from_items_internal(items)
fn try_from_items(
revision: Revision,
mut items: Vec<Item<Self::DataItem>>,
) -> Result<Self, ParseError> {
items.sort_unstable_by(Item::encoding_order);
Self::try_from_items_internal(revision, items)
}
/// Decodes a unified container from its string representation, preserving
@ -399,7 +641,7 @@ pub trait Encoding: private::SealedContainer {
/// ordering of the contained items such that it correctly obeys round-trip
/// serialization invariants.
fn encode(&self, network: &Network) -> String {
let hrp = Self::network_hrp(network);
let hrp = Self::network_hrp(self.revision(), network);
bech32::encode(
hrp,
self.to_jumbled_bytes(hrp).to_base32(),
@ -411,20 +653,13 @@ pub trait Encoding: private::SealedContainer {
/// Trait for for Unified containers, that exposes the items within them.
pub trait Container {
/// The type of item in this unified container.
type Item: SealedItem;
/// The type of data items in this unified container.
type DataItem: SealedDataItem;
/// Returns the items contained within this container, sorted in preference order.
fn items(&self) -> Vec<Self::Item> {
let mut items = self.items_as_parsed().to_vec();
// Unstable sorting is fine, because all items are guaranteed by construction
// to have distinct typecodes.
items.sort_unstable_by(Self::Item::preference_order);
items
}
/// Returns the items in encoding order.
fn items_as_parsed(&self) -> &[Item<Self::DataItem>];
/// 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 revision of the ZIP 316 standard that this unified container
/// conforms to.
fn revision(&self) -> Revision;
}

View File

@ -1,8 +1,8 @@
use super::{private::SealedItem, ParseError, Typecode};
use super::{private::SealedDataItem, DataTypecode, Item, ParseError, Revision};
use std::convert::{TryFrom, TryInto};
use std::{cmp, convert::TryInto};
/// The set of known Receivers for Unified Addresses.
/// The enumeration of Unified Address Receivers of known types.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Receiver {
Orchard([u8; 43]),
@ -12,34 +12,39 @@ pub enum Receiver {
Unknown { typecode: u32, data: Vec<u8> },
}
impl TryFrom<(u32, &[u8])> for Receiver {
type Error = ParseError;
fn try_from((typecode, addr): (u32, &[u8])) -> Result<Self, Self::Error> {
match typecode.try_into()? {
Typecode::P2pkh => addr.try_into().map(Receiver::P2pkh),
Typecode::P2sh => addr.try_into().map(Receiver::P2sh),
Typecode::Sapling => addr.try_into().map(Receiver::Sapling),
Typecode::Orchard => addr.try_into().map(Receiver::Orchard),
Typecode::Unknown(_) => Ok(Receiver::Unknown {
typecode,
data: addr.to_vec(),
}),
}
.map_err(|e| {
ParseError::InvalidEncoding(format!("Invalid address for typecode {}: {}", typecode, e))
})
impl Receiver {
fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
DataTypecode::preference_order(&a.typecode(), &b.typecode())
}
}
impl SealedItem for Receiver {
fn typecode(&self) -> Typecode {
impl SealedDataItem for Receiver {
fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
match typecode {
DataTypecode::P2pkh => data.try_into().map(Receiver::P2pkh),
DataTypecode::P2sh => data.try_into().map(Receiver::P2sh),
DataTypecode::Sapling => data.try_into().map(Receiver::Sapling),
DataTypecode::Orchard => data.try_into().map(Receiver::Orchard),
DataTypecode::Unknown(typecode) => Ok(Receiver::Unknown {
typecode,
data: data.to_vec(),
}),
}
.map_err(|e| {
ParseError::InvalidEncoding(format!(
"Invalid address for typecode {:?}: {:?}",
typecode, e
))
})
}
fn typecode(&self) -> DataTypecode {
match self {
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),
}
}
@ -62,7 +67,7 @@ impl SealedItem for Receiver {
/// # use std::convert::Infallible;
/// # use std::error::Error;
/// use zcash_address::{
/// unified::{self, Container, Encoding},
/// unified::{self, Container, Encoding, Item, Revision},
/// ConversionError, TryFromRawAddress, ZcashAddress,
/// };
///
@ -90,46 +95,92 @@ 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<unified::Receiver> = ua.items();
/// let receivers: Vec<unified::Receiver> = ua.receivers();
///
/// // And we can create the UA from a list of receivers:
/// let new_ua = unified::Address::try_from_items(receivers)?;
/// 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<Receiver>);
pub struct Address {
pub(crate) revision: Revision,
pub(crate) receivers: Vec<Item<Receiver>>,
}
impl Address {
/// Returns the receiver items for this address, in order of decreasing preference.
///
/// The receiver for a wallet to send to can safely be chosen by selecting the first receiver
/// of a type that wallet supports from the result.
pub fn receivers(&self) -> Vec<Receiver> {
let mut result = self
.receivers
.iter()
.filter_map(|item| match item {
Item::Data(r) => Some(r.clone()),
Item::Metadata(_) => None,
})
.collect::<Vec<Receiver>>();
result.sort_unstable_by(Receiver::preference_order);
result
}
}
impl super::private::SealedContainer for Address {
/// The HRP for a Bech32m-encoded mainnet Unified Address.
/// The HRP for a Bech32m-encoded mainnet Revision 0 Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET: &'static str = "u";
const MAINNET_R0: &'static str = "u";
/// The HRP for a Bech32m-encoded testnet Unified Address.
/// The HRP for a Bech32m-encoded testnet Revision 0 Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET: &'static str = "utest";
const TESTNET_R0: &'static str = "utest";
/// The HRP for a Bech32m-encoded regtest Unified Address.
const REGTEST: &'static str = "uregtest";
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified Address.
const REGTEST_R0: &'static str = "uregtest";
fn from_inner(receivers: Vec<Self::Item>) -> Self {
Self(receivers)
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET_R1: &'static str = "ur";
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET_R1: &'static str = "urtest";
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified Address.
const REGTEST_R1: &'static str = "urregtest";
fn from_inner(revision: Revision, receivers: Vec<Item<Self::DataItem>>) -> Self {
Self {
revision,
receivers,
}
}
}
impl super::Encoding for Address {}
impl super::Container for Address {
type Item = Receiver;
type DataItem = Receiver;
fn items_as_parsed(&self) -> &[Receiver] {
&self.0
fn items_as_parsed(&self) -> &[Item<Receiver>] {
&self.receivers
}
fn revision(&self) -> Revision {
self.revision
}
}
@ -142,7 +193,8 @@ mod tests {
use zcash_encoding::MAX_COMPACT_SIZE;
use crate::{
kind::unified::{private::SealedContainer, Container, Encoding},
kind::unified::{private::SealedContainer, Encoding},
unified::{DataTypecode, Item, Revision, Typecode},
Network,
};
@ -153,7 +205,7 @@ mod tests {
sample::select,
};
use super::{Address, ParseError, Receiver, Typecode};
use super::{Address, ParseError, Receiver};
prop_compose! {
fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] {
@ -164,52 +216,63 @@ mod tests {
}
}
fn arb_transparent_typecode() -> impl Strategy<Value = Typecode> {
select(vec![Typecode::P2pkh, Typecode::P2sh])
fn arb_transparent_typecode() -> impl Strategy<Value = DataTypecode> {
select(vec![DataTypecode::P2pkh, DataTypecode::P2sh])
}
fn arb_shielded_typecode() -> impl Strategy<Value = Typecode> {
fn arb_shielded_typecode() -> impl Strategy<Value = DataTypecode> {
prop_oneof![
Just(Typecode::Sapling),
Just(Typecode::Orchard),
((<u32>::from(Typecode::Orchard) + 1)..MAX_COMPACT_SIZE).prop_map(Typecode::Unknown)
Just(DataTypecode::Sapling),
Just(DataTypecode::Orchard),
((<u32>::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.
fn arb_typecodes() -> impl Strategy<Value = Vec<Typecode>> {
/// A strategy to generate an arbitrary valid set of typecodes without duplication and
/// containing only one of P2sh and P2pkh transparent typecodes.
fn arb_typecodes() -> impl Strategy<Value = Vec<DataTypecode>> {
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::<Vec<_>>())
})
}
fn arb_unified_address_for_typecodes(
typecodes: Vec<Typecode>,
/// 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<DataTypecode>,
) -> impl Strategy<Value = Vec<Receiver>> {
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::<u8>(), 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::<u8>(), 32..256)
.prop_map(move |data| Receiver::Unknown { typecode, data })
.boxed(),
})
.collect::<Vec<_>>()
}
/// A strategy to generate an arbitrary Unified Address containing only receivers, without
/// additional metadata. The items in this address will be sorted in encoding order. 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.
fn arb_unified_address() -> impl Strategy<Value = Address> {
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 receivers = rs.into_iter().map(Item::Data).collect::<Vec<_>>();
receivers.sort_unstable_by(Item::encoding_order);
Address {
revision: Revision::R0,
receivers,
}
})
}
proptest! {
@ -239,7 +302,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()
))
@ -254,7 +317,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()
))
@ -279,7 +342,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(_))
);
@ -292,7 +355,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(_))
);
}
@ -301,11 +364,17 @@ 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 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[..]),
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
Address::parse_internal(Address::MAINNET_R0, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
);
}
@ -313,11 +382,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![Receiver::P2pkh([0; 20]), 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)
);
}
@ -326,11 +401,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![Receiver::Sapling([0; 43]), 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)
);
}
@ -349,7 +430,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(_))
);
}
@ -357,19 +438,22 @@ mod tests {
#[test]
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 {
typecode: 0xff,
data: vec![],
},
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!(
ua.items(),
ua.receivers(),
vec![
Receiver::Orchard([0; 43]),
Receiver::Sapling([0; 43]),

View File

@ -1,8 +1,8 @@
use std::convert::{TryFrom, TryInto};
use std::convert::TryInto;
use super::{
private::{SealedContainer, SealedItem},
Container, Encoding, ParseError, Typecode,
private::{SealedContainer, SealedDataItem},
Container, DataTypecode, Encoding, Item, ParseError, Revision,
};
/// The set of known FVKs for Unified FVKs.
@ -39,31 +39,27 @@ pub enum Fvk {
},
}
impl TryFrom<(u32, &[u8])> for Fvk {
type Error = ParseError;
fn try_from((typecode, data): (u32, &[u8])) -> Result<Self, Self::Error> {
impl SealedDataItem for Fvk {
fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
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),
}
}
@ -83,7 +79,7 @@ impl SealedItem for Fvk {
///
/// ```
/// # use std::error::Error;
/// use zcash_address::unified::{self, Container, Encoding};
/// use zcash_address::unified::{self, Container, Encoding, Item, Revision};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// # let ufvk_from_user = || "uview1cgrqnry478ckvpr0f580t6fsahp0a5mj2e9xl7hv2d2jd4ldzy449mwwk2l9yeuts85wjls6hjtghdsy5vhhvmjdw3jxl3cxhrg3vs296a3czazrycrr5cywjhwc5c3ztfyjdhmz0exvzzeyejamyp0cr9z8f9wj0953fzht0m4lenk94t70ruwgjxag2tvp63wn9ftzhtkh20gyre3w5s24f6wlgqxnjh40gd2lxe75sf3z8h5y2x0atpxcyf9t3em4h0evvsftluruqne6w4sm066sw0qe5y8qg423grple5fftxrqyy7xmqmatv7nzd7tcjadu8f7mqz4l83jsyxy4t8pkayytyk7nrp467ds85knekdkvnd7hqkfer8mnqd7pv";
@ -91,54 +87,72 @@ 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<unified::Fvk> = ufvk.items();
/// // We can obtain the pool-specific Full Viewing Keys for the UFVK.
/// let fvks: &[Item<unified::Fvk>] = ufvk.items_as_parsed();
///
/// // And we can create the UFVK from a list of FVKs:
/// let new_ufvk = unified::Ufvk::try_from_items(fvks)?;
/// 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<Fvk>);
pub struct Ufvk {
pub(crate) revision: Revision,
pub(crate) fvks: Vec<Item<Fvk>>,
}
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] {
&self.0
fn items_as_parsed(&self) -> &[Item<Fvk>] {
&self.fvks
}
fn revision(&self) -> Revision {
self.revision
}
}
impl Encoding for Ufvk {}
impl SealedContainer for Ufvk {
/// The HRP for a Bech32m-encoded mainnet Unified FVK.
/// The HRP for a Bech32m-encoded mainnet Revision 0 Unified FVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET: &'static str = "uview";
const MAINNET_R0: &'static str = "uview";
/// The HRP for a Bech32m-encoded testnet Unified FVK.
/// The HRP for a Bech32m-encoded testnet Revision 0 Unified FVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET: &'static str = "uviewtest";
const TESTNET_R0: &'static str = "uviewtest";
/// The HRP for a Bech32m-encoded regtest Unified FVK.
const REGTEST: &'static str = "uviewregtest";
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified FVK.
const REGTEST_R0: &'static str = "uviewregtest";
fn from_inner(fvks: Vec<Self::Item>) -> Self {
Self(fvks)
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified FVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET_R1: &'static str = "urview";
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified FVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET_R1: &'static str = "urviewtest";
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified FVK.
const REGTEST_R1: &'static str = "urviewregtest";
fn from_inner(revision: Revision, fvks: Vec<Item<Self::DataItem>>) -> Self {
Self { revision, fvks }
}
}
@ -148,12 +162,10 @@ mod tests {
use proptest::{array::uniform1, array::uniform32, prelude::*, sample::select};
use super::{Fvk, ParseError, Typecode, Ufvk};
use super::{Fvk, ParseError, Ufvk};
use crate::{
kind::unified::{
private::{SealedContainer, SealedItem},
Container, Encoding,
},
kind::unified::{private::SealedContainer, Encoding},
unified::{Item, Revision, Typecode},
Network,
};
@ -211,9 +223,9 @@ mod tests {
shielded in arb_shielded_fvk(),
transparent in prop::option::of(arb_transparent_fvk()),
) -> Ufvk {
let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect();
items.sort_unstable_by(Fvk::encoding_order);
Ufvk(items)
let mut fvks: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
fvks.sort_unstable_by(Item::encoding_order);
Ufvk { revision: Revision::R0, fvks }
}
}
@ -245,7 +257,7 @@ mod tests {
0xdf, 0x63, 0xe7, 0xef, 0x65, 0x6b, 0x18, 0x23, 0xf7, 0x3e, 0x35, 0x7c, 0xf3, 0xc4,
];
assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &invalid_padding[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -263,7 +275,7 @@ mod tests {
0x43, 0x8e, 0xc0, 0x3e, 0x9f, 0xf4, 0xf1, 0x80, 0x32, 0xcf, 0x2f, 0x7e, 0x7f, 0x91,
];
assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_padding[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -295,7 +307,7 @@ mod tests {
0x8c, 0x7a, 0xbf, 0x7b, 0x9a, 0xdd, 0xee, 0x18, 0x2c, 0x2d, 0xc2, 0xfc,
];
assert_matches!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_sapling_data[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -310,7 +322,7 @@ mod tests {
0x54, 0xd1, 0x9e, 0xec, 0x8b, 0xef, 0x35, 0xb8, 0x44, 0xdd, 0xab, 0x9a, 0x8d,
];
assert_matches!(
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_after_sapling_typecode[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -319,11 +331,17 @@ 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 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[..]),
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
);
}
@ -340,36 +358,8 @@ mod tests {
];
assert_eq!(
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]),
Ufvk::parse_internal(Ufvk::MAINNET_R0, &encoded[..]),
Err(ParseError::OnlyTransparent)
);
}
#[test]
fn fvks_are_sorted() {
// Construct a UFVK with fvks in an unsorted order.
let ufvk = Ufvk(vec![
Fvk::P2pkh([0; 65]),
Fvk::Orchard([0; 96]),
Fvk::Unknown {
typecode: 0xff,
data: vec![],
},
Fvk::Sapling([0; 128]),
]);
// `Ufvk::items` sorts the fvks in priority order.
assert_eq!(
ufvk.items(),
vec![
Fvk::Orchard([0; 96]),
Fvk::Sapling([0; 128]),
Fvk::P2pkh([0; 65]),
Fvk::Unknown {
typecode: 0xff,
data: vec![],
},
]
)
}
}

View File

@ -1,8 +1,8 @@
use std::convert::{TryFrom, TryInto};
use std::convert::TryInto;
use super::{
private::{SealedContainer, SealedItem},
Container, Encoding, ParseError, Typecode,
private::{SealedContainer, SealedDataItem},
Container, DataTypecode, Encoding, Item, ParseError, Revision,
};
/// The set of known IVKs for Unified IVKs.
@ -44,31 +44,27 @@ pub enum Ivk {
},
}
impl TryFrom<(u32, &[u8])> for Ivk {
type Error = ParseError;
fn try_from((typecode, data): (u32, &[u8])) -> Result<Self, Self::Error> {
impl SealedDataItem for Ivk {
fn parse(typecode: DataTypecode, data: &[u8]) -> Result<Self, ParseError> {
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),
}
}
@ -88,7 +84,7 @@ impl SealedItem for Ivk {
///
/// ```
/// # use std::error::Error;
/// use zcash_address::unified::{self, Container, Encoding};
/// use zcash_address::unified::{self, Container, Encoding, Item, Revision};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// # let uivk_from_user = || "uivk1djetqg3fws7y7qu5tekynvcdhz69gsyq07ewvppmzxdqhpfzdgmx8urnkqzv7ylz78ez43ux266pqjhecd59fzhn7wpe6zarnzh804hjtkyad25ryqla5pnc8p5wdl3phj9fczhz64zprun3ux7y9jc08567xryumuz59rjmg4uuflpjqwnq0j0tzce0x74t4tv3gfjq7nczkawxy6y7hse733ae3vw7qfjd0ss0pytvezxp42p6rrpzeh6t2zrz7zpjk0xhngcm6gwdppxs58jkx56gsfflugehf5vjlmu7vj3393gj6u37wenavtqyhdvcdeaj86s6jczl4zq";
@ -96,54 +92,72 @@ 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<unified::Ivk> = uivk.items();
/// // We can obtain the pool-specific Incoming Viewing Keys for the UIVK.
/// let ivks: &[Item<unified::Ivk>] = 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(Revision::R0, ivks.to_vec())?;
/// assert_eq!(new_uivk, uivk);
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Uivk(pub(crate) Vec<Ivk>);
pub struct Uivk {
pub(crate) revision: Revision,
pub(crate) ivks: Vec<Item<Ivk>>,
}
impl Container for Uivk {
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] {
&self.0
fn items_as_parsed(&self) -> &[Item<Ivk>] {
&self.ivks
}
fn revision(&self) -> Revision {
self.revision
}
}
impl Encoding for Uivk {}
impl SealedContainer for Uivk {
/// The HRP for a Bech32m-encoded mainnet Unified IVK.
/// The HRP for a Bech32m-encoded mainnet Revision 0 Unified IVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET: &'static str = "uivk";
const MAINNET_R0: &'static str = "uivk";
/// The HRP for a Bech32m-encoded testnet Unified IVK.
/// The HRP for a Bech32m-encoded testnet Revision 0 Unified IVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET: &'static str = "uivktest";
const TESTNET_R0: &'static str = "uivktest";
/// The HRP for a Bech32m-encoded regtest Unified IVK.
const REGTEST: &'static str = "uivkregtest";
/// The HRP for a Bech32m-encoded regtest Revision 0 Unified IVK.
const REGTEST_R0: &'static str = "uivkregtest";
fn from_inner(ivks: Vec<Self::Item>) -> Self {
Self(ivks)
/// The HRP for a Bech32m-encoded mainnet Revision 1 Unified IVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const MAINNET_R1: &'static str = "urivk";
/// The HRP for a Bech32m-encoded testnet Revision 1 Unified IVK.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
const TESTNET_R1: &'static str = "urivktest";
/// The HRP for a Bech32m-encoded regtest Revision 1 Unified IVK.
const REGTEST_R1: &'static str = "urivkregtest";
fn from_inner(revision: Revision, ivks: Vec<Item<Self::DataItem>>) -> Self {
Self { revision, ivks }
}
}
@ -157,12 +171,10 @@ mod tests {
sample::select,
};
use super::{Ivk, ParseError, Typecode, Uivk};
use super::{Ivk, ParseError, Uivk};
use crate::{
kind::unified::{
private::{SealedContainer, SealedItem},
Container, Encoding,
},
kind::unified::{private::SealedContainer, Encoding},
unified::{Item, Revision, Typecode},
Network,
};
@ -204,9 +216,9 @@ mod tests {
shielded in arb_shielded_ivk(),
transparent in prop::option::of(arb_transparent_ivk()),
) -> Uivk {
let mut items: Vec<_> = transparent.into_iter().chain(shielded).collect();
items.sort_unstable_by(Ivk::encoding_order);
Uivk(items)
let mut ivks: Vec<_> = transparent.into_iter().chain(shielded).map(Item::Data).collect();
ivks.sort_unstable_by(Item::encoding_order);
Uivk { revision: Revision::R0, ivks }
}
}
@ -236,7 +248,7 @@ mod tests {
0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15,
];
assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &invalid_padding[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -252,7 +264,7 @@ mod tests {
0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47,
];
assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_padding[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -280,7 +292,7 @@ mod tests {
0xf5, 0xd5, 0x8a, 0xb5, 0x1a,
];
assert_matches!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_sapling_data[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -293,7 +305,7 @@ mod tests {
0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74,
];
assert_matches!(
Uivk::parse_internal(Uivk::MAINNET, &truncated_after_sapling_typecode[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -301,11 +313,17 @@ 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 {
revision: Revision::R0,
ivks: vec![
Item::Data(Ivk::Sapling([1; 64])),
Item::Data(Ivk::Sapling([2; 64])),
],
};
let encoded = uivk.encode(&Network::Main);
assert_eq!(
Uivk::decode(&encoded),
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
Err(ParseError::DuplicateTypecode(Typecode::SAPLING))
);
}
@ -322,36 +340,8 @@ mod tests {
];
assert_eq!(
Uivk::parse_internal(Uivk::MAINNET, &encoded[..]),
Uivk::parse_internal(Uivk::MAINNET_R0, &encoded[..]),
Err(ParseError::OnlyTransparent)
);
}
#[test]
fn ivks_are_sorted() {
// Construct a UIVK with ivks in an unsorted order.
let uivk = Uivk(vec![
Ivk::P2pkh([0; 65]),
Ivk::Orchard([0; 64]),
Ivk::Unknown {
typecode: 0xff,
data: vec![],
},
Ivk::Sapling([0; 64]),
]);
// `Uivk::items` sorts the ivks in priority order.
assert_eq!(
uivk.items(),
vec![
Ivk::Orchard([0; 64]),
Ivk::Sapling([0; 64]),
Ivk::P2pkh([0; 65]),
Ivk::Unknown {
typecode: 0xff,
data: vec![],
},
]
)
}
}

View File

@ -8,6 +8,7 @@ use {
unified::{
self,
address::{test_vectors::TEST_VECTORS, Receiver},
Item, Revision,
},
Network, ToAddress, ZcashAddress,
},
@ -36,9 +37,16 @@ fn unified() {
data: data.to_vec(),
})
}))
.map(Item::Data)
.collect();
let expected_addr = ZcashAddress::from_unified(Network::Main, unified::Address(receivers));
let expected_addr = ZcashAddress::from_unified(
Network::Main,
unified::Address {
revision: Revision::R0,
receivers,
},
);
// Test parsing
let addr: ZcashAddress = tv.unified_addr.parse().unwrap();

View File

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

View File

@ -855,7 +855,7 @@ where
.expect("Payment step references are checked at construction")
.recipient_address
{
Address::Transparent(t) => Some(t),
Address::Transparent(t) => Some(t.as_ref()),
Address::Unified(uaddr) => uaddr.transparent(),
_ => None,
}
@ -1019,11 +1019,15 @@ where
.map_or_else(MemoBytes::empty, |m| m.clone());
builder.add_sapling_output(
sapling_external_ovk,
*addr,
**addr,
payment.amount,
memo.clone(),
)?;
sapling_output_meta.push((Recipient::Sapling(*addr), payment.amount, Some(memo)));
sapling_output_meta.push((
Recipient::Sapling(addr.clone()),
payment.amount,
Some(memo),
));
}
Address::Transparent(to) => {
if payment.memo.is_some() {
@ -1167,7 +1171,12 @@ where
.map(|(index, _)| index)
.expect("An output should exist in the transaction for each transparent payment.");
SentTransactionOutput::from_parts(output_index, Recipient::Transparent(*addr), value, None)
SentTransactionOutput::from_parts(
output_index,
Recipient::Transparent(addr.clone()),
value,
None,
)
});
let mut outputs = vec![];

View File

@ -380,7 +380,7 @@ where
}
return Err(InputSelectorError::Selection(
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())),
GreedyInputSelectorError::UnsupportedAddress(addr.clone()),
));
}
}

View File

@ -68,9 +68,9 @@ impl NoteId {
/// output.
#[derive(Debug, Clone)]
pub enum Recipient<AccountId, N> {
Transparent(TransparentAddress),
Sapling(sapling::PaymentAddress),
Unified(UnifiedAddress, PoolType),
Transparent(Box<TransparentAddress>),
Sapling(Box<sapling::PaymentAddress>),
Unified(Box<UnifiedAddress>, PoolType),
InternalAccount {
receiving_account: AccountId,
external_address: Option<Address>,
@ -79,6 +79,22 @@ pub enum Recipient<AccountId, N> {
}
impl<AccountId, N> Recipient<AccountId, N> {
/// Constructs a [`Recipient`] from a [`TransparentAddress`].
pub fn transparent(addr: TransparentAddress) -> Self {
Recipient::Transparent(Box::new(addr))
}
/// Constructs a [`Recipient`] from a [`sapling::PaymentAddress`].
pub fn sapling(addr: sapling::PaymentAddress) -> Self {
Recipient::Sapling(Box::new(addr))
}
/// Constructs a [`Recipient`] from a [`UnifiedAddress`] and the pool type of the selected
/// receiver.
pub fn unified(addr: UnifiedAddress, pool_type: PoolType) -> Self {
Recipient::Unified(Box::new(addr), pool_type)
}
pub fn map_internal_account_note<B, F: FnOnce(N) -> B>(self, f: F) -> Recipient<AccountId, B> {
match self {
Recipient::Transparent(t) => Recipient::Transparent(t),

View File

@ -752,6 +752,7 @@ pub mod testing {
use crate::address::Address;
use super::{MemoBytes, Payment, TransactionRequest};
pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*";
#[cfg(feature = "transparent-inputs")]
@ -760,7 +761,7 @@ pub mod testing {
const TRANSPARENT_INPUTS_ENABLED: bool = false;
pub(crate) const UA_REQUEST: UnifiedAddressRequest =
UnifiedAddressRequest::unsafe_new(false, true, TRANSPARENT_INPUTS_ENABLED);
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, TRANSPARENT_INPUTS_ENABLED);
prop_compose! {
pub fn arb_valid_memo()(bytes in vec(any::<u8>(), 0..512)) -> MemoBytes {
@ -835,7 +836,7 @@ mod tests {
memo::Memo,
transaction::components::amount::{testing::arb_nonnegative_amount, NonNegativeAmount},
};
use zcash_protocol::consensus::{NetworkConstants, NetworkType, TEST_NETWORK};
use zcash_protocol::consensus::{NetworkConstants, TEST_NETWORK};
#[cfg(feature = "local-consensus")]
use zcash_primitives::{local_consensus::LocalNetwork, BlockHeight};
@ -883,7 +884,7 @@ mod tests {
let expected = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
recipient_address: Address::from(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::const_from_u64(376876902796286),
memo: None,
label: None,
@ -904,7 +905,7 @@ mod tests {
let expected = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
recipient_address: Address::from(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::ZERO,
memo: None,
label: None,
@ -922,7 +923,7 @@ mod tests {
let req = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
recipient_address: Address::from(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::ZERO,
memo: None,
label: None,

View File

@ -133,7 +133,7 @@ pub(crate) const UA_TRANSPARENT: bool = false;
pub(crate) const UA_TRANSPARENT: bool = true;
pub(crate) const DEFAULT_UA_REQUEST: UnifiedAddressRequest =
UnifiedAddressRequest::unsafe_new(UA_ORCHARD, true, UA_TRANSPARENT);
UnifiedAddressRequest::unsafe_new_without_expiry(UA_ORCHARD, true, UA_TRANSPARENT);
/// The ID type for accounts.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
@ -1064,7 +1064,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
match output.transfer_type() {
TransferType::Outgoing => {
//TODO: Recover the UA, if possible.
let recipient = Recipient::Sapling(output.note().recipient());
let recipient = Recipient::sapling(output.note().recipient());
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
@ -1103,7 +1103,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
let recipient = Recipient::InternalAccount {
receiving_account: *output.account(),
// TODO: recover the actual UA, if possible
external_address: Some(Address::Sapling(output.note().recipient())),
external_address: Some(Address::from(output.note().recipient())),
note: Note::Sapling(output.note().clone()),
};
@ -1127,13 +1127,12 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
match output.transfer_type() {
TransferType::Outgoing => {
// TODO: Recover the actual UA, if possible.
let recipient = Recipient::Unified(
let recipient = Recipient::unified(
UnifiedAddress::from_receivers(
Some(output.note().recipient()),
None,
None,
)
.expect("UA has an Orchard receiver by construction."),
),
PoolType::Shielded(ShieldedProtocol::Orchard),
);
@ -1176,12 +1175,13 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
let recipient = Recipient::InternalAccount {
receiving_account: *output.account(),
// TODO: recover the actual UA, if possible
external_address: Some(Address::Unified(
external_address: Some(Address::from(
UnifiedAddress::from_receivers(
Some(output.note().recipient()),
None,
None,
).expect("UA has an Orchard receiver by construction."))),
Some(output.note().recipient()),
None,
None,
)
)),
note: Note::Orchard(*output.note()),
};
@ -1246,7 +1246,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
account_id,
tx_ref,
output_index,
&Recipient::Transparent(address),
&Recipient::transparent(address),
txout.value,
None,
)?;

View File

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

View File

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

View File

@ -551,7 +551,7 @@ pub(crate) fn get_current_address<P: consensus::Parameters>(
SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
})
.and_then(|addr| match addr {
Address::Unified(ua) => Ok(ua),
Address::Unified(ua) => Ok(*ua),
_ => Err(SqliteClientError::CorruptedData(format!(
"Addresses table contains {} which is not a unified address",
addr_str,
@ -679,7 +679,7 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
conn: &rusqlite::Connection,
account_id: AccountId,
) -> Result<Option<(TransparentAddress, NonHardenedChildIndex)>, SqliteClientError> {
use zcash_address::unified::Container;
use zcash_address::unified::{Container, Item};
use zcash_primitives::legacy::keys::ExternalIvk;
// Get the UIVK for the account.
@ -701,9 +701,9 @@ pub(crate) fn get_legacy_transparent_address<P: consensus::Parameters>(
}
// Derive the default transparent address (if it wasn't already part of a derived UA).
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()));
}
}

View File

@ -1419,8 +1419,9 @@ mod tests {
// Unified addresses at the time of the addition of migrations did not contain an
// Orchard component.
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT);
let address_str = Address::Unified(
let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT);
let address_str = Address::from(
ufvk.default_address(ua_request)
.expect("A valid default address exists for the UFVK")
.0,
@ -1439,7 +1440,7 @@ mod tests {
// add a transparent "sent note"
#[cfg(feature = "transparent-inputs")]
{
let taddr = Address::Transparent(
let taddr = Address::from(
*ufvk
.default_address(ua_request)
.expect("A valid default address exists for the UFVK")
@ -1546,7 +1547,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(false, true, true);
let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true);
db_data
.get_next_available_address(account_id, ua_request)
.unwrap()

View File

@ -443,12 +443,12 @@ 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(UnifiedAddressRequest::unsafe_new(
.default_address(UnifiedAddressRequest::unsafe_new_without_expiry(
false,
true,
UA_TRANSPARENT,
))
.expect("A valid default address exists for the UFVK");
.unwrap();
let taddr = ufvk
.transparent()
.and_then(|k| {

View File

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

View File

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

View File

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

View File

@ -463,7 +463,6 @@ pub(crate) mod tests {
None,
None,
)
.unwrap()
.into()
}
@ -545,9 +544,7 @@ pub(crate) mod tests {
return Ok(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"),
)
}));

View File

@ -9,7 +9,12 @@ and this library adheres to Rust's notion of
## [0.2.0] - 2024-03-25
### Added
- `zcash_keys::address::Address::has_receiver`
- `zcash_keys::address`:
- `Address::has_receiver`
- `UnifiedAddress::{
new, expiry_height, expiry_time,
unknown_data, unknown_metadata
}`
- `impl Display for zcash_keys::keys::AddressGenerationError`
- `impl std::error::Error for zcash_keys::keys::AddressGenerationError`
- `impl From<hdwallet::error::Error> for zcash_keys::keys::DerivationError`
@ -28,10 +33,15 @@ and this library adheres to Rust's notion of
must be enabled for the `keys` module to be accessible.
- Updated to `zcash_primitives-0.15.0`
### Changed
- `zcash_keys::address::Address` variants now `Box` their contents to
avoid large discrepancies in enum variant sizing.
### Removed
- `UnifiedFullViewingKey::new` has been placed behind the `test-dependencies`
feature flag. UFVKs should only be produced by derivation from the USK, or
parsed from their string representation.
- `zcash_keys::address::UnifiedAddress::from_receivers`
### Fixed
- `UnifiedFullViewingKey::find_address` can now find an address for a diversifier
@ -65,28 +75,47 @@ The entries below are relative to the `zcash_client_backend` crate as of
- `UnifiedAddressRequest`
- A new `orchard` feature flag has been added to make it possible to
build client code without `orchard` dependendencies.
- 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<UnifiedAddress, AddressGenerationError>` instead of
`Option<UnifiedAddress>` 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`

View File

@ -1,15 +1,17 @@
//! Structs for handling supported address types.
use zcash_address::{
unified::{self, Container, Encoding, Typecode},
unified::{self, Container, DataTypecode, Encoding, Item, Revision, Typecode},
ConversionError, ToAddress, TryFromRawAddress, ZcashAddress,
};
use zcash_primitives::legacy::TransparentAddress;
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)]
@ -19,7 +21,10 @@ pub struct UnifiedAddress {
#[cfg(feature = "sapling")]
sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>,
unknown: Vec<(u32, Vec<u8>)>,
unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
}
impl TryFrom<unified::Address> for UnifiedAddress {
@ -31,14 +36,16 @@ impl TryFrom<unified::Address> for UnifiedAddress {
#[cfg(feature = "sapling")]
let mut sapling = None;
let mut transparent = None;
let mut unknown: Vec<(u32, Vec<u8>)> = 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(
@ -48,11 +55,11 @@ impl TryFrom<unified::Address> 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(
@ -62,20 +69,26 @@ impl TryFrom<unified::Address> 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()));
}
}
}
@ -86,7 +99,10 @@ impl TryFrom<unified::Address> for UnifiedAddress {
#[cfg(feature = "sapling")]
sapling,
transparent,
unknown,
unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
})
}
}
@ -94,36 +110,43 @@ impl TryFrom<unified::Address> 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.
#[cfg(any(test, feature = "test-dependencies"))]
pub fn from_receivers(
#[cfg(feature = "orchard")] orchard: Option<orchard::Address>,
#[cfg(feature = "sapling")] sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>,
// TODO: Add handling for address metadata items.
) -> Option<Self> {
#[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<orchard::Address>,
#[cfg(feature = "sapling")] sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
) -> Self {
Self {
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "sapling")]
sapling,
transparent,
unknown_data: vec![],
expiry_height,
expiry_time,
unknown_metadata: vec![],
}
}
@ -168,22 +191,60 @@ impl UnifiedAddress {
self.transparent.as_ref()
}
/// Returns the set of unknown receivers of the unified address.
pub fn unknown(&self) -> &[(u32, Vec<u8>)] {
&self.unknown
/// Returns any unknown data items parsed from the encoded form of the address.
pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
self.unknown_data.as_ref()
}
/// Returns the expiration height for this address.
pub fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
/// Sets the expiry height of this address.
pub fn set_expiry_height(&mut self, height: BlockHeight) {
self.expiry_height = Some(height);
}
/// Removes the expiry height from this address.
pub fn unset_expiry_height(&mut self) {
self.expiry_height = None;
}
/// Returns the expiration time for this address as a Unix Epoch Time.
pub fn expiry_time(&self) -> Option<u64> {
self.expiry_time
}
/// Sets the expiry time of this address.
pub fn set_expiry_time(&mut self, time: u64) {
self.expiry_time = Some(time);
}
/// Removes the expiry time from this address.
pub fn unset_expiry_time(&mut self) {
self.expiry_time = None;
}
/// Returns any unknown metadata items parsed from the encoded form of the address.
///
/// Unknown metadata items are guaranteed by construction and parsing to not have keys in the
/// MUST-understand metadata typecode range.
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
self.unknown_metadata.as_ref()
}
fn to_address(&self, net: NetworkType) -> ZcashAddress {
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())
@ -191,20 +252,46 @@ 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(
if self.expiry_height().is_some()
|| self.expiry_time().is_some()
|| !(self.has_orchard() || self.has_sapling())
{
Revision::R1
} else {
Revision::R0
},
data_items
.map(Item::Data)
.chain(meta_items.map(Item::Metadata))
.collect(),
)
.expect("UnifiedAddress should only be constructed safely");
ZcashAddress::from_unified(net, ua)
}
@ -217,17 +304,17 @@ impl UnifiedAddress {
pub fn receiver_types(&self) -> Vec<Typecode> {
let result = std::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()
}
@ -237,27 +324,27 @@ impl UnifiedAddress {
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Address {
#[cfg(feature = "sapling")]
Sapling(PaymentAddress),
Transparent(TransparentAddress),
Unified(UnifiedAddress),
Sapling(Box<PaymentAddress>),
Transparent(Box<TransparentAddress>),
Unified(Box<UnifiedAddress>),
}
#[cfg(feature = "sapling")]
impl From<PaymentAddress> for Address {
fn from(addr: PaymentAddress) -> Self {
Address::Sapling(addr)
Address::Sapling(Box::new(addr))
}
}
impl From<TransparentAddress> for Address {
fn from(addr: TransparentAddress) -> Self {
Address::Transparent(addr)
Address::Transparent(Box::new(addr))
}
}
impl From<UnifiedAddress> for Address {
fn from(addr: UnifiedAddress) -> Self {
Address::Unified(addr)
Address::Unified(Box::new(addr))
}
}
@ -301,12 +388,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),
@ -366,23 +453,23 @@ pub mod testing {
params: Network,
request: UnifiedAddressRequest,
) -> impl Strategy<Value = UnifiedAddress> {
arb_unified_spending_key(params).prop_map(move |k| k.default_address(request).0)
arb_unified_spending_key(params).prop_map(move |k| k.default_address(request).unwrap().0)
}
#[cfg(feature = "sapling")]
pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy<Value = Address> {
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),
]
}
#[cfg(not(feature = "sapling"))]
pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy<Value = Address> {
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),
];
}
}
@ -421,15 +508,15 @@ 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 = Address::from(ua);
let addr_str = addr.encode(&MAIN_NETWORK);
assert_eq!(Address::decode(&MAIN_NETWORK, &addr_str), Some(addr));
}
@ -438,7 +525,7 @@ mod tests {
#[cfg(not(any(feature = "orchard", feature = "sapling")))]
fn ua_round_trip() {
let transparent = None;
assert_eq!(UnifiedAddress::from_receivers(transparent), None)
assert_eq!(UnifiedAddress::new_internal(transparent, None, None), None)
}
#[test]

View File

@ -3,8 +3,8 @@ use std::{
error,
fmt::{self, Display},
};
use zcash_address::unified::{self, Container, Encoding, Typecode, Ufvk, Uivk};
use zcash_address::unified::{self, Container, Encoding, Item, MetadataItem, Revision, Typecode};
use zcash_primitives::consensus::BlockHeight;
use zcash_protocol::consensus;
use zip32::{AccountId, DiversifierIndex};
@ -258,7 +258,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![],
}
}
@ -298,7 +301,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();
@ -308,7 +311,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();
@ -318,7 +321,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();
@ -334,6 +337,8 @@ impl UnifiedSpendingKey {
#[allow(clippy::unnecessary_unwrap)]
#[cfg(feature = "unstable")]
pub fn from_bytes(era: Era, encoded: &[u8]) -> Result<Self, DecodingError> {
use zcash_address::unified::DataTypecode;
let mut source = std::io::Cursor::new(encoded);
let decoded_era = source
.read_u32::<LittleEndian>()
@ -353,21 +358,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")]
{
@ -375,43 +382,43 @@ impl UnifiedSpendingKey {
Option::<orchard::keys::SpendingKey>::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 != 64 {
return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len));
return Err(DecodingError::LengthMismatch(Typecode::P2PKH, len));
}
let mut key = [0u8; 64];
source
.read_exact(&mut key)
.map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?;
.map_err(|_| DecodingError::InsufficientData(Typecode::P2PKH))?;
#[cfg(feature = "transparent-inputs")]
{
transparent = Some(
legacy::AccountPrivKey::from_bytes(&key)
.ok_or(DecodingError::KeyDataInvalid(Typecode::P2pkh))?,
.ok_or(DecodingError::KeyDataInvalid(Typecode::P2PKH))?,
);
}
}
@ -444,7 +451,7 @@ impl UnifiedSpendingKey {
#[cfg(feature = "orchard")]
orchard.unwrap(),
)
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2pkh));
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::P2PKH));
}
}
}
@ -453,10 +460,8 @@ impl UnifiedSpendingKey {
pub fn default_address(
&self,
request: UnifiedAddressRequest,
) -> (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(
@ -549,22 +554,28 @@ pub struct UnifiedAddressRequest {
has_orchard: bool,
has_sapling: bool,
has_p2pkh: bool,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
}
impl UnifiedAddressRequest {
/// Construct a new unified address request from its constituent parts.
///
/// Returns `None` if the resulting unified address would not include at least one shielded receiver.
pub fn new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Option<Self> {
let has_shielded_receiver = has_orchard || has_sapling;
if !has_shielded_receiver {
/// Construct a new unified address request from its constituent parts
pub fn new(
has_orchard: bool,
has_sapling: bool,
has_p2pkh: bool,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
) -> Option<Self> {
if !(has_sapling || has_orchard || has_p2pkh) {
None
} else {
Some(Self {
has_orchard,
has_sapling,
has_p2pkh,
expiry_height,
expiry_time,
})
}
}
@ -584,21 +595,27 @@ impl UnifiedAddressRequest {
#[cfg(feature = "transparent-inputs")]
let _has_p2pkh = true;
Self::new(_has_orchard, _has_sapling, _has_p2pkh)
Self::new(_has_orchard, _has_sapling, _has_p2pkh, None, None)
}
/// Construct a new unified address request from its constituent parts.
///
/// Panics: at least one of `has_orchard` or `has_sapling` must be `true`.
pub const fn unsafe_new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Self {
if !(has_orchard || has_sapling) {
panic!("At least one shielded receiver must be requested.")
/// Panics: at least one of `has_orchard`, `has_sapling`, or `has_p2pkh` must be `true`.
pub const fn unsafe_new_without_expiry(
has_orchard: bool,
has_sapling: bool,
has_p2pkh: bool,
) -> Self {
if !(has_orchard || has_sapling || has_p2pkh) {
panic!("At least one receiver must be requested.")
}
Self {
has_orchard,
has_sapling,
has_p2pkh,
expiry_height: None,
expiry_time: None,
}
}
}
@ -619,7 +636,10 @@ pub struct UnifiedFullViewingKey {
sapling: Option<sapling::DiversifiableFullViewingKey>,
#[cfg(feature = "orchard")]
orchard: Option<orchard::keys::FullViewingKey>,
unknown: Vec<(u32, Vec<u8>)>,
unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
}
impl UnifiedFullViewingKey {
@ -645,6 +665,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![],
)
}
@ -654,7 +677,10 @@ impl UnifiedFullViewingKey {
#[cfg(feature = "transparent-inputs")] transparent: Option<legacy::AccountPubKey>,
#[cfg(feature = "sapling")] sapling: Option<sapling::DiversifiableFullViewingKey>,
#[cfg(feature = "orchard")] orchard: Option<orchard::keys::FullViewingKey>,
unknown: Vec<(u32, Vec<u8>)>,
unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
) -> Result<UnifiedFullViewingKey, DerivationError> {
// Verify that IVK derivation succeeds; we don't want to construct a UFVK
// that can't derive transparent addresses.
@ -671,7 +697,10 @@ impl UnifiedFullViewingKey {
sapling,
#[cfg(feature = "orchard")]
orchard,
unknown,
unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
})
}
@ -679,7 +708,8 @@ impl UnifiedFullViewingKey {
///
/// [ZIP 316]: https://zips.z.cash/zip-0316
pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> {
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!(
@ -694,64 +724,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<Self, DecodingError> {
pub fn parse(ufvk: &zcash_address::unified::Ufvk) -> Result<Self, DecodingError> {
#[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) => legacy::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::<Result<_, _>>()?;
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(
legacy::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")]
@ -760,9 +797,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.
@ -771,37 +811,64 @@ impl UnifiedFullViewingKey {
}
/// Returns the string encoding of this `UnifiedFullViewingKey` for the given network.
fn to_ufvk(&self) -> Ufvk {
let items = std::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(
if self.expiry_height().is_some()
|| self.expiry_time().is_some()
|| !(self.sapling.is_some() || self.orchard.is_some())
{
Revision::R1
} else {
Revision::R0
},
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.
@ -816,7 +883,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![],
}
}
@ -839,6 +910,26 @@ 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<u8>)] {
self.unknown_data.as_ref()
}
/// Returns the expiration height for this key.
pub fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
/// Returns the expiration time for this key.
pub fn expiry_time(&self) -> Option<u64> {
self.expiry_time
}
/// Returns any unknown metadata items parsed from the encoded form of the key.
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
self.unknown_metadata.as_ref()
}
/// Attempts to derive the Unified Address for the given diversifier index and
/// receiver types.
///
@ -857,10 +948,9 @@ impl UnifiedFullViewingKey {
///
/// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features
/// required to satisfy the unified address request are not properly enabled.
#[allow(unused_mut)]
pub fn find_address(
&self,
mut j: DiversifierIndex,
j: DiversifierIndex,
request: UnifiedAddressRequest,
) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
self.to_unified_incoming_viewing_key()
@ -889,8 +979,10 @@ pub struct UnifiedIncomingViewingKey {
sapling: Option<::sapling::zip32::IncomingViewingKey>,
#[cfg(feature = "orchard")]
orchard: Option<orchard::keys::IncomingViewingKey>,
/// Stores the unrecognized elements of the unified encoding.
unknown: Vec<(u32, Vec<u8>)>,
unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
}
impl UnifiedIncomingViewingKey {
@ -906,7 +998,10 @@ impl UnifiedIncomingViewingKey {
>,
#[cfg(feature = "sapling")] sapling: Option<::sapling::zip32::IncomingViewingKey>,
#[cfg(feature = "orchard")] orchard: Option<orchard::keys::IncomingViewingKey>,
// TODO: Implement construction of UIVKs with metadata items.
unknown_data: Vec<(u32, Vec<u8>)>,
expiry_height: Option<BlockHeight>,
expiry_time: Option<u64>,
unknown_metadata: Vec<(u32, Vec<u8>)>,
) -> UnifiedIncomingViewingKey {
UnifiedIncomingViewingKey {
#[cfg(feature = "transparent-inputs")]
@ -917,7 +1012,10 @@ impl UnifiedIncomingViewingKey {
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,
}
}
@ -925,7 +1023,7 @@ impl UnifiedIncomingViewingKey {
///
/// [ZIP 316]: https://zips.z.cash/zip-0316
pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> {
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!(
@ -934,62 +1032,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<Self, DecodingError> {
fn parse(uivk: &zcash_address::unified::Uivk) -> Result<Self, DecodingError> {
#[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(
legacy::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()));
}
unified::Ivk::Unknown { typecode, data } => {
unknown.push((*typecode, data.clone()));
Item::Data(unified::Ivk::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()));
}
}
}
@ -1001,7 +1110,10 @@ impl UnifiedIncomingViewingKey {
sapling,
#[cfg(feature = "orchard")]
orchard,
unknown,
unknown_data,
expiry_height,
expiry_time,
unknown_metadata,
})
}
@ -1011,37 +1123,61 @@ impl UnifiedIncomingViewingKey {
}
/// Converts this unified incoming viewing key to a unified encoding.
fn render(&self) -> Uivk {
let items = std::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(
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))
.collect(),
)
.expect("UnifiedIncomingViewingKey should only be constructed safely.")
}
/// Returns the Transparent external IVK, if present.
@ -1062,6 +1198,26 @@ impl UnifiedIncomingViewingKey {
&self.orchard
}
/// Returns any unknown data items parsed from the encoded form of the key.
pub fn unknown_data(&self) -> &[(u32, Vec<u8>)] {
self.unknown_data.as_ref()
}
/// Returns the expiration height for this key.
pub fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
/// Returns the expiration time for this key.
pub fn expiry_time(&self) -> Option<u64> {
self.expiry_time
}
/// Returns any unknown metadata items parsed from the encoded form of the key.
pub fn unknown_metadata(&self) -> &[(u32, Vec<u8>)] {
self.unknown_metadata.as_ref()
}
/// Attempts to derive the Unified Address for the given diversifier index and
/// receiver types.
///
@ -1076,7 +1232,7 @@ impl UnifiedIncomingViewingKey {
if request.has_orchard {
#[cfg(not(feature = "orchard"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::Orchard,
Typecode::ORCHARD,
));
#[cfg(feature = "orchard")]
@ -1084,7 +1240,7 @@ impl UnifiedIncomingViewingKey {
let orchard_j = orchard::keys::DiversifierIndex::from(*_j.as_bytes());
orchard = Some(oivk.address_at(orchard_j))
} else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::Orchard));
return Err(AddressGenerationError::KeyNotAvailable(Typecode::ORCHARD));
}
}
@ -1093,7 +1249,7 @@ impl UnifiedIncomingViewingKey {
if request.has_sapling {
#[cfg(not(feature = "sapling"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::Sapling,
Typecode::SAPLING,
));
#[cfg(feature = "sapling")]
@ -1106,7 +1262,7 @@ impl UnifiedIncomingViewingKey {
.ok_or(AddressGenerationError::InvalidSaplingDiversifierIndex(_j))?,
);
} else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::Sapling));
return Err(AddressGenerationError::KeyNotAvailable(Typecode::SAPLING));
}
}
@ -1115,7 +1271,7 @@ impl UnifiedIncomingViewingKey {
if request.has_p2pkh {
#[cfg(not(feature = "transparent-inputs"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::P2pkh,
Typecode::P2PKH,
));
#[cfg(feature = "transparent-inputs")]
@ -1131,20 +1287,21 @@ impl UnifiedIncomingViewingKey {
.map_err(|_| AddressGenerationError::InvalidTransparentChildIndex(_j))?,
);
} else {
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)
std::cmp::min(self.expiry_height, request.expiry_height),
std::cmp::min(self.expiry_time, request.expiry_time),
))
}
/// Searches the diversifier space starting at diversifier index `j` for one which will
@ -1153,7 +1310,6 @@ impl UnifiedIncomingViewingKey {
///
/// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features
/// required to satisfy the unified address request are not properly enabled.
#[allow(unused_mut)]
pub fn find_address(
&self,
mut j: DiversifierIndex,
@ -1181,6 +1337,8 @@ impl UnifiedIncomingViewingKey {
Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => {
if j.increment().is_err() {
return Err(AddressGenerationError::DiversifierSpaceExhausted);
} else {
continue;
}
}
Err(other) => {
@ -1236,7 +1394,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")]
@ -1372,13 +1530,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(
@ -1386,13 +1544,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(
@ -1400,13 +1558,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]
@ -1435,7 +1593,10 @@ mod tests {
}
let ua = ufvk
.address(d_idx, UnifiedAddressRequest::unsafe_new(false, true, true))
.address(
d_idx,
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true),
)
.unwrap_or_else(|err| {
panic!(
"unified address generation failed for account {}: {:?}",
@ -1497,6 +1658,10 @@ mod tests {
sapling,
#[cfg(feature = "orchard")]
orchard,
vec![],
None,
None,
vec![],
);
let encoded = uivk.render().encode(&NetworkType::Main);
@ -1517,7 +1682,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);
@ -1538,7 +1703,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()),
@ -1551,13 +1716,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(
@ -1565,13 +1730,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(
@ -1579,13 +1744,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]
@ -1616,7 +1781,10 @@ mod tests {
}
let ua = uivk
.address(d_idx, UnifiedAddressRequest::unsafe_new(false, true, true))
.address(
d_idx,
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, true),
)
.unwrap_or_else(|err| {
panic!(
"unified address generation failed for account {}: {:?}",