Repository: rust-bitcoin/rust-bitcoin
Pull request #1832 (2023-05-04 02:06:38)

Remove Network from AddressInner
An `AddressInner` struct (contains `Network` field) 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 an `Hrp` for bech32 addresses.


Fix: #1819

apoelstra: This looks good to me. I think we should use some sort of `KnownHrp` type rather than a bare `Hrp` to be inline with the other `Prefix` types. And to distinguish between testnet and signet.


tcharding: Thanks guys! I went back and forth about using the `WitnessProgram` or inlining the two fields. I can definitely put more thought into it, I'm not sure whats best without doing so.
stevenroose: Just a random thought, how about having a `Scope` type that is basically a bitmap to all the `Network` variants. So it could have `Add<Network>` but also have `From<bech32::Hrp>` and `fn from_base58_prefix(u8) -> Self` and an easy `fn allows(&self, Network) -> bool`.

We wouldn't need to have as many enums.
apoelstra: I proposed something similar here: https://github.com/rust-bitcoin/rust-bitcoin/issues/1840#issuecomment-1537448450

I like the idea, though we need some work to flesh out what we should do with such a bitmap in various cases (when more than 1 bit is set, and when 0 bits are set, in particular).
tcharding: All suggestions implemented I believe except the bitmap idea.  Note, this is still not mergable because it requires `Hrp` type to be in `bech32`.
tcharding: This needs an unreleased bug fix from `rust-bech32` to get past CI.
tcharding: The CI fail is due to a bug in `rust-bech32 v0.10.0-alpha` that is fixed on master.
apoelstra: Needs rebase.
tcharding: Rebased and implemented all suggestions from review.
tcharding: Improved the `KnownHrp` to/from functions.
tcharding: Converting to draft until I re look at this, I think patch 1 is wrong.
tcharding: The ongoing derives policy discussion is out of scope for this PR please. So the derives here should be ok, they are inline with the rest of the code base. 
tcharding: Force push is quite different to the original. While looking at the new private `Address::segwit` function I noticed that it only errored for incorrect witness program length but often we created the program ourselves, I changed it to be infallible and put `expect` calls on the `WitnessProgram` constructor (at the same location that the program was created). Note I used extremely long `expect` messages to try and make it obvious _why_ its ok since, at least for me the mental steps to verify it were not trivial, eg

"Bitcoin hash type has inner `hashes` hash type which has a length which when coupled with the segwit version means program is valid - phew"
tcharding: This PR is heavy to review, sorry about that. It leaves us open to a further improvement because many of the `Address` constructors now only return a single variant of `Error` - leaving that for later. Also this clears the way for `Network` vs `NetworkKind` change if we want to do that later too.
tcharding: Interesting, I got a fuzzing fail that I couldn't reproduce locally, either by running the fuzz test for 30 seconds or by using `duplicate_crash` with the failing value, here it is for later
```
gnu/release/bitcoin_deser_net_msg]
=====================================================================
8001c0ff6d65726b6c65626c6f636b006e000000010000000000650000000000000e0000000000000000000001000000000000000000000000000000000000080000000000000073656e6468656164657273000000000000000000000000000000000000000000000074786e00ffffffff00010000000000008000000068616465726e64800000000000000132019999999999999a62010000000000000032343333777478696472656c61793333333376657273696f6e3333036e646e6f732d36373760da6e39f366a458455df36895450c3f164d8af230393100000066656566696c7465726164650000000000000002727300000199993433333333333303000000696e7600000000000000000000000000000077747869647265
```
tcharding: Removed the private `Address::segwit()` function and called the identical public `Address::from_witness_program()` instead.
tcharding: hmm, this PR can be made more simple if we add `NetworkKind` first, it means all the legacy prefix types go away. Alternately we add `NetworkKind` after this PR and remove them.
tcharding: Now includes a new patch at the front that can be done as a separate PR if this one stalls but its kind of related to a few things going on at the moment, so here it is.

