From f7ab253ce4d6c277329c5239e5f9cc52de7e1a22 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Fri, 1 Dec 2023 07:19:54 +1100 Subject: [PATCH 1/4] Remove stale comment This comment appears to come from before we had types for tweaked and untweaked keys in taproot. We can remove it. --- bitcoin/src/address/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs index 07115408..653b700e 100644 --- a/bitcoin/src/address/mod.rs +++ b/bitcoin/src/address/mod.rs @@ -479,8 +479,6 @@ impl Address { } /// Creates a pay to taproot address from a pre-tweaked output key. - /// - /// This method is not recommended for use, [`Address::p2tr()`] should be used where possible. pub fn p2tr_tweaked(output_key: TweakedPublicKey, network: Network) -> Address { let prog = WitnessProgram::new(WitnessVersion::V1, output_key.to_inner().serialize()) .expect("taproot output key has len 32 <= 40"); From 3490433618022046edd3004b9a07b36512377952 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Wed, 29 Nov 2023 13:00:58 +1100 Subject: [PATCH 2/4] Return error from wpubkey_hash Calling `wpubkey_hash` on a key that is uncompressed is flat out an error, really it is a programmer error at build time because a segwit key should never be compressed, however, for historical reasons we do not enforce this in the type system. As a step towards clarity make it an error to call `wpubkey_hash` on a an uncompressed pubkey. This adds documentation and potentially might assist debugging for newer devs. --- bitcoin/src/address/error.rs | 16 +++++++++------- bitcoin/src/address/mod.rs | 15 +++++---------- bitcoin/src/crypto/key.rs | 26 ++++++++++++++++++++------ 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/bitcoin/src/address/error.rs b/bitcoin/src/address/error.rs index 7e01868e..fd35ed33 100644 --- a/bitcoin/src/address/error.rs +++ b/bitcoin/src/address/error.rs @@ -6,6 +6,7 @@ use internals::write_err; use crate::address::{Address, NetworkUnchecked}; use crate::blockdata::script::{witness_program, witness_version}; +use crate::crypto::key; use crate::prelude::String; use crate::{base58, Network}; @@ -18,7 +19,7 @@ pub enum Error { /// A witness program error. WitnessProgram(witness_program::Error), /// An uncompressed pubkey was used where it is not allowed. - UncompressedPubkey, + UncompressedPubkey(key::UncompressedPubkeyError), /// Address size more than 520 bytes is not allowed. ExcessiveScriptSize, /// Script is not a p2pkh, p2sh or witness program. @@ -41,8 +42,7 @@ impl fmt::Display for Error { match *self { WitnessVersion(ref e) => write_err!(f, "witness version construction error"; e), WitnessProgram(ref e) => write_err!(f, "witness program error"; e), - UncompressedPubkey => - write!(f, "an uncompressed pubkey was used where it is not allowed"), + UncompressedPubkey(ref e) => write_err!(f, "uncompressed pubkey"; e), ExcessiveScriptSize => write!(f, "script size exceed 520 bytes"), UnrecognizedScript => write!(f, "script is not a p2pkh, p2sh or witness program"), NetworkValidation { required, found, ref address } => { @@ -66,14 +66,16 @@ impl std::error::Error for Error { match self { WitnessVersion(e) => Some(e), WitnessProgram(e) => Some(e), - UncompressedPubkey - | ExcessiveScriptSize - | UnrecognizedScript - | NetworkValidation { .. } => None, + UncompressedPubkey(e) => Some(e), + ExcessiveScriptSize | UnrecognizedScript | NetworkValidation { .. } => None, } } } +impl From for Error { + fn from(e: key::UncompressedPubkeyError) -> Self { Self::UncompressedPubkey(e) } +} + impl From for Error { fn from(e: witness_version::TryFromError) -> Error { Error::WitnessVersion(e) } } diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs index 653b700e..ec6b226e 100644 --- a/bitcoin/src/address/mod.rs +++ b/bitcoin/src/address/mod.rs @@ -424,10 +424,7 @@ impl Address { /// # Errors /// Will only return an error if an uncompressed public key is provided. pub fn p2wpkh(pk: &PublicKey, network: Network) -> Result { - let prog = WitnessProgram::new( - WitnessVersion::V0, - pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?, - )?; + let prog = WitnessProgram::new(WitnessVersion::V0, pk.wpubkey_hash()?)?; let payload = Payload::WitnessProgram(prog); Ok(Address(AddressInner { network, payload }, PhantomData)) } @@ -439,9 +436,7 @@ impl Address { /// # Errors /// Will only return an Error if an uncompressed public key is provided. pub fn p2shwpkh(pk: &PublicKey, network: Network) -> Result { - let builder = script::Builder::new() - .push_int(0) - .push_slice(pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?); + let builder = script::Builder::new().push_int(0).push_slice(pk.wpubkey_hash()?); let payload = Payload::ScriptHash(builder.into_script().script_hash()); Ok(Address(AddressInner { network, payload }, PhantomData)) @@ -802,7 +797,7 @@ mod tests { use secp256k1::XOnlyPublicKey; use super::*; - use crate::crypto::key::PublicKey; + use crate::crypto::key::{PublicKey, UncompressedPubkeyError}; use crate::network::Network::{Bitcoin, Testnet}; fn roundtrips(addr: &Address) { @@ -903,7 +898,7 @@ mod tests { // Test uncompressed pubkey key.compressed = false; - assert_eq!(Address::p2wpkh(&key, Bitcoin), Err(Error::UncompressedPubkey)); + assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError.into()); } #[test] @@ -932,7 +927,7 @@ mod tests { // Test uncompressed pubkey key.compressed = false; - assert_eq!(Address::p2wpkh(&key, Bitcoin), Err(Error::UncompressedPubkey)); + assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError.into()); } #[test] diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs index 6c4d354c..abb1ea96 100644 --- a/bitcoin/src/crypto/key.rs +++ b/bitcoin/src/crypto/key.rs @@ -58,15 +58,13 @@ impl PublicKey { pub fn pubkey_hash(&self) -> PubkeyHash { self.with_serialized(PubkeyHash::hash) } /// Returns bitcoin 160-bit hash of the public key for witness program - pub fn wpubkey_hash(&self) -> Option { + pub fn wpubkey_hash(&self) -> Result { if self.compressed { - Some(WPubkeyHash::from_byte_array( + Ok(WPubkeyHash::from_byte_array( hash160::Hash::hash(&self.inner.serialize()).to_byte_array(), )) } else { - // We can't create witness pubkey hashes for an uncompressed - // public keys - None + Err(UncompressedPubkeyError) } } @@ -746,6 +744,22 @@ impl From for Error { fn from(e: hex::HexToArrayError) -> Self { Error::Hex(e) } } +/// Segwit public keys must always be compressed. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct UncompressedPubkeyError; + +impl fmt::Display for UncompressedPubkeyError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("segwit public keys must always be compressed") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for UncompressedPubkeyError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +} + #[cfg(test)] mod tests { use std::str::FromStr; @@ -826,7 +840,7 @@ mod tests { pk.wpubkey_hash().unwrap().to_string(), "9511aa27ef39bbfa4e4f3dd15f4d66ea57f475b4" ); - assert_eq!(upk.wpubkey_hash(), None); + assert!(upk.wpubkey_hash().is_err()); } #[cfg(feature = "serde")] From 923ce7402deb0a06d0529e0e9f878291c5c8284e Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 4 May 2023 12:03:25 +1000 Subject: [PATCH 3/4] Remove Network from AddressInner An `AddressInner` struct is created when parsing address strings however address strings do not map 1:1 to `Network` because signet and testnet use the same bech32 prefix "tb". We can fix this by inlining the `Payload` variants into `AddressInner` and adding prefix enums for legacy addresses and a `KnownHrp` for bech32 addresses. Also enables removing the `AddressEncoding` struct as we can display the `AddressInner` struct directly. (The `Display` impl is on `AddressInner` and not directly on address to ignore the `NetworkValidation` wrapper, may be able to be simplified still further.) --- bitcoin/src/address/error.rs | 45 +- bitcoin/src/address/mod.rs | 677 +++++++++--------- bitcoin/src/blockdata/script/owned.rs | 2 +- .../src/blockdata/script/witness_program.rs | 65 +- bitcoin/src/crypto/key.rs | 4 +- bitcoin/src/sign_message.rs | 6 +- bitcoin/tests/serde.rs | 2 +- 7 files changed, 434 insertions(+), 367 deletions(-) diff --git a/bitcoin/src/address/error.rs b/bitcoin/src/address/error.rs index fd35ed33..bb46a899 100644 --- a/bitcoin/src/address/error.rs +++ b/bitcoin/src/address/error.rs @@ -6,7 +6,6 @@ use internals::write_err; use crate::address::{Address, NetworkUnchecked}; use crate::blockdata::script::{witness_program, witness_version}; -use crate::crypto::key; use crate::prelude::String; use crate::{base58, Network}; @@ -18,8 +17,6 @@ pub enum Error { WitnessVersion(witness_version::TryFromError), /// A witness program error. WitnessProgram(witness_program::Error), - /// An uncompressed pubkey was used where it is not allowed. - UncompressedPubkey(key::UncompressedPubkeyError), /// Address size more than 520 bytes is not allowed. ExcessiveScriptSize, /// Script is not a p2pkh, p2sh or witness program. @@ -28,11 +25,11 @@ pub enum Error { NetworkValidation { /// Network that was required. required: Network, - /// Network on which the address was found to be valid. - found: Network, /// The address itself address: Address, }, + /// Unknown hrp for current bitcoin networks (in bech32 address). + UnknownHrp(UnknownHrpError), } impl fmt::Display for Error { @@ -42,18 +39,14 @@ impl fmt::Display for Error { match *self { WitnessVersion(ref e) => write_err!(f, "witness version construction error"; e), WitnessProgram(ref e) => write_err!(f, "witness program error"; e), - UncompressedPubkey(ref e) => write_err!(f, "uncompressed pubkey"; e), ExcessiveScriptSize => write!(f, "script size exceed 520 bytes"), UnrecognizedScript => write!(f, "script is not a p2pkh, p2sh or witness program"), - NetworkValidation { required, found, ref address } => { + NetworkValidation { required, ref address } => { write!(f, "address ")?; address.fmt_internal(f)?; // Using fmt_internal in order to remove the "Address(..)" wrapper - write!( - f, - " belongs to network {} which is different from required {}", - found, required - ) + write!(f, " is not valid on {}", required) } + Error::UnknownHrp(ref e) => write_err!(f, "unknown hrp"; e), } } } @@ -66,16 +59,12 @@ impl std::error::Error for Error { match self { WitnessVersion(e) => Some(e), WitnessProgram(e) => Some(e), - UncompressedPubkey(e) => Some(e), + UnknownHrp(e) => Some(e), ExcessiveScriptSize | UnrecognizedScript | NetworkValidation { .. } => None, } } } -impl From for Error { - fn from(e: key::UncompressedPubkeyError) -> Self { Self::UncompressedPubkey(e) } -} - impl From for Error { fn from(e: witness_version::TryFromError) -> Error { Error::WitnessVersion(e) } } @@ -112,6 +101,8 @@ pub enum ParseError { WitnessVersion(witness_version::TryFromError), /// A witness program error. WitnessProgram(witness_program::Error), + /// Tried to parse an unknown HRP. + UnknownHrp(UnknownHrpError), } impl fmt::Display for ParseError { @@ -123,6 +114,7 @@ impl fmt::Display for ParseError { Bech32(ref e) => write_err!(f, "bech32 segwit decoding error"; e), WitnessVersion(ref e) => write_err!(f, "witness version conversion/parsing error"; e), WitnessProgram(ref e) => write_err!(f, "witness program error"; e), + UnknownHrp(ref e) => write_err!(f, "tried to parse an unknown hrp"; e), } } } @@ -137,6 +129,7 @@ impl std::error::Error for ParseError { Bech32(ref e) => Some(e), WitnessVersion(ref e) => Some(e), WitnessProgram(ref e) => Some(e), + UnknownHrp(ref e) => Some(e), } } } @@ -156,3 +149,21 @@ impl From for ParseError { impl From for ParseError { fn from(e: witness_program::Error) -> Self { Self::WitnessProgram(e) } } + +impl From for ParseError { + fn from(e: UnknownHrpError) -> Self { Self::UnknownHrp(e) } +} + +/// Unknown HRP error. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct UnknownHrpError(pub String); + +impl fmt::Display for UnknownHrpError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "unknown hrp: {}", self.0) } +} + +#[cfg(feature = "std")] +impl std::error::Error for UnknownHrpError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +} diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs index ec6b226e..47fd4513 100644 --- a/bitcoin/src/address/mod.rs +++ b/bitcoin/src/address/mod.rs @@ -33,7 +33,7 @@ use core::fmt; use core::marker::PhantomData; use core::str::FromStr; -use bech32::primitives::hrp::{self, Hrp}; +use bech32::primitives::hrp::Hrp; use hashes::{sha256, Hash, HashEngine}; use secp256k1::{Secp256k1, Verification, XOnlyPublicKey}; @@ -44,17 +44,16 @@ use crate::blockdata::constants::{ }; use crate::blockdata::script::witness_program::WitnessProgram; use crate::blockdata::script::witness_version::WitnessVersion; -use crate::blockdata::script::{self, Script, ScriptBuf, ScriptHash}; -use crate::crypto::key::{PubkeyHash, PublicKey, TapTweak, TweakedPublicKey, UntweakedPublicKey}; +use crate::blockdata::script::{self, PushBytesBuf, Script, ScriptBuf, ScriptHash}; +use crate::crypto::key::{self, PubkeyHash, PublicKey, TweakedPublicKey, UntweakedPublicKey}; use crate::network::Network; use crate::prelude::*; -use crate::script::PushBytesBuf; use crate::taproot::TapNodeHash; #[rustfmt::skip] // Keep public re-exports separate. #[doc(inline)] pub use self::{ - error::{Error, ParseError, UnknownAddressTypeError}, + error::{Error, ParseError, UnknownAddressTypeError, UnknownHrpError}, }; /// The different types of addresses. @@ -99,61 +98,6 @@ impl FromStr for AddressType { } } -/// The method used to produce an address. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -enum Payload { - /// P2PKH address. - PubkeyHash(PubkeyHash), - /// P2SH address. - ScriptHash(ScriptHash), - /// Segwit address. - WitnessProgram(WitnessProgram), -} - -/// A utility struct to encode an address payload with the given parameters. -/// This is a low-level utility struct. Consider using `Address` instead. -struct AddressEncoding<'a> { - /// The address payload to encode. - payload: &'a Payload, - /// base58 version byte for p2pkh payloads (e.g. 0x00 for "1..." addresses). - p2pkh_prefix: u8, - /// base58 version byte for p2sh payloads (e.g. 0x05 for "3..." addresses). - p2sh_prefix: u8, - /// The bech32 human-readable part. - hrp: Hrp, -} - -/// Formats bech32 as upper case if alternate formatting is chosen (`{:#}`). -impl<'a> fmt::Display for AddressEncoding<'a> { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self.payload { - Payload::PubkeyHash(hash) => { - let mut prefixed = [0; 21]; - prefixed[0] = self.p2pkh_prefix; - prefixed[1..].copy_from_slice(&hash[..]); - base58::encode_check_to_fmt(fmt, &prefixed[..]) - } - Payload::ScriptHash(hash) => { - let mut prefixed = [0; 21]; - prefixed[0] = self.p2sh_prefix; - prefixed[1..].copy_from_slice(&hash[..]); - base58::encode_check_to_fmt(fmt, &prefixed[..]) - } - Payload::WitnessProgram(witness_program) => { - let hrp = &self.hrp; - let version = witness_program.version().to_fe(); - let program = witness_program.program().as_bytes(); - - if fmt.alternate() { - bech32::segwit::encode_upper_to_fmt_unchecked(fmt, hrp, version, program) - } else { - bech32::segwit::encode_lower_to_fmt_unchecked(fmt, hrp, version, program) - } - } - } - } -} - mod sealed { pub trait NetworkValidation {} impl NetworkValidation for super::NetworkChecked {} @@ -189,10 +133,148 @@ impl NetworkValidation for NetworkUnchecked { /// An `Address` is composed of a payload and a network. This struct represents the inner /// representation of an address without the network validation tag, which is used to ensure that /// addresses are used only on the appropriate network. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct AddressInner { - payload: Payload, - network: Network, +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum AddressInner { + P2pkh { hash: PubkeyHash, prefix: LegacyP2pkhPrefix }, + P2sh { hash: ScriptHash, prefix: LegacyP2shPrefix }, + Segwit { program: WitnessProgram, hrp: KnownHrp }, +} + +/// Formats bech32 as upper case if alternate formatting is chosen (`{:#}`). +impl fmt::Display for AddressInner { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + use AddressInner::*; + match self { + P2pkh { hash, prefix } => { + let mut prefixed = [0; 21]; + prefixed[0] = prefix.to_u8(); + prefixed[1..].copy_from_slice(&hash[..]); + base58::encode_check_to_fmt(fmt, &prefixed[..]) + } + P2sh { hash, prefix } => { + let mut prefixed = [0; 21]; + prefixed[0] = prefix.to_u8(); + prefixed[1..].copy_from_slice(&hash[..]); + base58::encode_check_to_fmt(fmt, &prefixed[..]) + } + Segwit { program, hrp } => { + let hrp = hrp.to_hrp(); + let version = program.version().to_fe(); + let program = program.program().as_ref(); + + if fmt.alternate() { + bech32::segwit::encode_upper_to_fmt_unchecked(fmt, &hrp, version, program) + } else { + bech32::segwit::encode_lower_to_fmt_unchecked(fmt, &hrp, version, program) + } + } + } + } +} + +/// Prefix byte used for legacy P2PKH addresses. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum LegacyP2pkhPrefix { + /// Prefix used for legacy addresses on the main Bitcoin network. + Mainnet, + /// Prefix used for legacy addresses on all other test networks (testnet, signet, regtest). + AllTestnets, +} + +impl LegacyP2pkhPrefix { + /// Creates a legacy prefix from the associated `network`. + fn from_network(network: Network) -> Self { + use Network::*; + + match network { + Bitcoin => Self::Mainnet, + Signet | Testnet | Regtest => Self::AllTestnets, + } + } + + /// Converts this prefix enum to the respective byte value. + fn to_u8(self) -> u8 { + match self { + Self::Mainnet => PUBKEY_ADDRESS_PREFIX_MAIN, + Self::AllTestnets => PUBKEY_ADDRESS_PREFIX_TEST, + } + } +} + +/// Prefix byte used for legacy P2SH addresses. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum LegacyP2shPrefix { + /// Prefix used for legacy addresses on the main Bitcoin network. + Mainnet, + /// Prefix used for legacy addresses on all other test networks (testnet, signet, regtest). + AllTestnets, +} + +impl LegacyP2shPrefix { + /// Creates a legacy prefix from the associated `network`. + fn from_network(network: Network) -> Self { + use Network::*; + + match network { + Bitcoin => Self::Mainnet, + Signet | Testnet | Regtest => Self::AllTestnets, + } + } + + /// Converts this prefix enum to the respective byte value. + fn to_u8(self) -> u8 { + match self { + Self::Mainnet => SCRIPT_ADDRESS_PREFIX_MAIN, + Self::AllTestnets => SCRIPT_ADDRESS_PREFIX_TEST, + } + } +} + +/// Known bech32 human-readable parts. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +enum KnownHrp { + /// The main Bitcoin network. + Mainnet, + /// The test networks, testnet and signet. + Testnets, + /// The regtest network. + Regtest, +} + +impl KnownHrp { + /// Creates a `KnownHrp` from `network`. + fn from_network(network: Network) -> Self { + use Network::*; + + match network { + Bitcoin => Self::Mainnet, + Testnet | Signet => Self::Testnets, + Regtest => Self::Regtest, + } + } + + /// Creates a `KnownHrp` from a [`bech32::Hrp`]. + fn from_hrp(hrp: Hrp) -> Result { + if hrp == bech32::hrp::BC { + Ok(Self::Mainnet) + } else if hrp.is_valid_on_testnet() || hrp.is_valid_on_signet() { + Ok(Self::Testnets) + } else if hrp == bech32::hrp::BCRT { + Ok(Self::Regtest) + } else { + Err(UnknownHrpError(hrp.to_lowercase())) + } + } + + /// Converts, infallibly a known HRP to a [`bech32::Hrp`]. + fn to_hrp(self) -> Hrp { + match self { + Self::Mainnet => bech32::hrp::BC, + Self::Testnets => bech32::hrp::TB, + Self::Regtest => bech32::hrp::BCRT, + } + } } /// A Bitcoin address. @@ -317,79 +399,15 @@ impl serde::Serialize for Address { /// Methods on [`Address`] that can be called on both `Address` and /// `Address`. impl Address { - /// Returns a reference to the payload of this address. - fn payload(&self) -> &Payload { &self.0.payload } - - /// Returns a reference to the network of this address. - pub fn network(&self) -> &Network { &self.0.network } - /// Returns a reference to the unchecked address, which is dangerous to use if the address /// is invalid in the context of `NetworkUnchecked`. pub fn as_unchecked(&self) -> &Address { unsafe { &*(self as *const Address as *const Address) } } - /// Extracts and returns the network and payload components of the `Address`. - fn into_parts(self) -> (Network, Payload) { - let AddressInner { payload, network } = self.0; - (network, payload) - } - - /// Gets the address type of the address. - /// - /// This method is publicly available as [`address_type`](Address::address_type) - /// on `Address` but internally can be called on `Address` as - /// `address_type_internal`. - /// - /// # Returns - /// None if unknown, non-standard or related to the future witness version. - fn address_type_internal(&self) -> Option { - match self.payload() { - Payload::PubkeyHash(_) => Some(AddressType::P2pkh), - Payload::ScriptHash(_) => Some(AddressType::P2sh), - Payload::WitnessProgram(ref prog) => { - // BIP-141 p2wpkh or p2wsh addresses. - match prog.version() { - WitnessVersion::V0 => match prog.program().len() { - 20 => Some(AddressType::P2wpkh), - 32 => Some(AddressType::P2wsh), - _ => unreachable!( - "Address creation invariant violation: invalid program length" - ), - }, - WitnessVersion::V1 if prog.program().len() == 32 => Some(AddressType::P2tr), - _ => None, - } - } - } - } - /// Format the address for the usage by `Debug` and `Display` implementations. fn fmt_internal(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - let p2pkh_prefix = match self.network() { - Network::Bitcoin => PUBKEY_ADDRESS_PREFIX_MAIN, - Network::Testnet | Network::Signet | Network::Regtest => PUBKEY_ADDRESS_PREFIX_TEST, - }; - let p2sh_prefix = match self.network() { - Network::Bitcoin => SCRIPT_ADDRESS_PREFIX_MAIN, - Network::Testnet | Network::Signet | Network::Regtest => SCRIPT_ADDRESS_PREFIX_TEST, - }; - let hrp = match self.network() { - Network::Bitcoin => hrp::BC, - Network::Testnet | Network::Signet => hrp::TB, - Network::Regtest => hrp::BCRT, - }; - let encoding = AddressEncoding { payload: self.payload(), p2pkh_prefix, p2sh_prefix, hrp }; - - use fmt::Display; - - encoding.fmt(fmt) - } - - /// Create new address from given components, infering the network validation - /// marker type of the address. - fn new(network: Network, payload: Payload) -> Self { - Self(AddressInner { network, payload }, PhantomData) + fmt::Display::fmt(&self.0, fmt) } } @@ -399,9 +417,10 @@ impl Address { /// /// This is the preferred non-witness type address. #[inline] - pub fn p2pkh(pk: &PublicKey, network: Network) -> Address { - let payload = Payload::PubkeyHash(pk.pubkey_hash()); - Address(AddressInner { network, payload }, PhantomData) + pub fn p2pkh(pk: impl Into, network: Network) -> Address { + let hash = pk.into(); + let prefix = LegacyP2pkhPrefix::from_network(network); + Self(AddressInner::P2pkh { hash, prefix }, PhantomData) } /// Creates a pay to script hash P2SH address from a script. @@ -413,50 +432,52 @@ impl Address { if script.len() > MAX_SCRIPT_ELEMENT_SIZE { return Err(Error::ExcessiveScriptSize); } - let payload = Payload::ScriptHash(script.script_hash()); - Ok(Address(AddressInner { network, payload }, PhantomData)) + let hash = script.script_hash(); + Ok(Address::p2sh_from_hash(hash, network)) + } + + // This is intentionally not public so we enforce script length checks. + fn p2sh_from_hash(hash: ScriptHash, network: Network) -> Address { + let prefix = LegacyP2shPrefix::from_network(network); + Self(AddressInner::P2sh { hash, prefix }, PhantomData) } /// Creates a witness pay to public key address from a public key. /// /// This is the native segwit address type for an output redeemable with a single signature. - /// - /// # Errors - /// Will only return an error if an uncompressed public key is provided. - pub fn p2wpkh(pk: &PublicKey, network: Network) -> Result { - let prog = WitnessProgram::new(WitnessVersion::V0, pk.wpubkey_hash()?)?; - let payload = Payload::WitnessProgram(prog); - Ok(Address(AddressInner { network, payload }, PhantomData)) + pub fn p2wpkh( + pk: &PublicKey, + network: Network, + ) -> Result { + let program = WitnessProgram::p2wpkh(pk)?; + Ok(Address::from_witness_program(program, network)) } /// Creates a pay to script address that embeds a witness pay to public key. /// /// This is a segwit address type that looks familiar (as p2sh) to legacy clients. - /// - /// # Errors - /// Will only return an Error if an uncompressed public key is provided. - pub fn p2shwpkh(pk: &PublicKey, network: Network) -> Result { + pub fn p2shwpkh( + pk: &PublicKey, + network: Network, + ) -> Result { let builder = script::Builder::new().push_int(0).push_slice(pk.wpubkey_hash()?); - - let payload = Payload::ScriptHash(builder.into_script().script_hash()); - Ok(Address(AddressInner { network, payload }, PhantomData)) + let script_hash = builder.as_script().script_hash(); + Ok(Address::p2sh_from_hash(script_hash, network)) } /// Creates a witness pay to script hash address. pub fn p2wsh(script: &Script, network: Network) -> Address { - let prog = WitnessProgram::new(WitnessVersion::V0, script.wscript_hash()) - .expect("wscript_hash has len 32 compatible with segwitv0"); - let payload = Payload::WitnessProgram(prog); - Address(AddressInner { network, payload }, PhantomData) + let program = WitnessProgram::p2wsh(script); + Address::from_witness_program(program, network) } /// Creates a pay to script address that embeds a witness pay to script hash address. /// /// This is a segwit address type that looks familiar (as p2sh) to legacy clients. pub fn p2shwsh(script: &Script, network: Network) -> Address { - let ws = script::Builder::new().push_int(0).push_slice(script.wscript_hash()).into_script(); - let payload = Payload::ScriptHash(ws.script_hash()); - Address(AddressInner { network, payload }, PhantomData) + let builder = script::Builder::new().push_int(0).push_slice(script.wscript_hash()); + let script_hash = builder.as_script().script_hash(); + Address::p2sh_from_hash(script_hash, network) } /// Creates a pay to taproot address from an untweaked key. @@ -466,19 +487,14 @@ impl Address { merkle_root: Option, network: Network, ) -> Address { - let (output_key, _parity) = internal_key.tap_tweak(secp, merkle_root); - let prog = WitnessProgram::new(WitnessVersion::V1, output_key.to_inner().serialize()) - .expect("taproot output key has len 32 <= 40"); - let payload = Payload::WitnessProgram(prog); - Address(AddressInner { network, payload }, PhantomData) + let program = WitnessProgram::p2tr(secp, internal_key, merkle_root); + Address::from_witness_program(program, network) } /// Creates a pay to taproot address from a pre-tweaked output key. pub fn p2tr_tweaked(output_key: TweakedPublicKey, network: Network) -> Address { - let prog = WitnessProgram::new(WitnessVersion::V1, output_key.to_inner().serialize()) - .expect("taproot output key has len 32 <= 40"); - let payload = Payload::WitnessProgram(prog); - Address(AddressInner { network, payload }, PhantomData) + let program = WitnessProgram::p2tr_tweaked(output_key); + Address::from_witness_program(program, network) } /// Creates an address from an arbitrary witness program. @@ -486,16 +502,53 @@ impl Address { /// This only exists to support future witness versions. If you are doing normal mainnet things /// then you likely do not need this constructor. pub fn from_witness_program(program: WitnessProgram, network: Network) -> Address { - let inner = AddressInner { payload: Payload::WitnessProgram(program), network }; + let hrp = KnownHrp::from_network(network); + let inner = AddressInner::Segwit { program, hrp }; Address(inner, PhantomData) } /// Gets the address type of the address. /// /// # Returns + /// /// None if unknown, non-standard or related to the future witness version. #[inline] - pub fn address_type(&self) -> Option { self.address_type_internal() } + pub fn address_type(&self) -> Option { + match self.0 { + AddressInner::P2pkh { .. } => Some(AddressType::P2pkh), + AddressInner::P2sh { .. } => Some(AddressType::P2sh), + AddressInner::Segwit { ref program, hrp: _ } => + if program.is_p2wpkh() { + Some(AddressType::P2wpkh) + } else if program.is_p2wsh() { + Some(AddressType::P2wsh) + } else if program.is_p2tr() { + Some(AddressType::P2tr) + } else { + None + }, + } + } + + /// Gets the pubkey hash for this address if this is a P2PKH address. + pub fn pubkey_hash(&self) -> Option { + use AddressInner::*; + + match self.0 { + P2pkh { ref hash, prefix: _ } => Some(*hash), + _ => None, + } + } + + /// Gets the script hash for this address if this is a P2SH address. + pub fn script_hash(&self) -> Option { + use AddressInner::*; + + match self.0 { + P2sh { ref hash, prefix: _ } => Some(*hash), + _ => None, + } + } /// Checks whether or not the address is following Bitcoin standardness rules when /// *spending* from this address. *NOT* to be called by senders. @@ -514,32 +567,34 @@ impl Address { /// Constructs an [`Address`] from an output script (`scriptPubkey`). pub fn from_script(script: &Script, network: Network) -> Result { - let payload = if script.is_p2pkh() { + if script.is_p2pkh() { let bytes = script.as_bytes()[3..23].try_into().expect("statically 20B long"); - Payload::PubkeyHash(PubkeyHash::from_byte_array(bytes)) + let hash = PubkeyHash::from_byte_array(bytes); + Ok(Address::p2pkh(hash, network)) } else if script.is_p2sh() { let bytes = script.as_bytes()[2..22].try_into().expect("statically 20B long"); - Payload::ScriptHash(ScriptHash::from_byte_array(bytes)) + let hash = ScriptHash::from_byte_array(bytes); + Ok(Address::p2sh_from_hash(hash, network)) } else if script.is_witness_program() { - let opcode = script.first_opcode().expect("witness_version guarantees len() > 4"); + let opcode = script.first_opcode().expect("is_witness_program guarantees len > 4"); - let witness_program = script.as_bytes()[2..].to_vec(); - - let witness_program = - WitnessProgram::new(WitnessVersion::try_from(opcode)?, witness_program)?; - Payload::WitnessProgram(witness_program) + let buf = PushBytesBuf::try_from(script.as_bytes()[2..].to_vec()) + .expect("is_witness_program guarantees len <= 42 bytes"); + let version = WitnessVersion::try_from(opcode)?; + let program = WitnessProgram::new(version, buf)?; + Ok(Address::from_witness_program(program, network)) } else { - return Err(Error::UnrecognizedScript); - }; - Ok(Address(AddressInner { network, payload }, PhantomData)) + Err(Error::UnrecognizedScript) + } } /// Generates a script pubkey spending to this address. pub fn script_pubkey(&self) -> ScriptBuf { - match self.payload() { - Payload::PubkeyHash(ref hash) => ScriptBuf::new_p2pkh(hash), - Payload::ScriptHash(ref hash) => ScriptBuf::new_p2sh(hash), - Payload::WitnessProgram(ref prog) => ScriptBuf::new_witness_program(prog), + use AddressInner::*; + match self.0 { + P2pkh { ref hash, prefix: _ } => ScriptBuf::new_p2pkh(hash), + P2sh { ref hash, prefix: _ } => ScriptBuf::new_p2sh(hash), + Segwit { ref program, hrp: _ } => ScriptBuf::new_witness_program(program), } } @@ -579,7 +634,7 @@ impl Address { /// given key. For taproot addresses, the supplied key is assumed to be tweaked pub fn is_related_to_pubkey(&self, pubkey: &PublicKey) -> bool { let pubkey_hash = pubkey.pubkey_hash(); - let payload = self.inner_prog_as_bytes(); + let payload = self.payload_as_bytes(); let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner); (*pubkey_hash.as_byte_array() == *payload) @@ -592,31 +647,38 @@ impl Address { /// This will only work for Taproot addresses. The Public Key is /// assumed to have already been tweaked. pub fn is_related_to_xonly_pubkey(&self, xonly_pubkey: &XOnlyPublicKey) -> bool { - let payload = self.inner_prog_as_bytes(); - payload == xonly_pubkey.serialize() + xonly_pubkey.serialize() == *self.payload_as_bytes() } /// Returns true if the address creates a particular script /// This function doesn't make any allocations. - pub fn matches_script_pubkey(&self, script_pubkey: &Script) -> bool { - match &self.payload() { - Payload::PubkeyHash(ref hash) if script_pubkey.is_p2pkh() => - &script_pubkey.as_bytes()[3..23] == >::as_ref(hash), - Payload::ScriptHash(ref hash) if script_pubkey.is_p2sh() => - &script_pubkey.as_bytes()[2..22] == >::as_ref(hash), - Payload::WitnessProgram(ref prog) if script_pubkey.is_witness_program() => - &script_pubkey.as_bytes()[2..] == prog.program().as_bytes(), - Payload::PubkeyHash(_) | Payload::ScriptHash(_) | Payload::WitnessProgram(_) => false, + pub fn matches_script_pubkey(&self, script: &Script) -> bool { + use AddressInner::*; + match self.0 { + P2pkh { ref hash, prefix: _ } if script.is_p2pkh() => + &script.as_bytes()[3..23] == >::as_ref(hash), + P2sh { ref hash, prefix: _ } if script.is_p2sh() => + &script.as_bytes()[2..22] == >::as_ref(hash), + Segwit { ref program, hrp: _ } if script.is_witness_program() => + &script.as_bytes()[2..] == program.program().as_bytes(), + P2pkh { .. } | P2sh { .. } | Segwit { .. } => false, } } - /// Returns a byte slice of the inner program of the payload. If the payload - /// is a script hash or pubkey hash, a reference to the hash is returned. - fn inner_prog_as_bytes(&self) -> &[u8] { - match &self.payload() { - Payload::ScriptHash(hash) => hash.as_ref(), - Payload::PubkeyHash(hash) => hash.as_ref(), - Payload::WitnessProgram(prog) => prog.program().as_bytes(), + /// Returns the "payload" for this address. + /// + /// The "payload" is the useful stuff excluding serialization prefix, the exact payload is + /// dependent on the inner address: + /// + /// - For p2sh, the payload is the script hash. + /// - For p2pkh, the payload is the pubkey hash. + /// - For segwit addresses, the payload is the witness program. + fn payload_as_bytes(&self) -> &[u8] { + use AddressInner::*; + match self.0 { + P2sh { ref hash, prefix: _ } => hash.as_ref(), + P2pkh { ref hash, prefix: _ } => hash.as_ref(), + Segwit { ref program, hrp: _ } => program.program().as_bytes(), } } } @@ -650,16 +712,11 @@ impl Address { /// assert_eq!(address.is_valid_for_network(Network::Testnet), false); /// ``` pub fn is_valid_for_network(&self, network: Network) -> bool { - let is_legacy = matches!( - self.address_type_internal(), - Some(AddressType::P2pkh) | Some(AddressType::P2sh) - ); - - match (self.network(), network) { - (a, b) if *a == b => true, - (Network::Bitcoin, _) | (_, Network::Bitcoin) => false, - (Network::Regtest, _) | (_, Network::Regtest) if !is_legacy => false, - (Network::Testnet, _) | (Network::Regtest, _) | (Network::Signet, _) => true, + use AddressInner::*; + match self.0 { + P2pkh { hash: _, ref prefix } => *prefix == LegacyP2pkhPrefix::from_network(network), + P2sh { hash: _, ref prefix } => *prefix == LegacyP2shPrefix::from_network(network), + Segwit { program: _, ref hrp } => *hrp == KnownHrp::from_network(network), } } @@ -672,7 +729,7 @@ impl Address { if self.is_valid_for_network(required) { Ok(self.assume_checked()) } else { - Err(Error::NetworkValidation { found: *self.network(), required, address: self }) + Err(Error::NetworkValidation { required, address: self }) } } @@ -684,22 +741,17 @@ impl Address { /// on [`Address`]. #[inline] pub fn assume_checked(self) -> Address { - let (network, payload) = self.into_parts(); - Address::new(network, payload) - } -} + use AddressInner::*; -// For NetworkUnchecked , it compare Addresses and if network and payload matches then return true. -impl PartialEq> for Address { - fn eq(&self, other: &Address) -> bool { - self.network() == other.network() && self.payload() == other.payload() + let inner = match self.0 { + P2pkh { hash, prefix } => P2pkh { hash, prefix }, + P2sh { hash, prefix } => P2sh { hash, prefix }, + Segwit { program, hrp } => Segwit { program, hrp }, + }; + Address(inner, PhantomData) } } -impl PartialEq
for Address { - fn eq(&self, other: &Address) -> bool { other == self } -} - impl From
for script::ScriptBuf { fn from(a: Address) -> Self { a.script_pubkey() } } @@ -722,41 +774,24 @@ impl fmt::Debug for Address { } } -/// Extracts the bech32 prefix. -/// -/// # Returns -/// The input slice if no prefix is found. -fn find_bech32_prefix(bech32: &str) -> &str { - // Split at the last occurrence of the separator character '1'. - match bech32.rfind('1') { - None => bech32, - Some(sep) => bech32.split_at(sep).0, - } -} - /// Address can be parsed only with `NetworkUnchecked`. impl FromStr for Address { type Err = ParseError; - fn from_str(s: &str) -> Result { - // try bech32 - let bech32_network = match find_bech32_prefix(s) { - // note that upper or lowercase is allowed but NOT mixed case - "bc" | "BC" => Some(Network::Bitcoin), - "tb" | "TB" => Some(Network::Testnet), // this may also be signet - "bcrt" | "BCRT" => Some(Network::Regtest), - _ => None, - }; - if let Some(network) = bech32_network { - let (_hrp, version, data) = bech32::segwit::decode(s)?; - let version = WitnessVersion::try_from(version).expect("we know this is in range 0-16"); - let program = PushBytesBuf::try_from(data).expect("decode() guarantees valid length"); - let witness_program = WitnessProgram::new(version, program)?; + fn from_str(s: &str) -> Result, ParseError> { + if let Ok((hrp, witness_version, data)) = bech32::segwit::decode(s) { + let version = WitnessVersion::try_from(witness_version)?; + let buf = PushBytesBuf::try_from(data).expect("bech32 guarantees valid program length"); + let program = WitnessProgram::new(version, buf) + .expect("bech32 guarantees valid program length for witness"); - return Ok(Address::new(network, Payload::WitnessProgram(witness_program))); + let hrp = KnownHrp::from_hrp(hrp)?; + let inner = AddressInner::Segwit { program, hrp }; + return Ok(Address(inner, PhantomData)); } - // Base58 + // If segwit decoding fails, assume its a legacy address. + if s.len() > 50 { return Err(ParseError::Base58(base58::Error::InvalidLength(s.len() * 11 / 15))); } @@ -765,19 +800,30 @@ impl FromStr for Address { return Err(ParseError::Base58(base58::Error::InvalidLength(data.len()))); } - let (network, payload) = match data[0] { - PUBKEY_ADDRESS_PREFIX_MAIN => - (Network::Bitcoin, Payload::PubkeyHash(PubkeyHash::from_slice(&data[1..]).unwrap())), - SCRIPT_ADDRESS_PREFIX_MAIN => - (Network::Bitcoin, Payload::ScriptHash(ScriptHash::from_slice(&data[1..]).unwrap())), - PUBKEY_ADDRESS_PREFIX_TEST => - (Network::Testnet, Payload::PubkeyHash(PubkeyHash::from_slice(&data[1..]).unwrap())), - SCRIPT_ADDRESS_PREFIX_TEST => - (Network::Testnet, Payload::ScriptHash(ScriptHash::from_slice(&data[1..]).unwrap())), + let (prefix, data) = data.split_first().expect("length checked above"); + let data: [u8; 20] = data.try_into().expect("length checked above"); + + let inner = match *prefix { + PUBKEY_ADDRESS_PREFIX_MAIN => { + let hash = PubkeyHash::from_byte_array(data); + AddressInner::P2pkh { hash, prefix: LegacyP2pkhPrefix::Mainnet } + } + PUBKEY_ADDRESS_PREFIX_TEST => { + let hash = PubkeyHash::from_byte_array(data); + AddressInner::P2pkh { hash, prefix: LegacyP2pkhPrefix::AllTestnets } + } + SCRIPT_ADDRESS_PREFIX_MAIN => { + let hash = ScriptHash::from_byte_array(data); + AddressInner::P2sh { hash, prefix: LegacyP2shPrefix::Mainnet } + } + SCRIPT_ADDRESS_PREFIX_TEST => { + let hash = ScriptHash::from_byte_array(data); + AddressInner::P2sh { hash, prefix: LegacyP2shPrefix::AllTestnets } + } x => return Err(ParseError::Base58(base58::Error::InvalidAddressVersion(x))), }; - Ok(Address::new(network, payload)) + Ok(Address(inner, PhantomData)) } } @@ -800,7 +846,7 @@ mod tests { use crate::crypto::key::{PublicKey, UncompressedPubkeyError}; use crate::network::Network::{Bitcoin, Testnet}; - fn roundtrips(addr: &Address) { + fn roundtrips(addr: &Address, network: Network) { assert_eq!( Address::from_str(&addr.to_string()).unwrap().assume_checked(), *addr, @@ -808,8 +854,9 @@ mod tests { addr, ); assert_eq!( - Address::from_script(&addr.script_pubkey(), *addr.network()).as_ref(), - Ok(addr), + Address::from_script(&addr.script_pubkey(), network) + .expect("failed to create inner address from script_pubkey"), + *addr, "script round-trip failed for {}", addr, ); @@ -825,10 +872,8 @@ mod tests { #[test] fn test_p2pkh_address_58() { - let addr = Address::new( - Bitcoin, - Payload::PubkeyHash("162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse().unwrap()), - ); + let hash = "162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse::().unwrap(); + let addr = Address::p2pkh(hash, Bitcoin); assert_eq!( addr.script_pubkey(), @@ -836,30 +881,28 @@ mod tests { ); assert_eq!(&addr.to_string(), "132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM"); assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); - roundtrips(&addr); + roundtrips(&addr, Bitcoin); } #[test] fn test_p2pkh_from_key() { let key = "048d5141948c1702e8c95f438815794b87f706a8d4cd2bffad1dc1570971032c9b6042a0431ded2478b5c9cf2d81c124a5e57347a3c63ef0e7716cf54d613ba183".parse::().unwrap(); - let addr = Address::p2pkh(&key, Bitcoin); + let addr = Address::p2pkh(key, Bitcoin); assert_eq!(&addr.to_string(), "1QJVDzdqb1VpbDK7uDeyVXy9mR27CJiyhY"); let key = "03df154ebfcf29d29cc10d5c2565018bce2d9edbab267c31d2caf44a63056cf99f" .parse::() .unwrap(); - let addr = Address::p2pkh(&key, Testnet); + let addr = Address::p2pkh(key, Testnet); assert_eq!(&addr.to_string(), "mqkhEMH6NCeYjFybv7pvFC22MFeaNT9AQC"); assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); - roundtrips(&addr); + roundtrips(&addr, Testnet); } #[test] fn test_p2sh_address_58() { - let addr = Address::new( - Bitcoin, - Payload::ScriptHash("162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse().unwrap()), - ); + let hash = "162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse::().unwrap(); + let addr = Address::p2sh_from_hash(hash, Bitcoin); assert_eq!( addr.script_pubkey(), @@ -867,7 +910,7 @@ mod tests { ); assert_eq!(&addr.to_string(), "33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k"); assert_eq!(addr.address_type(), Some(AddressType::P2sh)); - roundtrips(&addr); + roundtrips(&addr, Bitcoin); } #[test] @@ -876,7 +919,7 @@ mod tests { let addr = Address::p2sh(&script, Testnet).unwrap(); assert_eq!(&addr.to_string(), "2N3zXjbwdTcPsJiy8sUK9FhWJhqQCxA8Jjr"); assert_eq!(addr.address_type(), Some(AddressType::P2sh)); - roundtrips(&addr); + roundtrips(&addr, Testnet); } #[test] @@ -894,11 +937,11 @@ mod tests { let addr = Address::p2wpkh(&key, Bitcoin).unwrap(); assert_eq!(&addr.to_string(), "bc1qvzvkjn4q3nszqxrv3nraga2r822xjty3ykvkuw"); assert_eq!(addr.address_type(), Some(AddressType::P2wpkh)); - roundtrips(&addr); + roundtrips(&addr, Bitcoin); // Test uncompressed pubkey key.compressed = false; - assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError.into()); + assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError); } #[test] @@ -911,7 +954,7 @@ mod tests { "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej" ); assert_eq!(addr.address_type(), Some(AddressType::P2wsh)); - roundtrips(&addr); + roundtrips(&addr, Bitcoin); } #[test] @@ -923,11 +966,11 @@ mod tests { let addr = Address::p2shwpkh(&key, Bitcoin).unwrap(); assert_eq!(&addr.to_string(), "3QBRmWNqqBGme9er7fMkGqtZtp4gjMFxhE"); assert_eq!(addr.address_type(), Some(AddressType::P2sh)); - roundtrips(&addr); + roundtrips(&addr, Bitcoin); // Test uncompressed pubkey key.compressed = false; - assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError.into()); + assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError); } #[test] @@ -937,18 +980,19 @@ mod tests { let addr = Address::p2shwsh(&script, Bitcoin); assert_eq!(&addr.to_string(), "36EqgNnsWW94SreZgBWc1ANC6wpFZwirHr"); assert_eq!(addr.address_type(), Some(AddressType::P2sh)); - roundtrips(&addr); + roundtrips(&addr, Bitcoin); } #[test] fn test_non_existent_segwit_version() { // 40-byte program - let program = hex!( + let program = PushBytesBuf::from(hex!( "654f6ea368e0acdfd92976b7c2103a1b26313f430654f6ea368e0acdfd92976b7c2103a1b26313f4" - ); - let witness_prog = WitnessProgram::new(WitnessVersion::V13, program.to_vec()).unwrap(); - let addr = Address::new(Bitcoin, Payload::WitnessProgram(witness_prog)); - roundtrips(&addr); + )); + let program = WitnessProgram::new(WitnessVersion::V13, program).expect("valid program"); + + let addr = Address::from_witness_program(program, Bitcoin); + roundtrips(&addr, Bitcoin); } #[test] @@ -1106,57 +1150,6 @@ mod tests { } } - #[test] - fn test_valid_networks() { - let legacy_payload = &[ - Payload::PubkeyHash(PubkeyHash::all_zeros()), - Payload::ScriptHash(ScriptHash::all_zeros()), - ]; - let segwit_payload = (0..=16) - .map(|version| { - Payload::WitnessProgram( - WitnessProgram::new( - WitnessVersion::try_from(version).unwrap(), - vec![0xab; 32], // Choose 32 to make test case valid for all witness versions(including v0) - ) - .unwrap(), - ) - }) - .collect::>(); - - const LEGACY_EQUIVALENCE_CLASSES: &[&[Network]] = - &[&[Network::Bitcoin], &[Network::Testnet, Network::Regtest, Network::Signet]]; - const SEGWIT_EQUIVALENCE_CLASSES: &[&[Network]] = - &[&[Network::Bitcoin], &[Network::Regtest], &[Network::Testnet, Network::Signet]]; - - fn test_addr_type(payloads: &[Payload], equivalence_classes: &[&[Network]]) { - for pl in payloads { - for addr_net in equivalence_classes.iter().flat_map(|ec| ec.iter()) { - for valid_net in equivalence_classes - .iter() - .filter(|ec| ec.contains(addr_net)) - .flat_map(|ec| ec.iter()) - { - let addr = Address::new(*addr_net, pl.clone()); - assert!(addr.is_valid_for_network(*valid_net)); - } - - for invalid_net in equivalence_classes - .iter() - .filter(|ec| !ec.contains(addr_net)) - .flat_map(|ec| ec.iter()) - { - let addr = Address::new(*addr_net, pl.clone()); - assert!(!addr.is_valid_for_network(*invalid_net)); - } - } - } - } - - test_addr_type(legacy_payload, LEGACY_EQUIVALENCE_CLASSES); - test_addr_type(&segwit_payload, SEGWIT_EQUIVALENCE_CLASSES); - } - #[test] fn p2tr_from_untweaked() { //Test case from BIP-086 @@ -1171,7 +1164,7 @@ mod tests { "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr" ); assert_eq!(address.address_type(), Some(AddressType::P2tr)); - roundtrips(&address); + roundtrips(&address, Bitcoin); } #[test] diff --git a/bitcoin/src/blockdata/script/owned.rs b/bitcoin/src/blockdata/script/owned.rs index 904c0137..976caa51 100644 --- a/bitcoin/src/blockdata/script/owned.rs +++ b/bitcoin/src/blockdata/script/owned.rs @@ -140,7 +140,7 @@ impl ScriptBuf { /// Does not do any checks on version or program length. /// /// Convenience method used by `new_p2wpkh`, `new_p2wsh`, `new_p2tr`, and `new_p2tr_tweaked`. - fn new_witness_program_unchecked>( + pub(crate) fn new_witness_program_unchecked>( version: WitnessVersion, program: T, ) -> Self { diff --git a/bitcoin/src/blockdata/script/witness_program.rs b/bitcoin/src/blockdata/script/witness_program.rs index 1b792a51..2c4cf1c4 100644 --- a/bitcoin/src/blockdata/script/witness_program.rs +++ b/bitcoin/src/blockdata/script/witness_program.rs @@ -9,8 +9,13 @@ use core::fmt; +use hashes::Hash as _; +use secp256k1::{Secp256k1, Verification}; + use crate::blockdata::script::witness_version::WitnessVersion; -use crate::blockdata::script::{PushBytes, PushBytesBuf, PushBytesErrorReport}; +use crate::blockdata::script::{PushBytes, PushBytesBuf, PushBytesErrorReport, Script}; +use crate::crypto::key::{self, PublicKey, TapTweak, TweakedPublicKey, UntweakedPublicKey}; +use crate::taproot::TapNodeHash; /// The segregated witness program. /// @@ -46,11 +51,69 @@ impl WitnessProgram { Ok(WitnessProgram { version, program }) } + /// Creates a [`WitnessProgram`] from a 20 byte pubkey hash. + fn new_p2wpkh(program: [u8; 20]) -> Self { + WitnessProgram { version: WitnessVersion::V0, program: program.into() } + } + + /// Creates a [`WitnessProgram`] from a 32 byte script hash. + fn new_p2wsh(program: [u8; 32]) -> Self { + WitnessProgram { version: WitnessVersion::V0, program: program.into() } + } + + /// Creates a [`WitnessProgram`] from a 32 byte serialize taproot xonly pubkey. + fn new_p2tr(program: [u8; 32]) -> Self { + WitnessProgram { version: WitnessVersion::V1, program: program.into() } + } + + /// Creates a [`WitnessProgram`] from `pk` for a P2WPKH output. + pub fn p2wpkh(pk: &PublicKey) -> Result { + let hash = pk.wpubkey_hash()?; + let program = WitnessProgram::new_p2wpkh(hash.to_byte_array()); + Ok(program) + } + + /// Creates a [`WitnessProgram`] from `script` for a P2WSH output. + pub fn p2wsh(script: &Script) -> Self { + let hash = script.wscript_hash(); + WitnessProgram::new_p2wsh(hash.to_byte_array()) + } + + /// Creates a pay to taproot address from an untweaked key. + pub fn p2tr( + secp: &Secp256k1, + internal_key: UntweakedPublicKey, + merkle_root: Option, + ) -> Self { + let (output_key, _parity) = internal_key.tap_tweak(secp, merkle_root); + let pubkey = output_key.to_inner().serialize(); + WitnessProgram::new_p2tr(pubkey) + } + + /// Creates a pay to taproot address from a pre-tweaked output key. + pub fn p2tr_tweaked(output_key: TweakedPublicKey) -> Self { + let pubkey = output_key.to_inner().serialize(); + WitnessProgram::new_p2tr(pubkey) + } + /// Returns the witness program version. pub fn version(&self) -> WitnessVersion { self.version } /// Returns the witness program. pub fn program(&self) -> &PushBytes { &self.program } + + /// Returns true if this witness program is for a P2WPKH output. + pub fn is_p2wpkh(&self) -> bool { + self.version == WitnessVersion::V0 && self.program.len() == 20 + } + + /// Returns true if this witness program is for a P2WPSH output. + pub fn is_p2wsh(&self) -> bool { + self.version == WitnessVersion::V0 && self.program.len() == 32 + } + + /// Returns true if this witness program is for a P2TR output. + pub fn is_p2tr(&self) -> bool { self.version == WitnessVersion::V1 && self.program.len() == 32 } } /// Witness program error. diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs index abb1ea96..89509b66 100644 --- a/bitcoin/src/crypto/key.rs +++ b/bitcoin/src/crypto/key.rs @@ -781,7 +781,7 @@ mod tests { assert_eq!(&sk.to_wif(), "cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"); let secp = Secp256k1::new(); - let pk = Address::p2pkh(&sk.public_key(&secp), sk.network); + let pk = Address::p2pkh(sk.public_key(&secp), sk.network); assert_eq!(&pk.to_string(), "mqwpxxvfv3QbM8PU8uBx2jaNt9btQqvQNx"); // test string conversion @@ -802,7 +802,7 @@ mod tests { assert!(!pk.compressed); assert_eq!(&pk.to_string(), "042e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b133"); assert_eq!(pk, PublicKey::from_str("042e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b133").unwrap()); - let addr = Address::p2pkh(&pk, sk.network); + let addr = Address::p2pkh(pk, sk.network); assert_eq!(&addr.to_string(), "1GhQvF6dL8xa6wBxLnWmHcQsurx9RxiMc8"); pk.compressed = true; assert_eq!( diff --git a/bitcoin/src/sign_message.rs b/bitcoin/src/sign_message.rs index d846946a..ca21de32 100644 --- a/bitcoin/src/sign_message.rs +++ b/bitcoin/src/sign_message.rs @@ -151,7 +151,7 @@ mod message_signing { match address.address_type() { Some(AddressType::P2pkh) => { let pubkey = self.recover_pubkey(secp_ctx, msg_hash)?; - Ok(*address == Address::p2pkh(&pubkey, *address.network())) + Ok(address.pubkey_hash() == Some(pubkey.pubkey_hash())) } Some(address_type) => Err(MessageSignatureError::UnsupportedAddressType(address_type)), @@ -242,7 +242,7 @@ mod tests { assert!(pubkey.compressed); assert_eq!(pubkey.inner, secp256k1::PublicKey::from_secret_key(&secp, &privkey)); - let p2pkh = Address::p2pkh(&pubkey, Network::Bitcoin); + let p2pkh = Address::p2pkh(pubkey, Network::Bitcoin); assert_eq!(signature2.is_signed_by_address(&secp, &p2pkh, msg_hash), Ok(true)); let p2wpkh = Address::p2wpkh(&pubkey, Network::Bitcoin).unwrap(); assert_eq!( @@ -280,7 +280,7 @@ mod tests { PublicKey::from_slice(&BASE64_STANDARD.decode(pubkey_base64).expect("base64 string")) .expect("pubkey slice"); - let p2pkh = Address::p2pkh(&pubkey, Network::Bitcoin); + let p2pkh = Address::p2pkh(pubkey, Network::Bitcoin); assert_eq!(signature.is_signed_by_address(&secp, &p2pkh, msg_hash), Ok(false)); } } diff --git a/bitcoin/tests/serde.rs b/bitcoin/tests/serde.rs index 329af982..430e9b6c 100644 --- a/bitcoin/tests/serde.rs +++ b/bitcoin/tests/serde.rs @@ -145,7 +145,7 @@ fn serde_regression_witness() { fn serde_regression_address() { let s = include_str!("data/serde/public_key_hex"); let pk = PublicKey::from_str(s.trim()).unwrap(); - let addr = Address::p2pkh(&pk, Network::Bitcoin); + let addr = Address::p2pkh(pk, Network::Bitcoin); let got = serialize(&addr).unwrap(); let want = include_bytes!("data/serde/address_bincode") as &[_]; From 1ee989a3af6ab68b49671f09a6bdf01d2086582c Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Tue, 5 Dec 2023 09:12:23 +1100 Subject: [PATCH 4/4] Remove private fmt_internal function Just use `fmt::Display::fmt` directly since `fmt_internal` does exactly that. Refactor only, no logic changes. --- bitcoin/src/address/error.rs | 2 +- bitcoin/src/address/mod.rs | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/bitcoin/src/address/error.rs b/bitcoin/src/address/error.rs index bb46a899..a9019a70 100644 --- a/bitcoin/src/address/error.rs +++ b/bitcoin/src/address/error.rs @@ -43,7 +43,7 @@ impl fmt::Display for Error { UnrecognizedScript => write!(f, "script is not a p2pkh, p2sh or witness program"), NetworkValidation { required, ref address } => { write!(f, "address ")?; - address.fmt_internal(f)?; // Using fmt_internal in order to remove the "Address(..)" wrapper + fmt::Display::fmt(&address.0, f)?; write!(f, " is not valid on {}", required) } Error::UnknownHrp(ref e) => write_err!(f, "unknown hrp"; e), diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs index 47fd4513..362b2c42 100644 --- a/bitcoin/src/address/mod.rs +++ b/bitcoin/src/address/mod.rs @@ -380,7 +380,7 @@ struct DisplayUnchecked<'a, N: NetworkValidation>(&'a Address); #[cfg(feature = "serde")] impl fmt::Display for DisplayUnchecked<'_, N> { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { self.0.fmt_internal(fmt) } + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0 .0, fmt) } } #[cfg(feature = "serde")] @@ -404,11 +404,6 @@ impl Address { pub fn as_unchecked(&self) -> &Address { unsafe { &*(self as *const Address as *const Address) } } - - /// Format the address for the usage by `Debug` and `Display` implementations. - fn fmt_internal(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, fmt) - } } /// Methods and functions that can be called only on `Address`. @@ -759,16 +754,16 @@ impl From
for script::ScriptBuf { // Alternate formatting `{:#}` is used to return uppercase version of bech32 addresses which should // be used in QR codes, see [`Address::to_qr_uri`]. impl fmt::Display for Address { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { self.fmt_internal(fmt) } + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, fmt) } } impl fmt::Debug for Address { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if V::IS_CHECKED { - self.fmt_internal(f) + fmt::Display::fmt(&self.0, f) } else { write!(f, "Address(")?; - self.fmt_internal(f)?; + fmt::Display::fmt(&self.0, f)?; write!(f, ")") } }