zcash_address: Add address-encoding support

This provides round-trip encoding for Zcash addresses.
This commit is contained in:
Jack Grigg 2021-03-08 03:25:59 +00:00 committed by Jack Grigg
parent c754363c07
commit ec77175d2b
9 changed files with 425 additions and 6 deletions

View File

@ -12,3 +12,5 @@ license = "MIT OR Apache-2.0"
edition = "2018"
[dependencies]
bech32 = "0.8"
bs58 = { version = "0.4", features = ["check"] }

View File

@ -0,0 +1,314 @@
use std::{convert::TryInto, error::Error, fmt, str::FromStr};
use bech32::{self, FromBase32, ToBase32, Variant};
use crate::{kind::*, AddressKind, Network, ZcashAddress};
/// An error while attempting to parse a string as a Zcash address.
#[derive(Debug, PartialEq)]
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,
}
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"),
}
}
}
impl Error for ParseError {}
impl FromStr for ZcashAddress {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Remove leading and trailing whitespace, to handle copy-paste errors.
let s = s.trim();
// 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::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,
_ => {
// 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.
return Err(
if hrp.starts_with('z') && (data.len() == 43 || data.len() == 75) {
ParseError::MaybeZcash
} else {
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 });
}
Err(_) => (),
}
// The rest use Base58Check.
if let Ok(decoded) = bs58::decode(s).with_check(None).into_vec() {
let net = match decoded[..2].try_into().unwrap() {
sprout::MAINNET | p2pkh::MAINNET | p2sh::MAINNET => Network::Main,
sprout::TESTNET | p2pkh::TESTNET | p2sh::TESTNET => Network::Test,
// We will not define new Base58Check address encodings.
_ => return Err(ParseError::NotZcash),
};
return match decoded[..2].try_into().unwrap() {
sprout::MAINNET | sprout::TESTNET => {
decoded[2..].try_into().map(AddressKind::Sprout)
}
p2pkh::MAINNET | p2pkh::TESTNET => decoded[2..].try_into().map(AddressKind::P2pkh),
p2sh::MAINNET | p2sh::TESTNET => decoded[2..].try_into().map(AddressKind::P2sh),
_ => unreachable!(),
}
.map_err(|_| ParseError::InvalidEncoding)
.map(|kind| ZcashAddress { kind, net });
};
// If it's not valid Bech32 or Base58Check, it's not a Zcash address.
Err(ParseError::NotZcash)
}
}
fn encode_bech32(hrp: &str, data: &[u8]) -> String {
bech32::encode(hrp, data.to_base32(), Variant::Bech32).expect("hrp is invalid")
}
fn encode_b58(prefix: [u8; 2], data: &[u8]) -> String {
let mut decoded = Vec::with_capacity(2 + data.len());
decoded.extend_from_slice(&prefix);
decoded.extend_from_slice(data);
bs58::encode(decoded).with_check().into_string()
}
impl fmt::Display for ZcashAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let encoded = match self.kind {
AddressKind::Sprout(data) => encode_b58(
if let Network::Main = self.net {
sprout::MAINNET
} else {
sprout::TESTNET
},
&data,
),
AddressKind::Sapling(data) => encode_bech32(
match self.net {
Network::Main => sapling::MAINNET,
Network::Test => sapling::TESTNET,
Network::Regtest => sapling::REGTEST,
},
&data,
),
AddressKind::Orchard(data) => encode_bech32(
match self.net {
Network::Main => orchard::MAINNET,
Network::Test => orchard::TESTNET,
Network::Regtest => orchard::REGTEST,
},
&data,
),
AddressKind::P2pkh(data) => encode_b58(
if let Network::Main = self.net {
p2pkh::MAINNET
} else {
p2pkh::TESTNET
},
&data,
),
AddressKind::P2sh(data) => encode_b58(
if let Network::Main = self.net {
p2sh::MAINNET
} else {
p2sh::TESTNET
},
&data,
),
};
write!(f, "{}", encoded)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn encoding(encoded: &str, decoded: ZcashAddress) {
assert_eq!(decoded.to_string(), encoded);
assert_eq!(encoded.parse(), Ok(decoded));
}
#[test]
fn sprout() {
encoding(
"zc8E5gYid86n4bo2Usdq1cpr7PpfoJGzttwBHEEgGhGkLUg7SPPVFNB2AkRFXZ7usfphup5426dt1buMmY3fkYeRrQGLa8y",
ZcashAddress { net: Network::Main, kind: AddressKind::Sprout([0; 64]) },
);
encoding(
"ztJ1EWLKcGwF2S4NA17pAJVdco8Sdkz4AQPxt1cLTEfNuyNswJJc2BbBqYrsRZsp31xbVZwhF7c7a2L9jsF3p3ZwRWpqqyS",
ZcashAddress { net: Network::Test, kind: AddressKind::Sprout([0; 64]) },
);
}
#[test]
fn sapling() {
encoding(
"zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g",
ZcashAddress {
net: Network::Main,
kind: AddressKind::Sapling([0; 43]),
},
);
encoding(
"ztestsapling1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqfhgwqu",
ZcashAddress {
net: Network::Test,
kind: AddressKind::Sapling([0; 43]),
},
);
encoding(
"zregtestsapling1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqknpr3m",
ZcashAddress {
net: Network::Regtest,
kind: AddressKind::Sapling([0; 43]),
},
);
}
#[test]
fn orchard() {
encoding(
"zo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq58lk79",
ZcashAddress {
net: Network::Main,
kind: AddressKind::Orchard([0; 43]),
},
);
encoding(
"ztestorchard1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqcrmt3p",
ZcashAddress {
net: Network::Test,
kind: AddressKind::Orchard([0; 43]),
},
);
encoding(
"zregtestorchard1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq88jxqx",
ZcashAddress {
net: Network::Regtest,
kind: AddressKind::Orchard([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(
"t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs",
ZcashAddress {
net: Network::Main,
kind: AddressKind::P2pkh([0; 20]),
},
);
encoding(
"tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma",
ZcashAddress {
net: Network::Test,
kind: AddressKind::P2pkh([0; 20]),
},
);
encoding(
"t3JZcvsuaXE6ygokL4XUiZSTrQBUoPYFnXJ",
ZcashAddress {
net: Network::Main,
kind: AddressKind::P2sh([0; 20]),
},
);
encoding(
"t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2",
ZcashAddress {
net: Network::Test,
kind: AddressKind::P2sh([0; 20]),
},
);
}
#[test]
fn whitespace() {
assert_eq!(
" t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse(),
Ok(ZcashAddress {
net: Network::Main,
kind: AddressKind::P2pkh([0; 20])
}),
);
assert_eq!(
"t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs ".parse(),
Ok(ZcashAddress {
net: Network::Main,
kind: AddressKind::P2pkh([0; 20])
}),
);
assert_eq!(
"something t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse::<ZcashAddress>(),
Err(ParseError::NotZcash),
);
}
}

View File

@ -0,0 +1,6 @@
pub(crate) mod orchard;
pub(crate) mod sapling;
pub(crate) mod sprout;
pub(crate) mod p2pkh;
pub(crate) mod p2sh;

View File

@ -0,0 +1,18 @@
/// 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,7 @@
/// The prefix for a Base58Check-encoded mainnet transparent P2PKH address.
pub(crate) const MAINNET: [u8; 2] = [0x1c, 0xb8];
/// The prefix for a Base58Check-encoded testnet transparent P2PKH address.
pub(crate) const TESTNET: [u8; 2] = [0x1d, 0x25];
pub(crate) type Data = [u8; 20];

View File

@ -0,0 +1,7 @@
/// The prefix for a Base58Check-encoded mainnet transparent P2SH address.
pub(crate) const MAINNET: [u8; 2] = [0x1c, 0xbd];
/// The prefix for a Base58Check-encoded testnet transparent P2SH address.
pub(crate) const TESTNET: [u8; 2] = [0x1c, 0xba];
pub(crate) type Data = [u8; 20];

View File

@ -0,0 +1,22 @@
/// The HRP for a Bech32-encoded mainnet Sapling address.
///
/// Defined in the [Zcash Protocol Specification section 5.6.4][saplingpaymentaddrencoding].
///
/// [saplingpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding
pub(crate) const MAINNET: &str = "zs";
/// The HRP for a Bech32-encoded testnet Sapling address.
///
/// Defined in the [Zcash Protocol Specification section 5.6.4][saplingpaymentaddrencoding].
///
/// [saplingpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding
pub(crate) const TESTNET: &str = "ztestsapling";
/// The HRP for a Bech32-encoded regtest Sapling address.
///
/// It is defined in [the `zcashd` codebase].
///
/// [the `zcashd` codebase]: https://github.com/zcash/zcash/blob/128d863fb8be39ee294fda397c1ce3ba3b889cb2/src/chainparams.cpp#L493
pub(crate) const REGTEST: &str = "zregtestsapling";
pub(crate) type Data = [u8; 43];

View File

@ -0,0 +1,15 @@
/// The prefix for a Base58Check-encoded mainnet Sprout address.
///
/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding].
///
/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding
pub(crate) const MAINNET: [u8; 2] = [0x16, 0x9a];
/// The prefix for a Base58Check-encoded testnet Sprout address.
///
/// Defined in the [Zcash Protocol Specification section 5.6.3][].
///
/// []: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding
pub(crate) const TESTNET: [u8; 2] = [0x16, 0xb6];
pub(crate) type Data = [u8; 64];

View File

@ -1,7 +1,35 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
mod encoding;
mod kind;
pub use encoding::ParseError;
/// A Zcash address.
#[derive(Debug, PartialEq)]
pub struct ZcashAddress {
net: Network,
kind: AddressKind,
}
/// The Zcash network for which an address is encoded.
#[derive(Debug, PartialEq)]
enum Network {
/// Zcash Mainnet.
Main,
/// Zcash Testnet.
Test,
/// Private integration / regression testing, used in `zcashd`.
///
/// For some address types there is no distinction between test and regtest encodings;
/// those will always be parsed as `Network::Test`.
Regtest,
}
/// Known kinds of Zcash addresses.
#[derive(Debug, PartialEq)]
enum AddressKind {
Sprout(kind::sprout::Data),
Sapling(kind::sapling::Data),
Orchard(kind::orchard::Data),
P2pkh(kind::p2pkh::Data),
P2sh(kind::p2sh::Data),
}