Adds a bunch of new functions to the `WitnessProgram` and removes a bunch of calls to `expect` - thanks to @Kixunil 
tcharding: > I really want the _* methods renamed but I also do not want to review this again, so I'm fine with doing it in a followup PR.
>
> ACK https://github.com/rust-bitcoin/rust-bitcoin/commit/f3db91b2ecadb832b9f67bfc3470334c9a0b67c8


Ugh, sorry man I force pushed by habit forgetting this comment. Git range diff is small enough

`git range-diff git range-diff f3db91b...@`
```diff
1:  f3db91b2 ! 1:  2b30ba5e Remove Network from AddressInner
    @@ bitcoin/src/address/mod.rs: impl<N: NetworkValidation> serde::Serialize for Addr
      }
      
     @@ bitcoin/src/address/mod.rs: impl Address {
    +     ///
          /// This is the preferred non-witness type address.
          #[inline]
    -     pub fn p2pkh(pk: &PublicKey, network: Network) -> Address {
    +-    pub fn p2pkh(pk: &PublicKey, network: Network) -> Address {
     -        let payload = Payload::PubkeyHash(pk.pubkey_hash());
     -        Address(AddressInner { network, payload }, PhantomData)
    -+        let hash = pk.pubkey_hash();
    -+        Address::_p2pkh(hash, network)
    -+    }
    -+
    -+    fn _p2pkh(hash: PubkeyHash, network: Network) -> Address {
    ++    pub fn p2pkh(pk: impl Into<PubkeyHash>, network: Network) -> Address {
    ++        let hash = pk.into();
     +        let prefix = LegacyP2pkhPrefix::from_network(network);
     +        Self(AddressInner::P2pkh { hash, prefix }, PhantomData)
          }
    @@ bitcoin/src/address/mod.rs: impl Address {
     -        let payload = Payload::ScriptHash(script.script_hash());
     -        Ok(Address(AddressInner { network, payload }, PhantomData))
     +        let hash = script.script_hash();
    -+        Ok(Address::_p2sh(hash, network))
    ++        Ok(Address::p2sh_from_hash(hash, network))
     +    }
     +
    -+    fn _p2sh(hash: ScriptHash, network: Network) -> Address {
    ++    // 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)
          }
    @@ bitcoin/src/address/mod.rs: impl Address {
     -        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(script_hash, network))
    ++        Ok(Address::p2sh_from_hash(script_hash, network))
          }
      
          /// Creates a witness pay to script hash address.
    @@ bitcoin/src/address/mod.rs: impl Address {
     -        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(script_hash, network)
    ++        Address::p2sh_from_hash(script_hash, network)
          }
      
          /// Creates a pay to taproot address from an untweaked key.
    @@ bitcoin/src/address/mod.rs: impl Address {
                  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))
    ++            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(hash, network))
    ++            Ok(Address::p2sh_from_hash(hash, network))
              } else if script.is_witness_program() {
     -            let opcode = script.first_opcode().expect("witness_version guarantees len() > 4");
     -
    @@ bitcoin/src/address/mod.rs: mod tests {
     -            Payload::PubkeyHash("162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse().unwrap()),
     -        );
     +        let hash = "162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse::<PubkeyHash>().unwrap();
    -+        let addr = Address::_p2pkh(hash, Bitcoin);
    ++        let addr = Address::p2pkh(hash, Bitcoin);
      
              assert_eq!(
                  addr.script_pubkey(),
    @@ bitcoin/src/address/mod.rs: mod tests {
     -            Payload::ScriptHash("162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse().unwrap()),
     -        );
     +        let hash = "162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse::<ScriptHash>().unwrap();
    -+        let addr = Address::_p2sh(hash, Bitcoin);
    ++        let addr = Address::p2sh_from_hash(hash, Bitcoin);
      
              assert_eq!(
                  addr.script_pubkey(),
-:  -------- > 2:  b0f3a3eb Remove private fmt_internal function
```

Patch:
diff --git a/bitcoin/src/address/error.rs b/bitcoin/src/address/error.rs
index 7e01868e..a9019a70 100644
--- a/bitcoin/src/address/error.rs
+++ b/bitcoin/src/address/error.rs
@@ -17,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,
     /// Address size more than 520 bytes is not allowed.
     ExcessiveScriptSize,
     /// Script is not a p2pkh, p2sh or witness program.
