Skip to content

Commit

Permalink
zcash_address: Add support for ZIP 320, TEX addresses.
Browse files Browse the repository at this point in the history
  • Loading branch information
nuttycom committed Feb 29, 2024
1 parent fba4a1b commit 3742501
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 20 deletions.
3 changes: 3 additions & 0 deletions components/zcash_address/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ and this library adheres to Rust's notion of
- `MetadataItem`
- `MetadataTypecode`
- `Revision`
- `zcash_address::convert::TryFromRawAddress::try_from_raw_tex`
- `zcash_address::convert::TryFromAddress::try_from_tex`
- `zcash_address::convert::ToAddress::from_tex`

### Changed
- `zcash_address::unified`:
Expand Down
31 changes: 31 additions & 0 deletions components/zcash_address/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ pub trait TryFromRawAddress: Sized {
"transparent P2SH",
)))
}

fn try_from_raw_tex(data: [u8; 20]) -> Result<Self, ConversionError<Self::Error>> {
let _ = data;
Err(ConversionError::Unsupported(UnsupportedAddress(
"transparent-source restricted P2PKH",
)))
}
}

/// A helper trait for converting a [`ZcashAddress`] into another type.
Expand Down Expand Up @@ -225,6 +232,13 @@ pub trait TryFromAddress: Sized {
"transparent P2SH",
)))
}

fn try_from_tex(net: Network, data: [u8; 20]) -> Result<Self, ConversionError<Self::Error>> {
let _ = (net, data);
Err(ConversionError::Unsupported(UnsupportedAddress(
"transparent-source restricted P2PKH",
)))
}
}

impl<T: TryFromRawAddress> TryFromAddress for (Network, T) {
Expand Down Expand Up @@ -261,6 +275,10 @@ impl<T: TryFromRawAddress> TryFromAddress for (Network, T) {
) -> Result<Self, ConversionError<Self::Error>> {
T::try_from_raw_transparent_p2sh(data).map(|addr| (net, addr))
}

fn try_from_tex(net: Network, data: [u8; 20]) -> Result<Self, ConversionError<Self::Error>> {
T::try_from_raw_tex(data).map(|addr| (net, addr))
}
}

/// A helper trait for converting another type into a [`ZcashAddress`].
Expand Down Expand Up @@ -304,6 +322,8 @@ pub trait ToAddress: private::Sealed {
fn from_transparent_p2pkh(net: Network, data: [u8; 20]) -> Self;

fn from_transparent_p2sh(net: Network, data: [u8; 20]) -> Self;

fn from_tex(net: Network, data: [u8; 20]) -> Self;
}

impl ToAddress for ZcashAddress {
Expand Down Expand Up @@ -353,6 +373,17 @@ impl ToAddress for ZcashAddress {
kind: AddressKind::P2sh(data),
}
}

fn from_tex(net: Network, data: [u8; 20]) -> Self {
ZcashAddress {
net: if let Network::Regtest = net {
Network::Test
} else {
net
},
kind: AddressKind::Tex(data),
}
}
}

mod private {
Expand Down
106 changes: 86 additions & 20 deletions components/zcash_address/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,34 +54,55 @@ impl FromStr for ZcashAddress {
kind: AddressKind::Unified(data),
});
}
Err(unified::ParseError::NotUnified) => {
// allow decoding to fall through to Sapling/Transparent
Err(unified::ParseError::NotUnified | unified::ParseError::UnknownPrefix(_)) => {
// allow decoding to fall through to Sapling/TEX/Transparent
}
Err(e) => {
return Err(ParseError::from(e));
}
}

