diff --git a/Cargo.lock b/Cargo.lock index d88d1524b8..f3610f8105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2994,7 +2994,7 @@ checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" [[package]] name = "zcash_address" -version = "0.3.1" +version = "0.3.2" dependencies = [ "assert_matches", "bech32", @@ -3234,7 +3234,7 @@ dependencies = [ [[package]] name = "zcash_protocol" -version = "0.0.0" +version = "0.1.0" dependencies = [ "document-features", "incrementalmerkletree", diff --git a/Cargo.toml b/Cargo.toml index df119c2430..4ef3e13064 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ zcash_address = { version = "0.3", path = "components/zcash_address" } zcash_client_backend = { version = "0.11", path = "zcash_client_backend" } zcash_encoding = { version = "0.2", path = "components/zcash_encoding" } zcash_keys = { version = "0.1", path = "zcash_keys" } -zcash_protocol = { version = "0.0", path = "components/zcash_protocol" } +zcash_protocol = { version = "0.1", path = "components/zcash_protocol" } zcash_note_encryption = "0.4" zcash_primitives = { version = "0.14", path = "zcash_primitives", default-features = false } diff --git a/components/zcash_address/CHANGELOG.md b/components/zcash_address/CHANGELOG.md index 780712c60c..1664be0820 100644 --- a/components/zcash_address/CHANGELOG.md +++ b/components/zcash_address/CHANGELOG.md @@ -7,6 +7,13 @@ and this library adheres to Rust's notion of ## [Unreleased] +## [0.3.2] - 2024-03-06 +### Added +- `zcash_address::convert`: + - `TryFromRawAddress::try_from_raw_tex` + - `TryFromAddress::try_from_tex` + - `ToAddress::from_tex` + ## [0.3.1] - 2024-01-12 ### Fixed - Stubs for `zcash_address::convert` traits that are created by `rust-analyzer` diff --git a/components/zcash_address/Cargo.toml b/components/zcash_address/Cargo.toml index e8ed075a56..0c18fe6454 100644 --- a/components/zcash_address/Cargo.toml +++ b/components/zcash_address/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zcash_address" description = "Zcash address parsing and serialization" -version = "0.3.1" +version = "0.3.2" authors = [ "Jack Grigg ", ] diff --git a/components/zcash_address/src/convert.rs b/components/zcash_address/src/convert.rs index 682433aba2..225b8775bb 100644 --- a/components/zcash_address/src/convert.rs +++ b/components/zcash_address/src/convert.rs @@ -137,6 +137,13 @@ pub trait TryFromRawAddress: Sized { "transparent P2SH", ))) } + + fn try_from_raw_tex(data: [u8; 20]) -> Result> { + let _ = data; + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent-source restricted P2PKH", + ))) + } } /// A helper trait for converting a [`ZcashAddress`] into another type. @@ -225,6 +232,13 @@ pub trait TryFromAddress: Sized { "transparent P2SH", ))) } + + fn try_from_tex(net: Network, data: [u8; 20]) -> Result> { + let _ = (net, data); + Err(ConversionError::Unsupported(UnsupportedAddress( + "transparent-source restricted P2PKH", + ))) + } } impl TryFromAddress for (Network, T) { @@ -261,6 +275,10 @@ impl TryFromAddress for (Network, T) { ) -> Result> { T::try_from_raw_transparent_p2sh(data).map(|addr| (net, addr)) } + + fn try_from_tex(net: Network, data: [u8; 20]) -> Result> { + T::try_from_raw_tex(data).map(|addr| (net, addr)) + } } /// A helper trait for converting another type into a [`ZcashAddress`]. @@ -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 { @@ -353,6 +373,13 @@ impl ToAddress for ZcashAddress { kind: AddressKind::P2sh(data), } } + + fn from_tex(net: Network, data: [u8; 20]) -> Self { + ZcashAddress { + net, + kind: AddressKind::Tex(data), + } + } } mod private { diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index 2f5bf8445f..a0d3c117d6 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -56,34 +56,56 @@ 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::::from_base32(&data).map_err(|_| ParseError::InvalidEncoding)?; - let net = match hrp.as_str() { - mainnet::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Main, - testnet::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Test, - regtest::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Regtest, - // We will not define new Bech32 address encodings. - _ => { - return Err(ParseError::NotZcash); + match variant { + Variant::Bech32 => { + let net = match hrp.as_str() { + mainnet::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Main, + testnet::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Test, + regtest::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::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() { + mainnet::HRP_TEX_ADDRESS => NetworkType::Main, + testnet::HRP_TEX_ADDRESS => NetworkType::Test, + regtest::HRP_TEX_ADDRESS => NetworkType::Regtest, + // 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. @@ -122,8 +144,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 { @@ -137,12 +159,17 @@ impl fmt::Display for ZcashAddress { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let encoded = match &self.kind { AddressKind::Sprout(data) => encode_b58(self.net.b58_sprout_address_prefix(), data), - AddressKind::Sapling(data) => { - encode_bech32(self.net.hrp_sapling_payment_address(), data) - } + AddressKind::Sapling(data) => encode_bech32( + self.net.hrp_sapling_payment_address(), + data, + Variant::Bech32, + ), AddressKind::Unified(addr) => addr.encode(&self.net), AddressKind::P2pkh(data) => encode_b58(self.net.b58_pubkey_address_prefix(), data), AddressKind::P2sh(data) => encode_b58(self.net.b58_script_address_prefix(), data), + AddressKind::Tex(data) => { + encode_bech32(self.net.hrp_tex_address(), data, Variant::Bech32m) + } }; write!(f, "{}", encoded) } @@ -150,6 +177,8 @@ impl fmt::Display for ZcashAddress { #[cfg(test)] mod tests { + use assert_matches::assert_matches; + use super::*; use crate::{kind::unified, Network}; @@ -258,6 +287,74 @@ 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 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 tex_testnet() { + let p2pkh_str = "tm9ofD7kHR7AF8MsJomEzLqGcrLCBkD9gDj"; + let tex_str = "textest1qyqszqgpqyqszqgpqyqszqgpqyqszqgpfcjgfy"; + + // Transcode P2PKH to TEX + let p2pkh_zaddr: ZcashAddress = p2pkh_str.parse().unwrap(); + assert_matches!(p2pkh_zaddr.net, Network::Test); + 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 address."); + } + + // Transcode TEX to P2PKH + let tex_zaddr: ZcashAddress = tex_str.parse().unwrap(); + assert_matches!(tex_zaddr.net, Network::Test); + 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!( diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index 6f516a941e..0fc00cded1 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -158,6 +158,7 @@ enum AddressKind { Unified(unified::Address), P2pkh([u8; 20]), P2sh([u8; 20]), + Tex([u8; 20]), } impl ZcashAddress { @@ -222,6 +223,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), } } @@ -257,6 +259,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 network_matches => T::try_from_raw_tex(data), _ => Err(ConversionError::IncorrectNetwork { expected: net, actual: self.net, diff --git a/components/zcash_protocol/CHANGELOG.md b/components/zcash_protocol/CHANGELOG.md index 839e3b0845..5854c937a7 100644 --- a/components/zcash_protocol/CHANGELOG.md +++ b/components/zcash_protocol/CHANGELOG.md @@ -6,6 +6,8 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [0.1.0] - 2024-03-06 The entries below are relative to the `zcash_primitives` crate as of the tag `zcash_primitives-0.14.0`. @@ -17,8 +19,9 @@ The entries below are relative to the `zcash_primitives` crate as of the tag - `zcash_protocol::value` replaces `zcash_primitives::transaction::components::amount` - `zcash_protocol::consensus`: - `NetworkConstants` has been extracted from the `Parameters` trait. Relative to the - state prior to the extraction, the bech32 prefixes now return `&'static str` instead - of `&str`. + state prior to the extraction: + - The Bech32 prefixes now return `&'static str` instead of `&str`. + - Added `NetworkConstants::hrp_tex_address`. - `NetworkType` - `Parameters::b58_sprout_address_prefix` - `zcash_protocol::consensus`: diff --git a/components/zcash_protocol/Cargo.toml b/components/zcash_protocol/Cargo.toml index 45d2c53e63..7a8114dad1 100644 --- a/components/zcash_protocol/Cargo.toml +++ b/components/zcash_protocol/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zcash_protocol" description = "Zcash protocol network constants and value types." -version = "0.0.0" +version = "0.1.0" authors = [ "Jack Grigg ", "Kris Nuttycombe ", diff --git a/components/zcash_protocol/src/consensus.rs b/components/zcash_protocol/src/consensus.rs index dc4a5d775d..7793b15f63 100644 --- a/components/zcash_protocol/src/consensus.rs +++ b/components/zcash_protocol/src/consensus.rs @@ -190,6 +190,14 @@ pub trait NetworkConstants: Clone { /// /// [`TransparentAddress::Script`]: zcash_primitives::legacy::TransparentAddress::Script fn b58_script_address_prefix(&self) -> [u8; 2]; + + /// Returns the Bech32-encoded human-readable prefix for TEX addresses, for the + /// network to which this `NetworkConstants` value applies. + /// + /// Defined in [ZIP 320]. + /// + /// [ZIP 320]: https://zips.z.cash/zip-0320 + fn hrp_tex_address(&self) -> &'static str; } /// The enumeration of known Zcash network types. @@ -264,6 +272,14 @@ impl NetworkConstants for NetworkType { NetworkType::Regtest => regtest::B58_SCRIPT_ADDRESS_PREFIX, } } + + fn hrp_tex_address(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_TEX_ADDRESS, + NetworkType::Test => testnet::HRP_TEX_ADDRESS, + NetworkType::Regtest => regtest::HRP_TEX_ADDRESS, + } + } } /// Zcash consensus parameters. @@ -310,6 +326,10 @@ impl NetworkConstants for P { fn b58_script_address_prefix(&self) -> [u8; 2] { self.network_type().b58_script_address_prefix() } + + fn hrp_tex_address(&self) -> &'static str { + self.network_type().hrp_tex_address() + } } /// Marker struct for the production network. diff --git a/components/zcash_protocol/src/constants/mainnet.rs b/components/zcash_protocol/src/constants/mainnet.rs index 25193fab88..98c81caa25 100644 --- a/components/zcash_protocol/src/constants/mainnet.rs +++ b/components/zcash_protocol/src/constants/mainnet.rs @@ -45,3 +45,8 @@ pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xb8]; /// /// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xbd]; + +/// The HRP for a Bech32m-encoded mainnet [ZIP 320] TEX address. +/// +/// [ZIP 320]: https://zips.z.cash/zip-0320 +pub const HRP_TEX_ADDRESS: &str = "tex"; diff --git a/components/zcash_protocol/src/constants/regtest.rs b/components/zcash_protocol/src/constants/regtest.rs index 2674a416de..001baa7ea4 100644 --- a/components/zcash_protocol/src/constants/regtest.rs +++ b/components/zcash_protocol/src/constants/regtest.rs @@ -52,3 +52,8 @@ pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1d, 0x25]; /// /// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; + +/// The HRP for a Bech32m-encoded regtest [ZIP 320] TEX address. +/// +/// [ZIP 320]: https://zips.z.cash/zip-0320 +pub const HRP_TEX_ADDRESS: &str = "texregtest"; diff --git a/components/zcash_protocol/src/constants/testnet.rs b/components/zcash_protocol/src/constants/testnet.rs index bbe74b6a55..023926546e 100644 --- a/components/zcash_protocol/src/constants/testnet.rs +++ b/components/zcash_protocol/src/constants/testnet.rs @@ -45,3 +45,8 @@ pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1d, 0x25]; /// /// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; + +/// The HRP for a Bech32m-encoded testnet [ZIP 320] TEX address. +/// +/// [ZIP 320]: https://zips.z.cash/zip-0320 +pub const HRP_TEX_ADDRESS: &str = "textest";