@@ -27,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<NetworkUnchecked>,
     },
+    /// Unknown hrp for current bitcoin networks (in bech32 address).
+    UnknownHrp(UnknownHrpError),
 }
 
 impl fmt::Display for Error {
@@ -41,19 +39,14 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         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"),
             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<NetworkUnchecked>(..)" wrapper
-                write!(
-                    f,
-                    " belongs to network {} which is different from required {}",
-                    found, required
-                )
+                fmt::Display::fmt(&address.0, f)?;
+                write!(f, " is not valid on {}", required)
             }
+            Error::UnknownHrp(ref e) => write_err!(f, "unknown hrp"; e),
         }
     }
 }
@@ -66,10 +59,8 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         match self {
             WitnessVersion(e) => Some(e),
             WitnessProgram(e) => Some(e),
-            UncompressedPubkey
-            | ExcessiveScriptSize
-            | UnrecognizedScript
-            | NetworkValidation { .. } => None,
+            UnknownHrp(e) => Some(e),
+            ExcessiveScriptSize | UnrecognizedScript | NetworkValidation { .. } => None,
         }
     }
 }
@@ -110,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 {
@@ -121,6 +114,7 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
             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),
         }
     }
 }
@@ -135,6 +129,7 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
             Bech32(ref e) => Some(e),
             WitnessVersion(ref e) => Some(e),
             WitnessProgram(ref e) => Some(e),
+            UnknownHrp(ref e) => Some(e),
         }
     }
 }
@@ -154,3 +149,21 @@ fn from(e: witness_version::TryFromError) -> Self { Self::WitnessVersion(e) }
 impl From<witness_program::Error> for ParseError {
     fn from(e: witness_program::Error) -> Self { Self::WitnessProgram(e) }
 }
+
+impl From<UnknownHrpError> 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 07115408..362b2c42 100644
--- a/bitcoin/src/address/mod.rs
+++ b/bitcoin/src/address/mod.rs
@@ -33,7 +33,7 @@
 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::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 @@ fn from_str(s: &str) -> Result<Self, Self::Err> {
     }
 }
 
-/// 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<Self, UnknownHrpError> {
+        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.
@@ -298,7 +380,7 @@ pub struct Address<V = NetworkChecked>(AddressInner, PhantomData<V>)
 
 #[cfg(feature = "serde")]
 impl<N: NetworkValidation> 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")]
@@ -317,80 +399,11 @@ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
 /// Methods on [`Address`] that can be called on both `Address<NetworkChecked>` and
 /// `Address<NetworkUnchecked>`.
 impl<V: NetworkValidation> Address<V> {
-    /// 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<NetworkUnchecked> {
         unsafe { &*(self as *const Address<V> as *const Address<NetworkUnchecked>) }
     }
-
-    /// 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<NetworkChecked>::address_type)
-    /// on `Address<NetworkChecked>` but internally can be called on `Address<NetworkUnchecked>` as
-    /// `address_type_internal`.
-    ///
-    /// # Returns
-    /// None if unknown, non-standard or related to the future witness version.
-    fn address_type_internal(&self) -> Option<AddressType> {
-        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)
-    }
 }
 
 /// Methods and functions that can be called only on `Address<NetworkChecked>`.
