zcash_address: Replace Orchard address encodings with Unified Addresses

This commit removes the now-undefined Orchard encoding logic, and adds
the general Bech32m encoding/decoding logic for Unified Addresses. The
internal data format of Unified Addresses is not correct in this commit.
This commit is contained in:
Jack Grigg 2021-05-20 15:12:33 +01:00
parent 9f7398cd05
commit ae2b8bfd6d
6 changed files with 72 additions and 99 deletions

View File

@ -59,9 +59,9 @@ pub trait FromAddress: Sized {
Err(UnsupportedAddress("Sapling"))
}
fn from_orchard(net: Network, data: orchard::Data) -> Result<Self, UnsupportedAddress> {
fn from_unified(net: Network, data: unified::Data) -> Result<Self, UnsupportedAddress> {
let _ = (net, data);
Err(UnsupportedAddress("Orchard"))
Err(UnsupportedAddress("Unified"))
}
fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Result<Self, UnsupportedAddress> {
@ -111,7 +111,7 @@ pub trait ToAddress: private::Sealed {
fn from_sapling(net: Network, data: sapling::Data) -> Self;
fn from_orchard(net: Network, data: orchard::Data) -> Self;
fn from_unified(net: Network, data: unified::Data) -> Self;
fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Self;
@ -137,10 +137,10 @@ impl ToAddress for ZcashAddress {
}
}
fn from_orchard(net: Network, data: orchard::Data) -> Self {
fn from_unified(net: Network, data: unified::Data) -> Self {
ZcashAddress {
net,
kind: AddressKind::Orchard(data),
kind: AddressKind::Unified(data),
}
}

View File

@ -9,8 +9,6 @@ use crate::{kind::*, AddressKind, Network, ZcashAddress};
pub enum ParseError {
/// The string is an invalid encoding.
InvalidEncoding,
/// The string might be an unknown Zcash address from the future.
MaybeZcash,
/// The string is not a Zcash address.
NotZcash,
}
@ -19,10 +17,6 @@ impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::InvalidEncoding => write!(f, "Invalid encoding"),
ParseError::MaybeZcash => write!(
f,
"This might be a Zcash address from the future that we don't know about"
),
ParseError::NotZcash => write!(f, "Not a Zcash address"),
}
}
@ -40,49 +34,47 @@ impl FromStr for ZcashAddress {
// Most Zcash addresses use Bech32, so try that first.
match bech32::decode(s) {
// Zcash addresses only use the original Bech32 variant, since the data
// corresponding to a particular HRP always has a fixed length.
Ok((_, _, Variant::Bech32m)) => return Err(ParseError::NotZcash),
Ok((hrp, data, Variant::Bech32m)) => {
// If we reached this point, the encoding is supposed to be valid Bech32m.
let data =
Vec::<u8>::from_base32(&data).map_err(|_| ParseError::InvalidEncoding)?;
let net = match hrp.as_str() {
unified::MAINNET => Network::Main,
unified::TESTNET => Network::Test,
unified::REGTEST => Network::Regtest,
// We will not define new Bech32m address encodings.
_ => {
return Err(ParseError::NotZcash);
}
};
return data[..]
.try_into()
.map(AddressKind::Unified)
.map_err(|_| ParseError::InvalidEncoding)
.map(|kind| ZcashAddress { net, kind });
}
Ok((hrp, data, Variant::Bech32)) => {
// If we reached this point, the encoding is supposed to be valid Bech32.
let data =
Vec::<u8>::from_base32(&data).map_err(|_| ParseError::InvalidEncoding)?;
let net = match hrp.as_str() {
sapling::MAINNET | orchard::MAINNET => Network::Main,
sapling::TESTNET | orchard::TESTNET => Network::Test,
sapling::REGTEST | orchard::REGTEST => Network::Regtest,
sapling::MAINNET => Network::Main,
sapling::TESTNET => Network::Test,
sapling::REGTEST => Network::Regtest,
// We will not define new Bech32 address encodings.
_ => {
// Use some heuristics to try and guess whether this might be a Zcash
// address from the future:
// - Zcash HRPs always start with a 'z'.
// - Zcash shielded addresses with diversification have data of
// length 43, but if we added the simple form of detection keys
// the data would have length 75. Alternatively if we switch from a
// 11-byte diversifier to two field elements, that would be 64 bytes.
return Err(
if hrp.starts_with('z')
&& (data.len() == 43 || data.len() == 64 || data.len() == 75)
{
ParseError::MaybeZcash
} else {
ParseError::NotZcash
},
);
return Err(ParseError::NotZcash);
}
};
return match hrp.as_str() {
sapling::MAINNET | sapling::TESTNET | sapling::REGTEST => {
data[..].try_into().map(AddressKind::Sapling)
}
orchard::MAINNET | orchard::TESTNET | orchard::REGTEST => {
data[..].try_into().map(AddressKind::Orchard)
}
_ => unreachable!(),
}
.map_err(|_| ParseError::InvalidEncoding)
.map(|kind| ZcashAddress { net, kind });
return data[..]
.try_into()
.map(AddressKind::Sapling)
.map_err(|_| ParseError::InvalidEncoding)
.map(|kind| ZcashAddress { net, kind });
}
Err(_) => (),
}
@ -113,6 +105,10 @@ impl FromStr for ZcashAddress {
}
}
fn encode_bech32m(hrp: &str, data: &[u8]) -> String {
bech32::encode(hrp, data.to_base32(), Variant::Bech32m).expect("hrp is invalid")
}
fn encode_bech32(hrp: &str, data: &[u8]) -> String {
bech32::encode(hrp, data.to_base32(), Variant::Bech32).expect("hrp is invalid")
}
@ -143,11 +139,11 @@ impl fmt::Display for ZcashAddress {
},
&data,
),
AddressKind::Orchard(data) => encode_bech32(
AddressKind::Unified(data) => encode_bech32m(
match self.net {
Network::Main => orchard::MAINNET,
Network::Test => orchard::TESTNET,
Network::Regtest => orchard::REGTEST,
Network::Main => unified::MAINNET,
Network::Test => unified::TESTNET,
Network::Regtest => unified::REGTEST,
},
&data,
),
@ -219,49 +215,30 @@ mod tests {
}
#[test]
fn orchard() {
fn unified() {
encoding(
"zo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq58lk79",
ZcashAddress {
net: Network::Main,
kind: AddressKind::Orchard([0; 43]),
kind: AddressKind::Unified([0; 43]),
},
);
encoding(
"ztestorchard1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqcrmt3p",
ZcashAddress {
net: Network::Test,
kind: AddressKind::Orchard([0; 43]),
kind: AddressKind::Unified([0; 43]),
},
);
encoding(
"zregtestorchard1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq88jxqx",
ZcashAddress {
net: Network::Regtest,
kind: AddressKind::Orchard([0; 43]),
kind: AddressKind::Unified([0; 43]),
},
);
}
#[test]
fn maybe_zcash() {
assert_eq!(
"zmaybe1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqql7xs38"
.parse::<ZcashAddress>(),
Err(ParseError::MaybeZcash),
);
assert_eq!(
"zpossibly1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq559klt"
.parse::<ZcashAddress>(),
Err(ParseError::MaybeZcash),
);
assert_eq!(
"nope1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg8f5j9"
.parse::<ZcashAddress>(),
Err(ParseError::NotZcash),
);
}
#[test]
fn transparent() {
encoding(

View File

@ -1,4 +1,5 @@
pub(crate) mod orchard;
pub(crate) mod unified;
pub(crate) mod sapling;
pub(crate) mod sprout;

View File

@ -1,18 +0,0 @@
/// The HRP for a Bech32-encoded mainnet Orchard address.
///
/// Defined in the [Zcash Protocol Specification section 5.6.4.1][orchardpaymentaddrencoding].
///
/// [orchardpaymentaddrencoding]: https://zips.z.cash/protocol/nu5.pdf#orchardpaymentaddrencoding
pub(crate) const MAINNET: &str = "zo";
/// The HRP for a Bech32-encoded testnet Orchard address.
///
/// Defined in the [Zcash Protocol Specification section 5.6.4.1][orchardpaymentaddrencoding].
///
/// [orchardpaymentaddrencoding]: https://zips.z.cash/protocol/nu5.pdf#orchardpaymentaddrencoding
pub(crate) const TESTNET: &str = "ztestorchard";
/// The HRP for a Bech32-encoded regtest Orchard address.
pub(crate) const REGTEST: &str = "zregtestorchard";
pub(crate) type Data = [u8; 43];

View File

@ -0,0 +1,19 @@
/// The HRP for a Bech32m-encoded mainnet Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
pub(crate) const MAINNET: &str = "u";
/// The HRP for a Bech32m-encoded testnet Unified Address.
///
/// Defined in [ZIP 316][zip-0316].
///
/// [zip-0316]: https://zips.z.cash/zip-0316
pub(crate) const TESTNET: &str = "utest";
/// The HRP for a Bech32m-encoded regtest Unified Address.
pub(crate) const REGTEST: &str = "uregtest";
/// TODO
pub(crate) type Data = [u8; 43];

View File

@ -31,7 +31,7 @@ pub enum Network {
enum AddressKind {
Sprout(kind::sprout::Data),
Sapling(kind::sapling::Data),
Orchard(kind::orchard::Data),
Unified(kind::unified::Data),
P2pkh(kind::p2pkh::Data),
P2sh(kind::p2sh::Data),
}
@ -46,17 +46,11 @@ impl ZcashAddress {
///
/// # Errors
///
/// In most cases, [`ParseError::NotZcash`] will be returned on failure. The two
/// exceptions are:
///
/// - If the parser can detect that the string _must_ contain an address encoding used
/// by Zcash, [`ParseError::InvalidEncoding`] will be returned if any subsequent
/// part of that encoding is invalid.
///
/// - [`ParseError::MaybeZcash`] will be returned if the string is Bech32-encoded data
/// that satisfies some heuristics for probable future Zcash address formats (such
/// as beginning with a `z`). This can either be treated as an indication that this
/// library dependency should be updated, or mapped to [`ParseError::NotZcash`].
/// - In all other cases, [`ParseError::NotZcash`] will be returned on failure.
///
/// # Examples
///
@ -87,7 +81,7 @@ impl ZcashAddress {
match self.kind {
AddressKind::Sprout(data) => T::from_sprout(self.net, data),
AddressKind::Sapling(data) => T::from_sapling(self.net, data),
AddressKind::Orchard(data) => T::from_orchard(self.net, data),
AddressKind::Unified(data) => T::from_unified(self.net, data),
AddressKind::P2pkh(data) => T::from_transparent_p2pkh(self.net, data),
AddressKind::P2sh(data) => T::from_transparent_p2sh(self.net, data),
}