From 9862f6e5cffac6b964f78fd37f27ff8ffab7e469 Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Fri, 11 Mar 2022 00:23:04 -0300 Subject: [PATCH] feat: get addresses from transparent outputs (#3802) * feat: add Output::address() * test transactions in test blocks --- .../src/primitives/zcash_primitives.rs | 22 ++++- zebra-chain/src/transparent.rs | 12 ++- zebra-chain/src/transparent/address.rs | 16 ++++ zebra-chain/src/transparent/tests/vectors.rs | 89 +++++++++++++++++++ 4 files changed, 137 insertions(+), 2 deletions(-) diff --git a/zebra-chain/src/primitives/zcash_primitives.rs b/zebra-chain/src/primitives/zcash_primitives.rs index 4b2da31f..8ae8fc99 100644 --- a/zebra-chain/src/primitives/zcash_primitives.rs +++ b/zebra-chain/src/primitives/zcash_primitives.rs @@ -9,7 +9,7 @@ use std::{ use crate::{ amount::{Amount, NonNegative}, - parameters::NetworkUpgrade, + parameters::{Network, NetworkUpgrade}, serialization::ZcashSerialize, transaction::{AuthDigest, HashType, SigHash, Transaction}, transparent::{self, Script}, @@ -148,3 +148,23 @@ pub(crate) fn auth_digest(trans: &Transaction) -> AuthDigest { AuthDigest(digest_bytes) } + +/// Return the destination address from a transparent output. +/// +/// Returns None if the address type is not valid or unrecognized. +pub(crate) fn transparent_output_address( + output: &transparent::Output, + network: Network, +) -> Option { + let script = zcash_primitives::legacy::Script::from(&output.lock_script); + let alt_addr = script.address(); + match alt_addr { + Some(zcash_primitives::legacy::TransparentAddress::PublicKey(pub_key_hash)) => Some( + transparent::Address::from_pub_key_hash(network, pub_key_hash), + ), + Some(zcash_primitives::legacy::TransparentAddress::Script(script_hash)) => { + Some(transparent::Address::from_script_hash(network, script_hash)) + } + None => None, + } +} diff --git a/zebra-chain/src/transparent.rs b/zebra-chain/src/transparent.rs index 4e998d02..d95360ea 100644 --- a/zebra-chain/src/transparent.rs +++ b/zebra-chain/src/transparent.rs @@ -30,7 +30,10 @@ mod tests; use crate::{ amount::{Amount, NonNegative}, - block, transaction, + block, + parameters::Network, + primitives::zcash_primitives, + transaction, }; use std::{collections::HashMap, fmt, iter}; @@ -303,4 +306,11 @@ impl Output { pub fn value(&self) -> Amount { self.value } + + /// Return the destination address from a transparent output. + /// + /// Returns None if the address type is not valid or unrecognized. + pub fn address(&self, network: Network) -> Option
{ + zcash_primitives::transparent_output_address(self, network) + } } diff --git a/zebra-chain/src/transparent/address.rs b/zebra-chain/src/transparent/address.rs index 7cc31ee7..e4a2ad1b 100644 --- a/zebra-chain/src/transparent/address.rs +++ b/zebra-chain/src/transparent/address.rs @@ -192,6 +192,22 @@ impl ToAddressWithNetwork for PublicKey { } 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, + } + } + /// A hash of a transparent address payload, as used in /// transparent pay-to-script-hash and pay-to-publickey-hash /// addresses. diff --git a/zebra-chain/src/transparent/tests/vectors.rs b/zebra-chain/src/transparent/tests/vectors.rs index fa9fd262..174e27f1 100644 --- a/zebra-chain/src/transparent/tests/vectors.rs +++ b/zebra-chain/src/transparent/tests/vectors.rs @@ -1,4 +1,13 @@ +use std::sync::Arc; + use super::super::serialize::parse_coinbase_height; +use crate::{ + block::Block, parameters::Network, primitives::zcash_primitives::transparent_output_address, + serialization::ZcashDeserializeInto, transaction, +}; +use hex::FromHex; + +use zebra_test::prelude::*; #[test] fn parse_coinbase_height_mins() { @@ -37,3 +46,83 @@ fn parse_coinbase_height_mins() { let case4 = vec![0x04, 0x11, 0x00, 0x00, 0x00]; assert!(parse_coinbase_height(case4).is_err()); } + +#[test] +fn get_transparent_output_address() -> Result<()> { + zebra_test::init(); + + let script_tx: Vec = >::from_hex("0400008085202f8901fcaf44919d4a17f6181a02a7ebe0420be6f7dad1ef86755b81d5a9567456653c010000006a473044022035224ed7276e61affd53315eca059c92876bc2df61d84277cafd7af61d4dbf4002203ed72ea497a9f6b38eb29df08e830d99e32377edb8a574b8a289024f0241d7c40121031f54b095eae066d96b2557c1f99e40e967978a5fd117465dbec0986ca74201a6feffffff020050d6dc0100000017a9141b8a9bda4b62cd0d0582b55455d0778c86f8628f870d03c812030000001976a914e4ff5512ffafe9287992a1cd177ca6e408e0300388ac62070d0095070d000000000000000000000000") + .expect("Block bytes are in valid hex representation"); + + let transaction = script_tx.zcash_deserialize_into::>()?; + + // Hashes were extracted from the transaction (parsed with zebra-chain, + // then manually extracted from lock_script). + // Final expected values were generated with https://secretscan.org/PrivateKeyHex, + // by filling field 4 with the prefix followed by the address hash. + // Refer to + // for the prefixes. + + // Script hash 1b8a9bda4b62cd0d0582b55455d0778c86f8628f + let addr = transparent_output_address(&transaction.outputs()[0], Network::Mainnet) + .expect("should return address"); + assert_eq!(addr.to_string(), "t3M5FDmPfWNRG3HRLddbicsuSCvKuk9hxzZ"); + let addr = transparent_output_address(&transaction.outputs()[0], Network::Testnet) + .expect("should return address"); + assert_eq!(addr.to_string(), "t294SGSVoNq2daz15ZNbmAW65KQZ5e3nN5G"); + // Public key hash e4ff5512ffafe9287992a1cd177ca6e408e03003 + let addr = transparent_output_address(&transaction.outputs()[1], Network::Mainnet) + .expect("should return address"); + assert_eq!(addr.to_string(), "t1ekRwsd4LaSsd6NXgsx66q2HxQWTLCF44y"); + let addr = transparent_output_address(&transaction.outputs()[1], Network::Testnet) + .expect("should return address"); + assert_eq!(addr.to_string(), "tmWbBGi7TjExNmLZyMcFpxVh3ZPbGrpbX3H"); + + Ok(()) +} + +#[test] +fn get_transparent_output_address_with_blocks() { + zebra_test::init(); + + get_transparent_output_address_with_blocks_for_network(Network::Mainnet); + get_transparent_output_address_with_blocks_for_network(Network::Testnet); +} + +/// Test that the block test vector indexes match the heights in the block data, +/// and that each post-sapling block has a corresponding final sapling root. +fn get_transparent_output_address_with_blocks_for_network(network: Network) { + let block_iter = match network { + Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(), + Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(), + }; + + let mut valid_addresses = 0; + + for (&height, block_bytes) in block_iter.skip(1) { + let block = block_bytes + .zcash_deserialize_into::() + .expect("block is structurally valid"); + + for (idx, tx) in block.transactions.iter().enumerate() { + for output in tx.outputs() { + let addr = output.address(network); + if addr.is_none() && idx == 0 && output.lock_script.as_raw_bytes()[0] == 0x21 { + // There are a bunch of coinbase transactions with pay-to-pubkey scripts + // which we don't support; skip them + continue; + } + assert!( + addr.is_some(), + "address of {:?}; block #{}; tx #{}; must not be None", + output, + height, + idx, + ); + valid_addresses += 1; + } + } + } + // Make sure we didn't accidentally skip all vectors + assert!(valid_addresses > 0); +}