@@ -399,9 +412,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<PubkeyHash>, 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,55 +427,52 @@ pub fn p2sh(script: &Script, network: Network) -> Result<Address, Error> {
         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<Address, Error> {
-        let prog = WitnessProgram::new(
-            WitnessVersion::V0,
-            pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?,
-        )?;
-        let payload = Payload::WitnessProgram(prog);
-        Ok(Address(AddressInner { network, payload }, PhantomData))
+    pub fn p2wpkh(
+        pk: &PublicKey,
+        network: Network,
+    ) -> Result<Address, key::UncompressedPubkeyError> {
+        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<Address, Error> {
-        let builder = script::Builder::new()
-            .push_int(0)
-            .push_slice(pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?);
-
-        let payload = Payload::ScriptHash(builder.into_script().script_hash());
-        Ok(Address(AddressInner { network, payload }, PhantomData))
+    pub fn p2shwpkh(
+        pk: &PublicKey,
+        network: Network,
+    ) -> Result<Address, key::UncompressedPubkeyError> {
+        let builder = script::Builder::new().push_int(0).push_slice(pk.wpubkey_hash()?);
+        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.
@@ -471,21 +482,14 @@ pub fn p2tr<C: Verification>(
         merkle_root: Option<TapNodeHash>,
         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.
-    ///
-    /// 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");
-        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.
@@ -493,16 +497,53 @@ pub fn p2tr_tweaked(output_key: TweakedPublicKey, network: Network) -> 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<AddressType> { self.address_type_internal() }
+    pub fn address_type(&self) -> Option<AddressType> {
+        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<PubkeyHash> {
+        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<ScriptHash> {
+        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.
@@ -521,32 +562,34 @@ pub fn is_spend_standard(&self) -> bool { self.address_type().is_some() }
 
     /// Constructs an [`Address`] from an output script (`scriptPubkey`).
     pub fn from_script(script: &Script, network: Network) -> Result<Address, Error> {
-        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 witness_program = script.as_bytes()[2..].to_vec();
+            let opcode = script.first_opcode().expect("is_witness_program guarantees len > 4");
 
-            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),
         }
     }
 
@@ -586,7 +629,7 @@ pub fn to_qr_uri(&self) -> String { format!("bitcoin:{:#}", self) }
     /// 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)
@@ -599,31 +642,38 @@ pub fn is_related_to_pubkey(&self, pubkey: &PublicKey) -> bool {
     /// 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] == <PubkeyHash as AsRef<[u8; 20]>>::as_ref(hash),
-            Payload::ScriptHash(ref hash) if script_pubkey.is_p2sh() =>
-                &script_pubkey.as_bytes()[2..22] == <ScriptHash as AsRef<[u8; 20]>>::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] == <PubkeyHash as AsRef<[u8; 20]>>::as_ref(hash),
+            P2sh { ref hash, prefix: _ } if script.is_p2sh() =>
+                &script.as_bytes()[2..22] == <ScriptHash as AsRef<[u8; 20]>>::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(),
         }
     }
 }
@@ -657,16 +707,11 @@ pub fn assume_checked_ref(&self) -> &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),
         }
     }
 
@@ -679,7 +724,7 @@ pub fn require_network(self, required: Network) -> Result<Address, Error> {
         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 })
         }
     }
 
@@ -691,22 +736,17 @@ pub fn require_network(self, required: Network) -> Result<Address, Error> {
     /// 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<Address<NetworkUnchecked>> for Address {
-    fn eq(&self, other: &Address<NetworkUnchecked>) -> 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<Address> for Address<NetworkUnchecked> {
-    fn eq(&self, other: &Address) -> bool { other == self }
-}
-
 impl From<Address> for script::ScriptBuf {
     fn from(a: Address) -> Self { a.script_pubkey() }
 }
@@ -714,56 +754,39 @@ fn from(a: Address) -> Self { a.script_pubkey() }
 // 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<V: NetworkValidation> fmt::Debug for Address<V> {
     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<NetworkUnchecked>(")?;
-            self.fmt_internal(f)?;
+            fmt::Display::fmt(&self.0, f)?;
             write!(f, ")")
         }
     }
 }
 
-/// 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<NetworkUnchecked> {
     type Err = ParseError;
 
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        // 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<Address<NetworkUnchecked>, 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)));
         }
@@ -772,19 +795,30 @@ fn from_str(s: &str) -> Result<Self, Self::Err> {
             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))
     }
 }
 
