Skip to content

Commit

Permalink
zcash_address: Add support for ZIP 316, Revision 1
Browse files Browse the repository at this point in the history
  • Loading branch information
nuttycom committed Feb 16, 2024
1 parent 776ec12 commit 2833744
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 181 deletions.
9 changes: 9 additions & 0 deletions components/zcash_address/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@ and this library adheres to Rust's notion of
- `MetadataTypecode`
- `Item`
- `MetadataItem`
- `Revision`
- `Container::revision`
- `Address::revision`
- `Ufvk::revision`
- `Uivk::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.
- `Address::try_from_items` now takes an additional `Revision` argument.
- `Ufvk::try_from_items` now takes an additional `Revision` argument.
- `Uivk::try_from_items` now takes an additional `Revision` argument.

### Removed

Expand Down
17 changes: 13 additions & 4 deletions components/zcash_address/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ mod tests {
use super::*;
use crate::{
kind::unified,
unified::{Item, Receiver},
unified::{Item, Receiver, Revision},
};

fn encoding(encoded: &str, decoded: ZcashAddress) {
Expand Down Expand Up @@ -215,21 +215,30 @@ mod tests {
"u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl",
ZcashAddress {
net: Network::Main,
kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);
encoding(
"utest10c5kutapazdnf8ztl3pu43nkfsjx89fy3uuff8tsmxm6s86j37pe7uz94z5jhkl49pqe8yz75rlsaygexk6jpaxwx0esjr8wm5ut7d5s",
ZcashAddress {
net: Network::Test,
kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);
encoding(
"uregtest15xk7vj4grjkay6mnfl93dhsflc2yeunhxwdh38rul0rq3dfhzzxgm5szjuvtqdha4t4p2q02ks0jgzrhjkrav70z9xlvq0plpcjkd5z3",
ZcashAddress {
net: Network::Regtest,
kind: AddressKind::Unified(unified::Address(vec![Item::Data(Receiver::Sapling([0; 43]))])),
kind: AddressKind::Unified(unified::Address {
revision: Revision::R0,
receivers: vec![Item::Data(Receiver::Sapling([0; 43]))]
}),
},
);

Expand Down
118 changes: 86 additions & 32 deletions components/zcash_address/src/kind/unified.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ impl TryFrom<u32> for MetadataTypecode {

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..=0xEF => Ok(MetadataTypecode::MustUnderstand(typecode)),
0xF0..=0xFC => Ok(MetadataTypecode::Unknown(typecode)),
0xE2..=0xFC => Ok(MetadataTypecode::MustUnderstand(typecode)),
_ => Err(()),

Check warning on line 127 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L121-L127

Added lines #L121 - L127 were not covered by tests
}
}
Expand Down Expand Up @@ -212,9 +212,13 @@ pub enum MetadataItem {

impl MetadataItem {
/// Parse a metadata item for the specified metadata typecode from the provided bytes.
pub fn parse(typecode: MetadataTypecode, data: &[u8]) -> Result<Self, ParseError> {
match typecode {
MetadataTypecode::ExpiryHeight => data
pub fn parse(
revision: Revision,
typecode: MetadataTypecode,
data: &[u8],
) -> Result<Self, ParseError> {
match (revision, typecode) {
(Revision::R1, MetadataTypecode::ExpiryHeight) => data

Check warning on line 221 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L220-L221

Added lines #L220 - L221 were not covered by tests
.try_into()
.map(u32::from_le_bytes)
.map(MetadataItem::ExpiryHeight)
Expand All @@ -223,7 +227,7 @@ impl MetadataItem {
"Expiry height must be a 32-bit little-endian value.".to_string(),

Check warning on line 227 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L223-L227

Added lines #L223 - L227 were not covered by tests
)
}),
MetadataTypecode::ExpiryTime => data
(Revision::R1, MetadataTypecode::ExpiryTime) => data

Check warning on line 230 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L230

Added line #L230 was not covered by tests
.try_into()
.map(u64::from_le_bytes)
.map(MetadataItem::ExpiryTime)
Expand All @@ -232,11 +236,18 @@ impl MetadataItem {
"Expiry time must be a 64-bit little-endian value.".to_string(),

Check warning on line 236 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L232-L236

Added lines #L232 - L236 were not covered by tests
)
}),
MetadataTypecode::MustUnderstand(tc) => Err(ParseError::NotUnderstood(tc)),
MetadataTypecode::Unknown(typecode) => Ok(MetadataItem::Unknown {
(Revision::R1, MetadataTypecode::Unknown(typecode)) => Ok(MetadataItem::Unknown {
typecode,
data: data.to_vec(),

Check warning on line 241 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L239-L241

Added lines #L239 - L241 were not covered by tests
}),
(Revision::R1, MetadataTypecode::MustUnderstand(tc)) => {
Err(ParseError::NotUnderstood(tc))

Check warning on line 244 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L243-L244

Added lines #L243 - L244 were not covered by tests
}
(Revision::R0, _) => {
// The entire R0 metadata range has now been reserved for MUST-understand metadata
// in R1, so no R0 metadata range now exists.
Err(ParseError::NotUnderstood(typecode.into()))

Check warning on line 249 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L249

Added line #L249 was not covered by tests
}
}
}

Expand Down Expand Up @@ -346,8 +357,15 @@ impl fmt::Display for ParseError {

impl Error for ParseError {}

/// The revision of the Unified Address standard that an address was parsed under.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Revision {
R0,
R1,
}

pub(crate) mod private {
use super::{DataTypecode, ParseError, Typecode, PADDING_LEN};
use super::{DataTypecode, ParseError, Revision, Typecode, PADDING_LEN};
use crate::{
unified::{Item, MetadataItem},
Network,
Expand All @@ -372,29 +390,47 @@ pub(crate) mod private {

/// A Unified Container containing addresses or viewing keys.
pub trait SealedContainer: super::Container + std::marker::Sized {
const MAINNET: &'static str;
const TESTNET: &'static str;
const REGTEST: &'static str;
const MAINNET_R0: &'static str;
const TESTNET_R0: &'static str;
const REGTEST_R0: &'static str;

const MAINNET_R1: &'static str;
const TESTNET_R1: &'static str;
const REGTEST_R1: &'static str;

/// Implementations of this method should act as unchecked constructors
/// of the container type; the caller is guaranteed to check the
/// general invariants that apply to all unified containers.
fn from_inner(items: Vec<Item<Self::DataItem>>) -> Self;
fn from_inner(revision: Revision, items: Vec<Item<Self::DataItem>>) -> Self;

fn network_hrp(revision: Revision, network: &Network) -> &'static str {

Check warning on line 406 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L406

Added line #L406 was not covered by tests
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,

Check warning on line 413 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L411-L413

Added lines #L411 - L413 were not covered by tests
}
}

fn network_hrp(network: &Network) -> &'static str {
match network {
Network::Main => Self::MAINNET,
Network::Test => Self::TESTNET,
Network::Regtest => Self::REGTEST,
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

Check warning on line 420 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L420

Added line #L420 was not covered by tests
{
Some(Revision::R1)

Check warning on line 422 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L422

Added line #L422 was not covered by tests
} else {
None

Check warning on line 424 in components/zcash_address/src/kind/unified.rs

View check run for this annotation

Codecov / codecov/patch

components/zcash_address/src/kind/unified.rs#L424

Added line #L424 was not covered by tests
}
}

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
Expand Down Expand Up @@ -431,11 +467,13 @@ pub(crate) mod private {
}

/// Parse the items of the unified container.
#[allow(clippy::type_complexity)]
fn parse_items<T: Into<Vec<u8>>>(
hrp: &str,
buf: T,
) -> Result<Vec<Item<Self::DataItem>>, ParseError> {
fn read_receiver<R: SealedDataItem>(
) -> Result<(Revision, Vec<Item<Self::DataItem>>), ParseError> {
fn read_item<R: SealedDataItem>(
revision: Revision,
mut cursor: &mut std::io::Cursor<&[u8]>,
) -> Result<Item<R>, ParseError> {
let typecode = CompactSize::read(&mut cursor)
Expand Down Expand Up @@ -468,7 +506,9 @@ pub(crate) mod private {
let data = &buf[cursor.position() as usize..addr_end as usize];
let result = match Typecode::try_from(typecode)? {
Typecode::Data(tc) => Item::Data(R::parse(tc, data)?),
Typecode::Metadata(tc) => Item::Metadata(MetadataItem::parse(tc, data)?),
Typecode::Metadata(tc) => {
Item::Metadata(MetadataItem::parse(revision, tc, data)?)
}
};
cursor.set_position(addr_end);
Ok(result)
Expand All @@ -495,19 +535,25 @@ pub(crate) mod private {
)),
}?;

let revision = Self::hrp_revision(hrp)
.ok_or_else(|| ParseError::UnknownPrefix(hrp.to_string()))?;

let mut cursor = std::io::Cursor::new(encoded);
let mut result = vec![];
while cursor.position() < encoded.len().try_into().unwrap() {
result.push(read_receiver(&mut cursor)?);
result.push(read_item(revision, &mut cursor)?);
}
assert_eq!(cursor.position(), encoded.len().try_into().unwrap());

Ok(result)
Ok((revision, result))
}

/// A private function that constructs a unified container with the
/// specified items, which must be in ascending typecode order.
fn try_from_items_internal(items: Vec<Item<Self::DataItem>>) -> Result<Self, ParseError> {
fn try_from_items_internal(
revision: Revision,
items: Vec<Item<Self::DataItem>>,
) -> Result<Self, ParseError> {
assert!(u32::from(Typecode::P2SH) == u32::from(Typecode::P2PKH) + 1);

let mut only_transparent = true;
Expand Down Expand Up @@ -535,12 +581,13 @@ pub(crate) mod private {
Err(ParseError::OnlyTransparent)
} else {
// All checks pass!
Ok(Self::from_inner(items))
Ok(Self::from_inner(revision, items))
}
}

fn parse_internal<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Self, ParseError> {
Self::parse_items(hrp, buf).and_then(Self::try_from_items_internal)
Self::parse_items(hrp, buf)
.and_then(|(revision, items)| Self::try_from_items_internal(revision, items))
}
}
}
Expand All @@ -559,9 +606,12 @@ pub trait Encoding: private::SealedContainer {
/// * the item list may not contain two items having the same typecode
/// * the item list may not contain only transparent items (or no items)
/// * the item list may not contain both P2PKH and P2SH items.
fn try_from_items(mut items: Vec<Item<Self::DataItem>>) -> Result<Self, ParseError> {
fn try_from_items(
revision: Revision,
mut items: Vec<Item<Self::DataItem>>,
) -> Result<Self, ParseError> {
items.sort_unstable_by(Item::encoding_order);
Self::try_from_items_internal(items)
Self::try_from_items_internal(revision, items)
}

/// Decodes a unified container from its string representation, preserving
Expand All @@ -588,7 +638,7 @@ pub trait Encoding: private::SealedContainer {
/// ordering of the contained items such that it correctly obeys round-trip
/// serialization invariants.
fn encode(&self, network: &Network) -> String {
let hrp = Self::network_hrp(network);
let hrp = Self::network_hrp(self.revision(), network);
bech32::encode(
hrp,
self.to_jumbled_bytes(hrp).to_base32(),
Expand All @@ -607,4 +657,8 @@ pub trait Container {
///
/// This API is for advanced usage; in most cases you should use `Self::items`.
fn items_as_parsed(&self) -> &[Item<Self::DataItem>];

/// Returns the revision of the ZIP 316 standard that this unified container
/// conforms to.
fn revision(&self) -> Revision;
}
Loading

0 comments on commit 2833744

Please sign in to comment.