// Try decoding as a Sapling address (Bech32)
if let Ok((hrp, data, Variant::Bech32)) = bech32::decode(s) {
// If we reached this point, the encoding is supposed to be valid Bech32.
// Try decoding as a Sapling or TEX address (Bech32/Bech32m)
if let Ok((hrp, data, variant)) = bech32::decode(s) {
// If we reached this point, the encoding is found to be valid Bech32 or Bech32m.
let data = Vec::<u8>::from_base32(&data).map_err(|_| ParseError::InvalidEncoding)?;

let net = match hrp.as_str() {
sapling::MAINNET => Network::Main,
sapling::TESTNET => Network::Test,
sapling::REGTEST => Network::Regtest,
// We will not define new Bech32 address encodings.
_ => {
return Err(ParseError::NotZcash);
match variant {
Variant::Bech32 => {
let net = match hrp.as_str() {
sapling::MAINNET => Network::Main,
sapling::TESTNET => Network::Test,
sapling::REGTEST => Network::Regtest,
// We will not define new Bech32 address encodings.
_ => {
return Err(ParseError::NotZcash);
}
};

return data[..]
.try_into()
.map(AddressKind::Sapling)
.map_err(|_| ParseError::InvalidEncoding)
.map(|kind| ZcashAddress { net, kind });
}
};
Variant::Bech32m => {
// Try decoding as a TEX address (Bech32m)
let net = match hrp.as_str() {
tex::MAINNET => Network::Main,
tex::TESTNET => Network::Test,
// Not recognized as a Zcash address type
_ => {
return Err(ParseError::NotZcash);
}
};

return data[..]
.try_into()
.map(AddressKind::Sapling)
.map_err(|_| ParseError::InvalidEncoding)
.map(|kind| ZcashAddress { net, kind });
return data[..]
.try_into()
.map(AddressKind::Tex)
.map_err(|_| ParseError::InvalidEncoding)
.map(|kind| ZcashAddress { net, kind });
}
}
}

// The rest use Base58Check.
Expand Down Expand Up @@ -110,8 +131,8 @@ impl FromStr for ZcashAddress {
}
}

fn encode_bech32(hrp: &str, data: &[u8]) -> String {
bech32::encode(hrp, data.to_base32(), Variant::Bech32).expect("hrp is invalid")
fn encode_bech32(hrp: &str, data: &[u8], variant: Variant) -> String {
bech32::encode(hrp, data.to_base32(), variant).expect("hrp is invalid")
}

fn encode_b58(prefix: [u8; 2], data: &[u8]) -> String {
Expand All @@ -138,6 +159,7 @@ impl fmt::Display for ZcashAddress {
Network::Regtest => sapling::REGTEST,
},
data,
Variant::Bech32,
),
AddressKind::Unified(addr) => addr.encode(&self.net),
AddressKind::P2pkh(data) => encode_b58(
Expand All @@ -154,13 +176,23 @@ impl fmt::Display for ZcashAddress {
},
data,
),
AddressKind::Tex(data) => encode_bech32(
match self.net {
Network::Main => tex::MAINNET,
Network::Test | Network::Regtest => tex::TESTNET,
},
data,
Variant::Bech32m,
),
};
write!(f, "{}", encoded)
}
}

#[cfg(test)]
mod tests {
use assert_matches::assert_matches;

use super::*;
use crate::{
kind::unified,
Expand Down Expand Up @@ -281,6 +313,40 @@ mod tests {
);
}

#[test]
fn tex() {
let p2pkh_str = "t1VmmGiyjVNeCjxDZzg7vZmd99WyzVby9yC";
let tex_str = "tex1s2rt77ggv6q989lr49rkgzmh5slsksa9khdgte";

// Transcode P2PKH to TEX
let p2pkh_zaddr: ZcashAddress = p2pkh_str.parse().unwrap();
assert_matches!(p2pkh_zaddr.net, Network::Main);
if let AddressKind::P2pkh(zaddr_data) = p2pkh_zaddr.kind {
let tex_zaddr = ZcashAddress {
net: p2pkh_zaddr.net,
kind: AddressKind::Tex(zaddr_data),
};

assert_eq!(tex_zaddr.to_string(), tex_str);
} else {
panic!("Decoded address should have been a p2pkh_str address.");
}

// Transcode TEX to P2PKH
let tex_zaddr: ZcashAddress = tex_str.parse().unwrap();
assert_matches!(tex_zaddr.net, Network::Main);
if let AddressKind::Tex(zaddr_data) = tex_zaddr.kind {
let p2pkh_zaddr = ZcashAddress {
net: tex_zaddr.net,
kind: AddressKind::P2pkh(zaddr_data),
};

assert_eq!(p2pkh_zaddr.to_string(), p2pkh_str);
} else {
panic!("Decoded address should have been a TEX address.");
}
}

#[test]
fn whitespace() {
assert_eq!(
Expand Down
1 change: 1 addition & 0 deletions components/zcash_address/src/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pub(crate) mod sprout;

pub(crate) mod p2pkh;
pub(crate) mod p2sh;
pub(crate) mod tex;
7 changes: 7 additions & 0 deletions components/zcash_address/src/kind/tex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// The prefix for a Base58Check-encoded mainnet transparent
/// [ZIP 320](https://zips.z.cash/zip-0320.html) TEX address.
pub(crate) const MAINNET: &str = "tex";

/// The prefix for a Base58Check-encoded testnet transparent
/// [ZIP 320](https://zips.z.cash/zip-0320.html) TEX address.
pub(crate) const TESTNET: &str = "textest";
3 changes: 3 additions & 0 deletions components/zcash_address/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ enum AddressKind {
Unified(unified::Address),
P2pkh([u8; 20]),
P2sh([u8; 20]),
Tex([u8; 20]),
}

impl ZcashAddress {
Expand Down Expand Up @@ -235,6 +236,7 @@ impl ZcashAddress {
AddressKind::Unified(data) => T::try_from_unified(self.net, data),
AddressKind::P2pkh(data) => T::try_from_transparent_p2pkh(self.net, data),
AddressKind::P2sh(data) => T::try_from_transparent_p2sh(self.net, data),
AddressKind::Tex(data) => T::try_from_tex(self.net, data),
}
}

Expand Down Expand Up @@ -270,6 +272,7 @@ impl ZcashAddress {
T::try_from_raw_transparent_p2pkh(data)
}
AddressKind::P2sh(data) if regtest_exception => T::try_from_raw_transparent_p2sh(data),
AddressKind::Tex(data) if regtest_exception => T::try_from_raw_tex(data),
_ => Err(ConversionError::IncorrectNetwork {
expected: net,
actual: self.net,
Expand Down

0 comments on commit 3742501

Please sign in to comment.