@@ -804,10 +838,10 @@ 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) {
+    fn roundtrips(addr: &Address, network: Network) {
         assert_eq!(
             Address::from_str(&addr.to_string()).unwrap().assume_checked(),
             *addr,
@@ -815,8 +849,9 @@ fn roundtrips(addr: &Address) {
             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,
         );
@@ -832,10 +867,8 @@ fn roundtrips(addr: &Address) {
 
     #[test]
     fn test_p2pkh_address_58() {
-        let addr = Address::new(
-            Bitcoin,
-            Payload::PubkeyHash("162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse().unwrap()),
-        );
+        let hash = "162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse::<PubkeyHash>().unwrap();
+        let addr = Address::p2pkh(hash, Bitcoin);
 
         assert_eq!(
             addr.script_pubkey(),
@@ -843,30 +876,28 @@ fn test_p2pkh_address_58() {
         );
         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::<PublicKey>().unwrap();
-        let addr = Address::p2pkh(&key, Bitcoin);
+        let addr = Address::p2pkh(key, Bitcoin);
         assert_eq!(&addr.to_string(), "1QJVDzdqb1VpbDK7uDeyVXy9mR27CJiyhY");
 
         let key = "03df154ebfcf29d29cc10d5c2565018bce2d9edbab267c31d2caf44a63056cf99f"
             .parse::<PublicKey>()
             .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::<ScriptHash>().unwrap();
+        let addr = Address::p2sh_from_hash(hash, Bitcoin);
 
         assert_eq!(
             addr.script_pubkey(),
@@ -874,7 +905,7 @@ fn test_p2sh_address_58() {
         );
         assert_eq!(&addr.to_string(), "33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k");
         assert_eq!(addr.address_type(), Some(AddressType::P2sh));
-        roundtrips(&addr);
+        roundtrips(&addr, Bitcoin);
     }
 
     #[test]
@@ -883,7 +914,7 @@ fn test_p2sh_parse() {
         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]
@@ -901,11 +932,11 @@ fn test_p2wpkh() {
         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), Err(Error::UncompressedPubkey));
+        assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError);
     }
 
     #[test]
@@ -918,7 +949,7 @@ fn test_p2wsh() {
             "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej"
         );
         assert_eq!(addr.address_type(), Some(AddressType::P2wsh));
-        roundtrips(&addr);
+        roundtrips(&addr, Bitcoin);
     }
 
     #[test]
@@ -930,11 +961,11 @@ fn test_p2shwpkh() {
         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), Err(Error::UncompressedPubkey));
+        assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError);
     }
 
     #[test]
@@ -944,18 +975,19 @@ fn test_p2shwsh() {
         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]
@@ -1113,57 +1145,6 @@ fn test_qr_string() {
         }
     }
 
-    #[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::<Vec<_>>();
-
-        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
@@ -1178,7 +1159,7 @@ fn p2tr_from_untweaked() {
             "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 @@ pub fn new_witness_program(witness_program: &WitnessProgram) -> Self {
     /// 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<T: AsRef<PushBytes>>(
+    pub(crate) fn new_witness_program_unchecked<T: AsRef<PushBytes>>(
         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 @@ pub fn new<P>(version: WitnessVersion, program: P) -> Result<Self, Error>
         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<Self, key::UncompressedPubkeyError> {
+        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<C: Verification>(
+        secp: &Secp256k1<C>,
+        internal_key: UntweakedPublicKey,
+        merkle_root: Option<TapNodeHash>,
+    ) -> 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 6c4d354c..89509b66 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -58,15 +58,13 @@ fn with_serialized<R, F: FnOnce(&[u8]) -> R>(&self, f: F) -> R {
     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<WPubkeyHash> {
+    pub fn wpubkey_hash(&self) -> Result<WPubkeyHash, UncompressedPubkeyError> {
         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<hex::HexToArrayError> 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;
@@ -767,7 +781,7 @@ fn test_key_derivation() {
         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
@@ -788,7 +802,7 @@ fn test_key_derivation() {
         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!(
@@ -826,7 +840,7 @@ fn test_wpubkey_hash() {
             pk.wpubkey_hash().unwrap().to_string(),
             "9511aa27ef39bbfa4e4f3dd15f4d66ea57f475b4"
         );
-        assert_eq!(upk.wpubkey_hash(), None);
+        assert!(upk.wpubkey_hash().is_err());
     }
 
     #[cfg(feature = "serde")]
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 @@ pub fn is_signed_by_address<C: secp256k1::Verification>(
             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 @@ fn test_message_signature() {
         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 @@ fn test_incorrect_message_signature() {
             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));
     }
 }


Test patch:
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 &[_];