366 lines
12 KiB
Rust
366 lines
12 KiB
Rust
//! Transparent Address types.
|
|
|
|
use std::{fmt, io};
|
|
|
|
use ripemd::{Digest, Ripemd160};
|
|
use secp256k1::PublicKey;
|
|
use sha2::Sha256;
|
|
|
|
use crate::{
|
|
parameters::Network,
|
|
serialization::{SerializationError, ZcashDeserialize, ZcashSerialize},
|
|
transparent::{opcodes::OpCode, Script},
|
|
};
|
|
|
|
#[cfg(test)]
|
|
use proptest::prelude::*;
|
|
|
|
/// Transparent Zcash Addresses
|
|
///
|
|
/// In Bitcoin a single byte is used for the version field identifying
|
|
/// the address type. In Zcash two bytes are used. For addresses on
|
|
/// the production network, this and the encoded length cause the first
|
|
/// two characters of the Base58Check encoding to be fixed as "t3" for
|
|
/// P2SH addresses, and as "t1" for P2PKH addresses. (This does not
|
|
/// imply that a transparent Zcash address can be parsed identically
|
|
/// to a Bitcoin address just by removing the "t".)
|
|
///
|
|
/// <https://zips.z.cash/protocol/protocol.pdf#transparentaddrencoding>
|
|
#[derive(
|
|
Copy, Clone, Eq, PartialEq, Hash, serde_with::SerializeDisplay, serde_with::DeserializeFromStr,
|
|
)]
|
|
#[cfg_attr(
|
|
any(test, feature = "proptest-impl"),
|
|
derive(proptest_derive::Arbitrary)
|
|
)]
|
|
pub enum Address {
|
|
/// P2SH (Pay to Script Hash) addresses
|
|
PayToScriptHash {
|
|
/// Production, test, or other network
|
|
network: Network,
|
|
/// 20 bytes specifying a script hash.
|
|
script_hash: [u8; 20],
|
|
},
|
|
|
|
/// P2PKH (Pay to Public Key Hash) addresses
|
|
PayToPublicKeyHash {
|
|
/// Production, test, or other network
|
|
network: Network,
|
|
/// 20 bytes specifying a public key hash, which is a RIPEMD-160
|
|
/// hash of a SHA-256 hash of a compressed ECDSA key encoding.
|
|
pub_key_hash: [u8; 20],
|
|
},
|
|
}
|
|
|
|
impl fmt::Debug for Address {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let mut debug_struct = f.debug_struct("TransparentAddress");
|
|
|
|
match self {
|
|
Address::PayToScriptHash {
|
|
network,
|
|
script_hash,
|
|
} => debug_struct
|
|
.field("network", network)
|
|
.field("script_hash", &hex::encode(script_hash))
|
|
.finish(),
|
|
Address::PayToPublicKeyHash {
|
|
network,
|
|
pub_key_hash,
|
|
} => debug_struct
|
|
.field("network", network)
|
|
.field("pub_key_hash", &hex::encode(pub_key_hash))
|
|
.finish(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Address {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let mut bytes = io::Cursor::new(Vec::new());
|
|
let _ = self.zcash_serialize(&mut bytes);
|
|
|
|
f.write_str(&bs58::encode(bytes.get_ref()).with_check().into_string())
|
|
}
|
|
}
|
|
|
|
impl std::str::FromStr for Address {
|
|
type Err = SerializationError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let result = &bs58::decode(s).with_check(None).into_vec();
|
|
|
|
match result {
|
|
Ok(bytes) => Self::zcash_deserialize(&bytes[..]),
|
|
Err(_) => Err(SerializationError::Parse("t-addr decoding error")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ZcashSerialize for Address {
|
|
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
|
match self {
|
|
Address::PayToScriptHash {
|
|
network,
|
|
script_hash,
|
|
} => {
|
|
writer.write_all(&network.b58_script_address_prefix())?;
|
|
writer.write_all(script_hash)?
|
|
}
|
|
Address::PayToPublicKeyHash {
|
|
network,
|
|
pub_key_hash,
|
|
} => {
|
|
writer.write_all(&network.b58_pubkey_address_prefix())?;
|
|
writer.write_all(pub_key_hash)?
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl ZcashDeserialize for Address {
|
|
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
|
let mut version_bytes = [0; 2];
|
|
reader.read_exact(&mut version_bytes)?;
|
|
|
|
let mut hash_bytes = [0; 20];
|
|
reader.read_exact(&mut hash_bytes)?;
|
|
|
|
match version_bytes {
|
|
zcash_primitives::constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX => {
|
|
Ok(Address::PayToScriptHash {
|
|
network: Network::Mainnet,
|
|
script_hash: hash_bytes,
|
|
})
|
|
}
|
|
zcash_primitives::constants::testnet::B58_SCRIPT_ADDRESS_PREFIX => {
|
|
Ok(Address::PayToScriptHash {
|
|
network: Network::Testnet,
|
|
script_hash: hash_bytes,
|
|
})
|
|
}
|
|
zcash_primitives::constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX => {
|
|
Ok(Address::PayToPublicKeyHash {
|
|
network: Network::Mainnet,
|
|
pub_key_hash: hash_bytes,
|
|
})
|
|
}
|
|
zcash_primitives::constants::testnet::B58_PUBKEY_ADDRESS_PREFIX => {
|
|
Ok(Address::PayToPublicKeyHash {
|
|
network: Network::Testnet,
|
|
pub_key_hash: hash_bytes,
|
|
})
|
|
}
|
|
_ => Err(SerializationError::Parse("bad t-addr version/type")),
|
|
}
|
|
}
|
|
}
|
|
|
|
trait ToAddressWithNetwork {
|
|
/// Convert `self` to an `Address`, given the current `network`.
|
|
fn to_address(&self, network: Network) -> Address;
|
|
}
|
|
|
|
impl ToAddressWithNetwork for Script {
|
|
fn to_address(&self, network: Network) -> Address {
|
|
Address::PayToScriptHash {
|
|
network,
|
|
script_hash: Address::hash_payload(self.as_raw_bytes()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToAddressWithNetwork for PublicKey {
|
|
fn to_address(&self, network: Network) -> Address {
|
|
Address::PayToPublicKeyHash {
|
|
network,
|
|
pub_key_hash: Address::hash_payload(&self.serialize()[..]),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Address {
|
|
/// Create an address for the given public key hash and network.
|
|
pub fn from_pub_key_hash(network: Network, pub_key_hash: [u8; 20]) -> Self {
|
|
Self::PayToPublicKeyHash {
|
|
network,
|
|
pub_key_hash,
|
|
}
|
|
}
|
|
|
|
/// Create an address for the given script hash and network.
|
|
pub fn from_script_hash(network: Network, script_hash: [u8; 20]) -> Self {
|
|
Self::PayToScriptHash {
|
|
network,
|
|
script_hash,
|
|
}
|
|
}
|
|
|
|
/// Returns the network for this address.
|
|
pub fn network(&self) -> Network {
|
|
match *self {
|
|
Address::PayToScriptHash { network, .. } => network,
|
|
Address::PayToPublicKeyHash { network, .. } => network,
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the address is `PayToScriptHash`, and `false` if it is `PayToPublicKeyHash`.
|
|
pub fn is_script_hash(&self) -> bool {
|
|
matches!(self, Address::PayToScriptHash { .. })
|
|
}
|
|
|
|
/// Returns the hash bytes for this address, regardless of the address type.
|
|
///
|
|
/// # Correctness
|
|
///
|
|
/// Use [`ZcashSerialize`] and [`ZcashDeserialize`] for consensus-critical serialization.
|
|
pub fn hash_bytes(&self) -> [u8; 20] {
|
|
match *self {
|
|
Address::PayToScriptHash { script_hash, .. } => script_hash,
|
|
Address::PayToPublicKeyHash { pub_key_hash, .. } => pub_key_hash,
|
|
}
|
|
}
|
|
|
|
/// A hash of a transparent address payload, as used in
|
|
/// transparent pay-to-script-hash and pay-to-publickey-hash
|
|
/// addresses.
|
|
///
|
|
/// The resulting hash in both of these cases is always exactly 20
|
|
/// bytes.
|
|
/// <https://en.bitcoin.it/Base58Check_encoding#Encoding_a_Bitcoin_address>
|
|
fn hash_payload(bytes: &[u8]) -> [u8; 20] {
|
|
let sha_hash = Sha256::digest(bytes);
|
|
let ripe_hash = Ripemd160::digest(sha_hash);
|
|
let mut payload = [0u8; 20];
|
|
payload[..].copy_from_slice(&ripe_hash[..]);
|
|
payload
|
|
}
|
|
|
|
/// Given a transparent address (P2SH or a P2PKH), create a script that can be used in a coinbase
|
|
/// transaction output.
|
|
pub fn create_script_from_address(&self) -> Script {
|
|
let mut script_bytes = Vec::new();
|
|
|
|
match self {
|
|
// https://developer.bitcoin.org/devguide/transactions.html#pay-to-script-hash-p2sh
|
|
Address::PayToScriptHash { .. } => {
|
|
script_bytes.push(OpCode::Hash160 as u8);
|
|
script_bytes.push(OpCode::Push20Bytes as u8);
|
|
script_bytes.extend(self.hash_bytes());
|
|
script_bytes.push(OpCode::Equal as u8);
|
|
}
|
|
// https://developer.bitcoin.org/devguide/transactions.html#pay-to-public-key-hash-p2pkh
|
|
Address::PayToPublicKeyHash { .. } => {
|
|
script_bytes.push(OpCode::Dup as u8);
|
|
script_bytes.push(OpCode::Hash160 as u8);
|
|
script_bytes.push(OpCode::Push20Bytes as u8);
|
|
script_bytes.extend(self.hash_bytes());
|
|
script_bytes.push(OpCode::EqualVerify as u8);
|
|
script_bytes.push(OpCode::CheckSig as u8);
|
|
}
|
|
};
|
|
|
|
Script::new(&script_bytes)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
|
|
use secp256k1::PublicKey;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn pubkey_mainnet() {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
let pub_key = PublicKey::from_slice(&[
|
|
3, 23, 183, 225, 206, 31, 159, 148, 195, 42, 67, 115, 146, 41, 248, 140, 11, 3, 51, 41,
|
|
111, 180, 110, 143, 114, 134, 88, 73, 198, 174, 52, 184, 78,
|
|
])
|
|
.expect("A PublicKey from slice");
|
|
|
|
let t_addr = pub_key.to_address(Network::Mainnet);
|
|
|
|
assert_eq!(format!("{t_addr}"), "t1bmMa1wJDFdbc2TiURQP5BbBz6jHjUBuHq");
|
|
}
|
|
|
|
#[test]
|
|
fn pubkey_testnet() {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
let pub_key = PublicKey::from_slice(&[
|
|
3, 23, 183, 225, 206, 31, 159, 148, 195, 42, 67, 115, 146, 41, 248, 140, 11, 3, 51, 41,
|
|
111, 180, 110, 143, 114, 134, 88, 73, 198, 174, 52, 184, 78,
|
|
])
|
|
.expect("A PublicKey from slice");
|
|
|
|
let t_addr = pub_key.to_address(Network::Testnet);
|
|
|
|
assert_eq!(format!("{t_addr}"), "tmTc6trRhbv96kGfA99i7vrFwb5p7BVFwc3");
|
|
}
|
|
|
|
#[test]
|
|
fn empty_script_mainnet() {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
let script = Script::new(&[0u8; 20]);
|
|
|
|
let t_addr = script.to_address(Network::Mainnet);
|
|
|
|
assert_eq!(format!("{t_addr}"), "t3Y5pHwfgHbS6pDjj1HLuMFxhFFip1fcJ6g");
|
|
}
|
|
|
|
#[test]
|
|
fn empty_script_testnet() {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
let script = Script::new(&[0; 20]);
|
|
|
|
let t_addr = script.to_address(Network::Testnet);
|
|
|
|
assert_eq!(format!("{t_addr}"), "t2L51LcmpA43UMvKTw2Lwtt9LMjwyqU2V1P");
|
|
}
|
|
|
|
#[test]
|
|
fn from_string() {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
let t_addr: Address = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".parse().unwrap();
|
|
|
|
assert_eq!(format!("{t_addr}"), "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd");
|
|
}
|
|
|
|
#[test]
|
|
fn debug() {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
let t_addr: Address = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".parse().unwrap();
|
|
|
|
assert_eq!(
|
|
format!("{t_addr:?}"),
|
|
"TransparentAddress { network: Mainnet, script_hash: \"7d46a730d31f97b1930d3368a967c309bd4d136a\" }"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
proptest! {
|
|
|
|
#[test]
|
|
fn transparent_address_roundtrip(taddr in any::<Address>()) {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
let mut data = Vec::new();
|
|
|
|
taddr.zcash_serialize(&mut data).expect("t-addr should serialize");
|
|
|
|
let taddr2 = Address::zcash_deserialize(&data[..]).expect("randomized t-addr should deserialize");
|
|
|
|
prop_assert_eq![taddr, taddr2];
|
|
}
|
|
}
|