zebra/zebra-chain/src/transparent/address.rs

366 lines
12 KiB
Rust
Raw Normal View History

//! Transparent Address types.
use std::{fmt, io};
change(nu5): use new V5 transaction script verification API (#3799) * update librustzcash; adapt to new API * add ticket reference for removing zcash_proofs duplicated dependencies * update to new zcash_script V5 API * use zp_tx shorthand * update to Zcash 4.7.0 dependencies * update protocol versions * feat(rpc): Implement `getblockchaininfo` RPC method (#3891) * Implement `getblockchaininfo` RPC method * add a test for `get_blockchain_info` * fix tohex/fromhex * move comment * Update lightwalletd acceptance test for getblockchaininfo RPC (#3914) * change(rpc): Return getblockchaininfo network upgrades in height order (#3915) * Update lightwalletd acceptance test for getblockchaininfo RPC * Update some doc comments for network upgrades * List network upgrades in order in the getblockchaininfo RPC Also: - Use a constant for the "missing consensus branch ID" RPC value - Simplify fetching consensus branch IDs - Make RPC type derives consistent - Update RPC type documentation * Make RPC type derives consistent * Fix a confusing test comment * get hashand height at the same time * fix estimated_height * fix lint * add extra check Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> * fix typo Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> * split test Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> * fix(rpc): ignore an expected error in the RPC acceptance tests (#3961) * Add ignored regexes to test command failure regex methods * Ignore empty chain error in getblockchaininfo We expect this error when zebrad starts up with an empty state. Co-authored-by: teor <teor@riseup.net> Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Make sync error logs more user-friendly (#3944) - use info level, there is nothing the user needs to do, particularly for a single error - explain that the errors are temporary - hide backtraces, because they look like crashes * Update test.patch.yml with lightwalletd job (#3970) * Update test.patch.yml with lightwalletd job * Remove a workflow condition that will always be false In general, patch workflows need the opposite conditions to the original workflow. But in this case, we know the result of the condition will always be true, so we can just delete it. Co-authored-by: teor <teor@riseup.net> * fix(doc): Fix bugs in the lightwalletd database design (#3964) * Re-order column families in design in dependency order * Minor RFC design tweaks and fixes Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Repoint zebra image links to our new zfnd.org site for now (#3949) * Repoint zebra image links to our new zfnd.org site for now * Remove images/ Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Fix typos (#3956) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: teor <teor@riseup.net> * bump database version to trigger testnet rollback * reduce minimum protocol version for now (will be changed later) * update dependencies * Apply suggestions from code review Co-authored-by: teor <teor@riseup.net> * update versions to match zcash 4.7.0 * deny.toml: update 'darling' Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com> Co-authored-by: teor <teor@riseup.net> Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Deirdre Connolly <durumcrustulum@gmail.com> Co-authored-by: Dimitris Apostolou <dimitris.apostolou@icloud.com>
2022-04-18 17:14:16 -07:00
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],
},
6. feat(db): Add a transparent address UTXO index (#3999) * Add test-only serialization, and make existing serialization test-only * Make AddressLocations clearer in the API * Add UnspentOutputAddressLocation * Add the AddressLocation to the UTXO database value * Update the snapshot test code for UnspentOutputAddressLocation * Update the raw data snapshots * Update the high-level data snapshots * Increment the database version * Make serialization clearer Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> * Fix code formatting Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> * Add an empty utxo_by_transparent_addr_loc column family * Update snapshot data for the new column family * Add an AddressUnspentOutputs type * Add round-trip tests for AddressUnspentOutputs * Move address balances into their own method * Simplify updating address balances * Fix utxo_by_out_loc column family name * Implement reads and writes of address UTXOs * Update raw data snapshots * Update the snapshot tests for high-level address UTXOs * Assert rather than taking empty address snapshots for genesis * Update high-level address UTXO snapshot data, and delete empty snapshots * Increment the database version * Use typed values for all ReadDisk methods * Implement test-only serialization for transparent::Address * Implement FromDisk for () * Store AddressUnspentOutput as the column family key * Update round-trip serialization tests for AddressUnspentOutput * Update snapshot test code, and add a UTXO data snapshot * Update existing snapshot data * Add new UTXO snapshot data * Update column family name ```sh fastmod utxo_by_transparent_addr_loc utxo_loc_by_transparent_addr_loc zebra* ``` * cargo fmt --all * cargo insta test --review --delete-unreferenced-snapshots * Explain why it is ok to use invalid database iterator indexes Co-authored-by: Conrado Gouvea <conrado@zfnd.org> * Add explanations of UTXO database updates * Simplify an assertion * Remove UnspentOutputAddressLocation and just store transparent::Output * Update snapshot test data Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> Co-authored-by: Conrado Gouvea <conrado@zfnd.org> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2022-04-12 21:06:52 -07:00
/// 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];
}
}