Repository: rust-bitcoin/rust-bitcoin
Pull request #3611 (2024-11-13 03:00:41)

Hashes: Make `hex` dependency optional
This is done in 3 parts:

1. Pull the `serde` stuff out of `impl_bytelike_traits` to fix the bug described here: https://github.com/rust-bitcoin/rust-bitcoin/issues/2654#issuecomment-2470716693
2. Prepare the `hashes` trait by removing string/hex trait bounds from `GeneralHash` and also pull the hex/string stuff out of `impl_bytelike_traits`
3. Make hex optional, including adding custom debug logic when `hex` feature is not enabled

Patch 3 is tested in `hashes/embedded`, by the new `debug` unit test, and there is a `Midstate` unit test as well that covers the `Debug` impl.

Close: #2654 - BOOM!


Patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 50b8a0d8..cf06c57a 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -253,8 +253,10 @@ jobs:
         run: cd bitcoin/embedded && cargo run --target thumbv7m-none-eabi
       - name: "Run hashes/embedded no alloc"
         run: cd hashes/embedded && cargo run --target thumbv7m-none-eabi
-      - name: "Run hashes/embedded with alloc"
+      - name: "Run hashes/embedded with alloc and no hex"
         run: cd hashes/embedded && cargo run --target thumbv7m-none-eabi --features=alloc
+      - name: "Run hashes/embedded with alloc and hex"
+        run: cd hashes/embedded && cargo run --target thumbv7m-none-eabi --features=alloc,hex
 
   ASAN:                         # hashes crate only.
     name: ASAN - nightly toolchain
diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml
index ff078247..d96e8af5 100644
--- a/bitcoin/Cargo.toml
+++ b/bitcoin/Cargo.toml
@@ -27,7 +27,7 @@ arbitrary = ["dep:arbitrary", "units/arbitrary", "primitives/arbitrary"]
 [dependencies]
 base58 = { package = "base58ck", version = "0.1.0", default-features = false, features = ["alloc"] }
 bech32 = { version = "0.11.0", default-features = false, features = ["alloc"] }
-hashes = { package = "bitcoin_hashes", version = "0.15.0", default-features = false, features = ["alloc", "bitcoin-io"] }
+hashes = { package = "bitcoin_hashes", version = "0.15.0", default-features = false, features = ["alloc", "bitcoin-io", "hex"] }
 hex = { package = "hex-conservative", version = "0.3.0", default-features = false, features = ["alloc"] }
 internals = { package = "bitcoin-internals", version = "0.4.0", features = ["alloc"] }
 io = { package = "bitcoin-io", version = "0.2.0", default-features = false, features = ["alloc"] }
diff --git a/bitcoin/src/bip158.rs b/bitcoin/src/bip158.rs
index 8c5499ad..55a29073 100644
--- a/bitcoin/src/bip158.rs
+++ b/bitcoin/src/bip158.rs
@@ -62,6 +62,10 @@
     pub struct FilterHeader(sha256d::Hash);
 }
 
+hashes::impl_hex_for_newtype!(FilterHash, FilterHeader);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(FilterHash, FilterHeader);
+
 impl_hashencode!(FilterHash);
 impl_hashencode!(FilterHeader);
 
diff --git a/bitcoin/src/bip32.rs b/bitcoin/src/bip32.rs
index 6f609b59..8f11e67b 100644
--- a/bitcoin/src/bip32.rs
+++ b/bitcoin/src/bip32.rs
@@ -58,6 +58,10 @@ fn from_hmac(hmac: Hmac<sha512::Hash>) -> Self {
     pub struct XKeyIdentifier(hash160::Hash);
 }
 
+hashes::impl_hex_for_newtype!(XKeyIdentifier);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(XKeyIdentifier);
+
 /// Extended private key
 #[derive(Copy, Clone, PartialEq, Eq)]
 #[cfg_attr(feature = "std", derive(Debug))]
diff --git a/bitcoin/src/blockdata/script/mod.rs b/bitcoin/src/blockdata/script/mod.rs
index 774e95c3..55947e43 100644
--- a/bitcoin/src/blockdata/script/mod.rs
+++ b/bitcoin/src/blockdata/script/mod.rs
@@ -92,6 +92,11 @@
     /// SegWit version of a Bitcoin Script bytecode hash.
     pub struct WScriptHash(sha256::Hash);
 }
+
+hashes::impl_hex_for_newtype!(ScriptHash, WScriptHash);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(ScriptHash, WScriptHash);
+
 impl_asref_push_bytes!(ScriptHash, WScriptHash);
 
 impl ScriptHash {
diff --git a/bitcoin/src/blockdata/witness.rs b/bitcoin/src/blockdata/witness.rs
index 318baaf5..0844161c 100644
--- a/bitcoin/src/blockdata/witness.rs
+++ b/bitcoin/src/blockdata/witness.rs
@@ -147,15 +147,19 @@ fn push_ecdsa_signature(&mut self, signature: ecdsa::Signature) {
         ///
         /// See [`Script::is_p2tr`] to check whether this is actually a Taproot witness.
         fn tapscript(&self) -> Option<&Script> {
-            if self.is_empty() {
-                return None;
-            }
-
-            if self.taproot_annex().is_some() {
-                self.third_to_last().map(Script::from_bytes)
-            } else {
-                self.second_to_last().map(Script::from_bytes)
-            }
+            self.last().and_then(|last| {
+                // From BIP341:
+                // If there are at least two witness elements, and the first byte of
+                // the last element is 0x50, this last element is called annex a
+                // and is removed from the witness stack.
+                if self.len() >= 3 && last.first() == Some(&TAPROOT_ANNEX_PREFIX) {
+                    self.nth(self.len() - 3).map(Script::from_bytes)
+                } else if self.len() >= 2 {
+                    self.nth(self.len() - 2).map(Script::from_bytes)
+                } else {
+                    None
+                }
+            })
         }
 
         /// Get the taproot control block following BIP341 rules.
@@ -166,15 +170,19 @@ fn tapscript(&self) -> Option<&Script> {
         ///
         /// See [`Script::is_p2tr`] to check whether this is actually a Taproot witness.
         fn taproot_control_block(&self) -> Option<&[u8]> {
-            if self.is_empty() {
-                return None;
-            }
-
-            if self.taproot_annex().is_some() {
-                self.second_to_last()
-            } else {
-                self.last()
-            }
+            self.last().and_then(|last| {
+                // From BIP341:
+                // If there are at least two witness elements, and the first byte of
+                // the last element is 0x50, this last element is called annex a
+                // and is removed from the witness stack.
+                if self.len() >= 3 && last.first() == Some(&TAPROOT_ANNEX_PREFIX) {
+                    self.nth(self.len() - 2)
+                } else if self.len() >= 2 {
+                    Some(last)
+                } else {
+                    None
+                }
+            })
         }
 
         /// Get the taproot annex following BIP341 rules.
@@ -306,26 +314,6 @@ fn test_get_tapscript() {
         assert_eq!(witness_annex.tapscript(), Some(Script::from_bytes(&tapscript[..])));
     }
 
-    #[test]
-    fn test_get_tapscript_from_keypath() {
-        let signature = hex!("deadbeef");
-        // annex starting with 0x50 causes the branching logic.
-        let annex = hex!("50");
-
-        let witness_vec = vec![signature.clone()];
-        let witness_vec_annex = vec![signature.clone(), annex];
-
-        let witness_serialized: Vec<u8> = serialize(&witness_vec);
-        let witness_serialized_annex: Vec<u8> = serialize(&witness_vec_annex);
-
-        let witness = deserialize::<Witness>(&witness_serialized[..]).unwrap();
-        let witness_annex = deserialize::<Witness>(&witness_serialized_annex[..]).unwrap();
-
-        // With or without annex, no tapscript should be returned.
-        assert_eq!(witness.tapscript(), None);
-        assert_eq!(witness_annex.tapscript(), None);
-    }
-
     #[test]
     fn test_get_control_block() {
         let tapscript = hex!("deadbeef");
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index a0627ba0..9772f39d 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -264,6 +264,11 @@ fn from_str(s: &str) -> Result<PublicKey, ParsePublicKeyError> {
     /// SegWit version of a public key hash.
     pub struct WPubkeyHash(hash160::Hash);
 }
+
+hashes::impl_hex_for_newtype!(PubkeyHash, WPubkeyHash);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(PubkeyHash, WPubkeyHash);
+
 impl_asref_push_bytes!(PubkeyHash, WPubkeyHash);
 
 impl From<PublicKey> for PubkeyHash {
diff --git a/bitcoin/src/crypto/sighash.rs b/bitcoin/src/crypto/sighash.rs
index e6530921..d5651de3 100644
--- a/bitcoin/src/crypto/sighash.rs
+++ b/bitcoin/src/crypto/sighash.rs
@@ -56,6 +56,10 @@ fn from(hash: $ty) -> secp256k1::Message {
     pub struct SegwitV0Sighash(sha256d::Hash);
 }
 
+hashes::impl_hex_for_newtype!(LegacySighash, SegwitV0Sighash);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(LegacySighash, SegwitV0Sighash);
+
 impl_message_from_hash!(LegacySighash);
 impl_message_from_hash!(SegwitV0Sighash);
 
@@ -82,6 +86,10 @@ fn from_engine(e: sha256d::HashEngine) -> Self { Self(sha256d::Hash::from_engine
     pub struct TapSighash(sha256t::Hash<TapSighashTag>);
 }
 
+hashes::impl_hex_for_newtype!(TapSighash);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(TapSighash);
+
 impl_message_from_hash!(TapSighash);
 
 /// Efficiently calculates signature hash message for legacy, segwit and Taproot inputs.
diff --git a/hashes/Cargo.toml b/hashes/Cargo.toml
index 1553f788..c05e3188 100644
--- a/hashes/Cargo.toml
+++ b/hashes/Cargo.toml
@@ -17,12 +17,13 @@ exclude = ["tests", "contrib"]
 default = ["std"]
 std = ["alloc", "bitcoin-io?/std", "hex/std"]
 alloc = ["bitcoin-io?/alloc", "hex/alloc"]
+serde = ["dep:serde", "hex"]
 # Smaller (but slower) implementation of sha256, sha512 and ripemd160
 small-hash = []
 
 [dependencies]
-hex = { package = "hex-conservative", version = "0.3.0", default-features = false }
 
+hex = { package = "hex-conservative", version = "0.3.0", default-features = false, optional = true }
 bitcoin-io = { version = "0.2.0", default-features = false, optional = true }
 serde = { version = "1.0", default-features = false, optional = true }
 
diff --git a/hashes/embedded/Cargo.toml b/hashes/embedded/Cargo.toml
index 3cebf964..be73fe56 100644
--- a/hashes/embedded/Cargo.toml
+++ b/hashes/embedded/Cargo.toml
@@ -11,6 +11,7 @@ members = ["."]
 
 [features]
 alloc = ["alloc-cortex-m", "bitcoin_hashes/alloc"]
+hex = ["bitcoin_hashes/hex"]
 
 [dependencies]
 cortex-m = "0.6.0"
diff --git a/hashes/embedded/src/main.rs b/hashes/embedded/src/main.rs
index 027563d0..3a19faaf 100644
--- a/hashes/embedded/src/main.rs
+++ b/hashes/embedded/src/main.rs
@@ -12,13 +12,20 @@
 use bitcoin_hashes::{sha256, HashEngine};
 use bitcoin_io::Write;
 use cortex_m_rt::entry;
-use cortex_m_semihosting::{debug, hprintln};
+use cortex_m_semihosting::debug;
+#[cfg(feature = "hex")]
+use cortex_m_semihosting::hprintln;
 use panic_halt as _;
 
 hash_newtype! {
     struct TestType(sha256::Hash);
 }
 
+#[cfg(feature = "hex")]
+bitcoin_hashes::impl_hex_for_newtype!(TestType);
+#[cfg(not(feature = "hex"))]
+bitcoin_hashes::impl_debug_only_for_newtype!(TestType);
+
 // this is the allocator the application will use
 #[cfg(feature = "alloc")]
 #[global_allocator]
@@ -34,16 +41,19 @@ fn main() -> ! {
 
     let mut engine = sha256::Hash::engine();
     engine.write_all(b"abc").unwrap();
+    #[cfg(feature = "hex")]
     check_result(engine);
 
     let mut engine = sha256::Hash::engine();
     engine.input(b"abc");
+    #[cfg(feature = "hex")]
     check_result(engine);
 
     debug::exit(debug::EXIT_SUCCESS);
     loop {}
 }
 
+#[cfg(feature = "hex")]
 fn check_result(engine: sha256::HashEngine) {
     let hash = TestType(sha256::Hash::from_engine(engine));
 
diff --git a/hashes/src/hmac.rs b/hashes/src/hmac.rs
index a14a5445..283bdc7d 100644
--- a/hashes/src/hmac.rs
+++ b/hashes/src/hmac.rs
@@ -96,15 +96,15 @@ fn n_bytes_hashed(&self) -> u64 { self.iengine.n_bytes_hashed() }
     fn input(&mut self, buf: &[u8]) { self.iengine.input(buf) }
 }
 
-impl<T: GeneralHash> fmt::Debug for Hmac<T> {
+impl<T: GeneralHash + fmt::Debug> fmt::Debug for Hmac<T> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(&self.0, f) }
 }
 
-impl<T: GeneralHash> fmt::Display for Hmac<T> {
+impl<T: GeneralHash + fmt::Display> fmt::Display for Hmac<T> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
 }
 
-impl<T: GeneralHash> fmt::LowerHex for Hmac<T> {
+impl<T: GeneralHash + fmt::LowerHex> fmt::LowerHex for Hmac<T> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(&self.0, f) }
 }
 
diff --git a/hashes/src/internal_macros.rs b/hashes/src/internal_macros.rs
index a707d222..39999b3d 100644
--- a/hashes/src/internal_macros.rs
+++ b/hashes/src/internal_macros.rs
@@ -20,7 +20,11 @@
 /// `from_engine` obviously implements the finalization algorithm.
 macro_rules! hash_trait_impls {
     ($bits:expr, $reverse:expr $(, $gen:ident: $gent:ident)*) => {
-        $crate::impl_bytelike_traits!(Hash, { $bits / 8 }, $reverse $(, $gen: $gent)*);
+        $crate::impl_bytelike_traits!(Hash, { $bits / 8 } $(, $gen: $gent)*);
+        #[cfg(feature = "hex")]
+        $crate::impl_hex_string_traits!(Hash, { $bits / 8 }, $reverse $(, $gen: $gent)*);
+        #[cfg(not(feature = "hex"))]
+        $crate::impl_debug_only!(Hash, { $bits / 8 }, $reverse $(, $gen: $gent)*);
 
         impl<$($gen: $gent),*> $crate::GeneralHash for Hash<$($gen),*> {
             type Engine = HashEngine;
@@ -28,6 +32,9 @@ impl<$($gen: $gent),*> $crate::GeneralHash for Hash<$($gen),*> {
             fn from_engine(e: HashEngine) -> Hash<$($gen),*> { Self::from_engine(e) }
         }
 
+        #[cfg(feature = "serde")]
+        $crate::serde_impl!(Hash, { $bits / 8} $(, $gen: $gent)*);
+
         impl<$($gen: $gent),*> $crate::Hash for Hash<$($gen),*> {
             type Bytes = [u8; $bits / 8];
 
diff --git a/hashes/src/lib.rs b/hashes/src/lib.rs
index 11acde41..bc6206d3 100644
--- a/hashes/src/lib.rs
+++ b/hashes/src/lib.rs
@@ -86,6 +86,7 @@
 extern crate test;
 
 /// Re-export the `hex-conservative` crate.
+#[cfg(feature = "hex")]
 pub extern crate hex;
 
 #[doc(hidden)]
@@ -126,7 +127,8 @@ pub mod serde_details {
     }
 }
 
-use core::{convert, fmt, hash};
+use core::fmt::{self, Write as _};
+use core::{convert, hash};
 
 #[rustfmt::skip]                // Keep public re-exports separate.
 #[doc(inline)]
@@ -261,20 +263,10 @@ fn hash_reader<R: io::BufRead>(reader: &mut R) -> Result<Self, io::Error>
 
 /// Trait which applies to hashes of all types.
 pub trait Hash:
-    Copy
-    + Clone
-    + PartialEq
-    + Eq
-    + PartialOrd
-    + Ord
-    + hash::Hash
-    + fmt::Debug
-    + fmt::Display
-    + fmt::LowerHex
-    + convert::AsRef<[u8]>
+    Copy + Clone + PartialEq + Eq + PartialOrd + Ord + hash::Hash + convert::AsRef<[u8]>
 {
     /// The byte array that represents the hash internally.
-    type Bytes: hex::FromHex + Copy + IsByteArray;
+    type Bytes: Copy + IsByteArray;
 
     /// Length of the hash, in bytes.
     const LEN: usize = Self::Bytes::LEN;
@@ -322,6 +314,22 @@ fn incomplete_block_len<H: HashEngine>(eng: &H) -> usize {
     (eng.n_bytes_hashed() % block_size) as usize
 }
 
+/// Writes `bytes` as a `hex` string to the formatter.
+///
+/// For when we cannot rely on having the `hex` feature enabled. Ignores formatter options and just
+/// writes with plain old `f.write_char()`.
+pub fn debug_hex(bytes: &[u8], f: &mut fmt::Formatter) -> fmt::Result {
+    const HEX_TABLE: [u8; 16] = *b"0123456789abcdef";
+
+    for &b in bytes {
+        let lower = HEX_TABLE[usize::from(b >> 4)];
+        let upper = HEX_TABLE[usize::from(b & 0b00001111)];
+        f.write_char(char::from(lower))?;
+        f.write_char(char::from(upper))?;
+    }
+    Ok(())
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -335,6 +343,11 @@ mod tests {
         struct TestNewtype2(sha256d::Hash);
     }
 
+    #[cfg(feature = "hex")]
+    crate::impl_hex_for_newtype!(TestNewtype, TestNewtype2);
+    #[cfg(not(feature = "hex"))]
+    crate::impl_debug_only_for_newtype!(TestNewtype, TestNewtype2);
+
     #[test]
     #[cfg(feature = "alloc")]
     fn newtype_fmt_roundtrip() {
diff --git a/hashes/src/macros.rs b/hashes/src/macros.rs
index 5520ffe9..3d5382ae 100644
--- a/hashes/src/macros.rs
+++ b/hashes/src/macros.rs
@@ -4,6 +4,8 @@
 //!
 //! - [`sha256t_tag`](crate::sha256t_tag)
 //! - [`hash_newtype`](crate::hash_newtype)
+//! - [`impl_hex_for_newtype`](crate::impl_hex_for_newtype)
+//! - [`impl_serde_for_newtype`](crate::impl_serde_for_newtype)
 
 /// Macro used to define a tag.
 ///
@@ -71,8 +73,7 @@ fn engine() -> $crate::sha256::HashEngine {
 ///
 /// You can add arbitrary doc comments or other attributes to the struct or it's field. Note that
 /// the macro already derives [`Copy`], [`Clone`], [`Eq`], [`PartialEq`],
-/// [`Hash`](core::hash::Hash), [`Ord`], [`PartialOrd`]. With the `serde` feature on, this also adds
-/// `Serialize` and `Deserialize` implementations.
+/// [`Hash`](core::hash::Hash), [`Ord`], [`PartialOrd`].
 ///
 /// You can also define multiple newtypes within one macro call:
 ///
@@ -133,7 +134,7 @@ macro_rules! hash_newtype {
             $({ $($type_attrs)* })*
         }
 
-        $crate::impl_bytelike_traits!($newtype, { <$newtype as $crate::Hash>::LEN }, <$newtype as $crate::Hash>::DISPLAY_BACKWARD);
+        $crate::impl_bytelike_traits!($newtype, { <$newtype as $crate::Hash>::LEN });
 
         #[allow(unused)] // Private wrapper types may not need all functions.
         impl $newtype {
@@ -194,19 +195,81 @@ fn as_byte_array(&self) -> &Self::Bytes { self.as_byte_array() }
     };
 }
 
-/// Adds trait impls to a bytelike type.
+/// Implements string functions using hex for a new type crated with [`crate::hash_newtype`] macro.
 ///
 /// Implements:
 ///
 /// * `str::FromStr`
-/// * `fmt::{LowerHex, UpperHex}` using `hex-conservative`.
+/// * `fmt::{LowerHex, UpperHex}` using `hex-conservative`
 /// * `fmt::{Display, Debug}` by calling `LowerHex`
-/// * `serde::{Deserialize, Serialize}`
+#[macro_export]
+#[cfg(feature = "hex")]
+macro_rules! impl_hex_for_newtype {
+    ($($newtype:ident),*) => {
+        $(
+            $crate::impl_hex_string_traits!($newtype, { <$newtype as $crate::Hash>::LEN }, { <$newtype as $crate::Hash>::DISPLAY_BACKWARD });
+        )*
+    }
+}
+
+/// Implements `fmt::Debug` using hex for a new type crated with [`crate::hash_newtype`] macro.
+///
+/// This is provided in case you do not want to use the `hex` feature.
+#[macro_export]
+macro_rules! impl_debug_only_for_newtype {
+    ($($newtype:ident),*) => {
+        $(
+            $crate::impl_debug_only!($newtype, { <$newtype as $crate::Hash>::LEN }, { <$newtype as $crate::Hash>::DISPLAY_BACKWARD });
+        )*
+    }
+}
+
+/// Adds trait impls to a bytelike type.
+///
+/// Implements:
+///
 /// * `AsRef[u8; $len]`
 /// * `AsRef[u8]`
 /// * `Borrow<[u8; $len]>`
 /// * `Borrow<[u8]>`
 ///
+/// ## Parameters
+///
+/// * `ty` - The bytelike type to implement the traits on.
+/// * `$len` - The number of bytes this type has.
+/// * `$gen: $gent` - generic type(s) and trait bound(s).
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_bytelike_traits {
+    ($ty:ident, $len:expr $(, $gen:ident: $gent:ident)*) => {
+        impl<$($gen: $gent),*> $crate::_export::_core::convert::AsRef<[u8; { $len }]> for $ty<$($gen),*> {
+            #[inline]
+            fn as_ref(&self) -> &[u8; { $len }] { self.as_byte_array() }
+        }
+
+        impl<$($gen: $gent),*> $crate::_export::_core::convert::AsRef<[u8]> for $ty<$($gen),*> {
+            #[inline]
+            fn as_ref(&self) -> &[u8] { self.as_byte_array() }
+        }
+
+        impl<$($gen: $gent),*> $crate::_export::_core::borrow::Borrow<[u8; { $len }]> for $ty<$($gen),*>  {
+            fn borrow(&self) -> &[u8; { $len }] {  self.as_byte_array() }
+        }
+
+        impl<$($gen: $gent),*> $crate::_export::_core::borrow::Borrow<[u8]> for $ty<$($gen),*>  {
+            fn borrow(&self) -> &[u8] {  self.as_byte_array() }
+        }
+    }
+}
+
+/// Adds hex string trait impls to a bytelike type using hex.
+///
+/// Implements:
+///
+/// * `str::FromStr`
+/// * `fmt::{LowerHex, UpperHex}` using `hex-conservative`.
+/// * `fmt::{Display, Debug}` by calling `LowerHex`
+///
 /// Requires:
 ///
 /// * [`hex-conservative`] to publicly available as `$crate::hex`.
@@ -224,7 +287,8 @@ fn as_byte_array(&self) -> &Self::Bytes { self.as_byte_array() }
 /// [`hex-conservative`]: <https://crates.io/crates/hex-conservative>
 #[doc(hidden)]
 #[macro_export]
-macro_rules! impl_bytelike_traits {
+#[cfg(feature = "hex")]
+macro_rules! impl_hex_string_traits {
     ($ty:ident, $len:expr, $reverse:expr $(, $gen:ident: $gent:ident)*) => {
         impl<$($gen: $gent),*> $crate::_export::_core::str::FromStr for $ty<$($gen),*> {
             type Err = $crate::hex::HexToArrayError;
@@ -246,25 +310,19 @@ impl<$($gen: $gent),*> fmt_traits for $ty<$($gen),*> {
                 const LENGTH: usize = ($len); // parens required due to rustc parser weirdness
             }
         }
+    }
+}
 
-        $crate::serde_impl!($ty, $len $(, $gen: $gent)*);
-
-        impl<$($gen: $gent),*> $crate::_export::_core::convert::AsRef<[u8; { $len }]> for $ty<$($gen),*> {
-            #[inline]
-            fn as_ref(&self) -> &[u8; { $len }] { self.as_byte_array() }
-        }
-
-        impl<$($gen: $gent),*> $crate::_export::_core::convert::AsRef<[u8]> for $ty<$($gen),*> {
+/// Implements `fmt::Debug` using hex.
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_debug_only {
+    ($ty:ident, $len:expr, $reverse:expr $(, $gen:ident: $gent:ident)*) => {
+        impl<$($gen: $gent),*> $crate::_export::_core::fmt::Debug for $ty<$($gen),*> {
             #[inline]
-            fn as_ref(&self) -> &[u8] { self.as_byte_array() }
-        }
-
-        impl<$($gen: $gent),*> $crate::_export::_core::borrow::Borrow<[u8; { $len }]> for $ty<$($gen),*>  {
-            fn borrow(&self) -> &[u8; { $len }] {  self.as_byte_array() }
-        }
-
-        impl<$($gen: $gent),*> $crate::_export::_core::borrow::Borrow<[u8]> for $ty<$($gen),*>  {
-            fn borrow(&self) -> &[u8] {  self.as_byte_array() }
+            fn fmt(&self, f: &mut $crate::_export::_core::fmt::Formatter) -> $crate::_export::_core::fmt::Result {
+                $crate::debug_hex(self.as_byte_array(), f)
+            }
         }
     }
 }
@@ -433,6 +491,17 @@ fn visit_bytes<E>(self, v: &[u8]) -> core::result::Result<Self::Value, E>
     }
 }
 
+/// Implements `Serialize` and `Deserialize` for a new type created with [`crate::hash_newtype`] macro.
+#[macro_export]
+#[cfg(feature = "serde")]
+macro_rules! impl_serde_for_newtype {
+    ($($newtype:ident),*) => {
+        $(
+            $crate::serde_impl!($newtype, { <$newtype as $crate::Hash>::LEN });
+        )*
+    }
+}
+
 /// Implements `Serialize` and `Deserialize` for a type `$t` which
 /// represents a newtype over a byte-slice over length `$len`.
 #[doc(hidden)]
@@ -501,13 +570,29 @@ fn hash_borrow() {
         /// Test hash.
         struct TestHash(crate::sha256d::Hash);
     }
+    #[cfg(feature = "hex")]
+    crate::impl_hex_for_newtype!(TestHash);
+    #[cfg(not(feature = "hex"))]
+    crate::impl_debug_only_for_newtype!(TestHash);
 
     impl TestHash {
         fn all_zeros() -> Self { Self::from_byte_array([0; 32]) }
     }
 
+    // NB: This runs with and without `hex` feature enabled, testing different code paths for each.
+    #[test]
+    #[cfg(feature = "alloc")]
+    fn debug() {
+        use alloc::format;
+
+        let want = "0000000000000000000000000000000000000000000000000000000000000000";
+        let got = format!("{:?}", TestHash::all_zeros());
+        assert_eq!(got, want)
+    }
+
     #[test]
     #[cfg(feature = "alloc")]
+    #[cfg(feature = "hex")]
     fn display() {
         use alloc::format;
 
@@ -518,6 +603,7 @@ fn display() {
 
     #[test]
     #[cfg(feature = "alloc")]
+    #[cfg(feature = "hex")]
     fn display_alternate() {
         use alloc::format;
 
@@ -528,6 +614,7 @@ fn display_alternate() {
 
     #[test]
     #[cfg(feature = "alloc")]
+    #[cfg(feature = "hex")]
     fn lower_hex() {
         use alloc::format;
 
@@ -538,6 +625,7 @@ fn lower_hex() {
 
     #[test]
     #[cfg(feature = "alloc")]
+    #[cfg(feature = "hex")]
     fn lower_hex_alternate() {
         use alloc::format;
 
diff --git a/hashes/src/sha256.rs b/hashes/src/sha256.rs
index a040338f..98e8ec13 100644
--- a/hashes/src/sha256.rs
+++ b/hashes/src/sha256.rs
@@ -8,8 +8,6 @@
 use core::arch::x86_64::*;
 use core::{cmp, convert, fmt};
 
-use hex::DisplayHex;
-
 use crate::{incomplete_block_len, sha256d, HashEngine as _};
 #[cfg(doc)]
 use crate::{sha256t, sha256t_tag};
@@ -218,8 +216,15 @@ pub const fn hash_tag(tag: &[u8]) -> Self {
 
 impl fmt::Debug for Midstate {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        struct Encoder<'a> {
+            bytes: &'a [u8; 32],
+        }
+        impl fmt::Debug for Encoder<'_> {
+            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { crate::debug_hex(self.bytes, f) }
+        }
+
         f.debug_struct("Midstate")
-            .field("bytes", &self.bytes.as_hex())
+            .field("bytes", &Encoder { bytes: &self.bytes })
             .field("length", &self.bytes_hashed)
             .finish()
     }
diff --git a/hashes/src/sha256t.rs b/hashes/src/sha256t.rs
index 1b4188d2..cb3f4aaa 100644
--- a/hashes/src/sha256t.rs
+++ b/hashes/src/sha256t.rs
@@ -321,9 +321,14 @@ fn manually_created_sha256t_hash_type() {
         #[hash_newtype(backward)]
         struct NewTypeHashBackward(sha256t::Hash<NewTypeTagBackward>);
     }
+    #[cfg(feature = "hex")]
+    crate::impl_hex_for_newtype!(NewTypeHashBackward);
+    #[cfg(not(feature = "hex"))]
+    crate::impl_debug_only_for_newtype!(NewTypeHashBackward);
 
     #[test]
     #[cfg(feature = "alloc")]
+    #[cfg(feature = "hex")]
     fn macro_created_sha256t_hash_type_backward() {
         use alloc::string::ToString;
 
@@ -344,9 +349,14 @@ fn macro_created_sha256t_hash_type_backward() {
         #[hash_newtype(forward)]
         struct NewTypeHashForward(sha256t::Hash<NewTypeTagForward>);
     }
+    #[cfg(feature = "hex")]
+    crate::impl_hex_for_newtype!(NewTypeHashForward);
+    #[cfg(not(feature = "hex"))]
+    crate::impl_debug_only_for_newtype!(NewTypeHashForward);
 
     #[test]
     #[cfg(feature = "alloc")]
+    #[cfg(feature = "hex")]
     fn macro_created_sha256t_hash_type_prints_forward() {
         use alloc::string::ToString;
 
diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml
index 1387fc1d..fb60db40 100644
--- a/primitives/Cargo.toml
+++ b/primitives/Cargo.toml
@@ -22,7 +22,7 @@ serde = ["dep:serde", "hashes/serde", "internals/serde", "units/serde", "alloc"]
 arbitrary = ["dep:arbitrary", "units/arbitrary"]
 
 [dependencies]
-hashes = { package = "bitcoin_hashes", version = "0.15.0", default-features = false, features = ["bitcoin-io"] }
+hashes = { package = "bitcoin_hashes", version = "0.15.0", default-features = false, features = ["bitcoin-io", "hex"] }
 hex = { package = "hex-conservative", version = "0.3.0", default-features = false }
 internals = { package = "bitcoin-internals", version = "0.4.0" }
 io = { package = "bitcoin-io", version = "0.2.0", default-features = false }
diff --git a/primitives/src/block.rs b/primitives/src/block.rs
index 20475c94..9edea147 100644
--- a/primitives/src/block.rs
+++ b/primitives/src/block.rs
@@ -162,6 +162,10 @@ fn default() -> Version { Self::NO_SOFT_FORK_SIGNALLING }
     pub struct WitnessCommitment(sha256d::Hash);
 }
 
+hashes::impl_hex_for_newtype!(BlockHash, WitnessCommitment);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(BlockHash, WitnessCommitment);
+
 impl BlockHash {
     /// Dummy hash used as the previous blockhash of the genesis block.
     pub const GENESIS_PREVIOUS_BLOCK_HASH: Self = Self::from_byte_array([0; 32]);
diff --git a/primitives/src/merkle_tree.rs b/primitives/src/merkle_tree.rs
index 763ace1f..18a16a5a 100644
--- a/primitives/src/merkle_tree.rs
+++ b/primitives/src/merkle_tree.rs
@@ -10,3 +10,7 @@
     /// A hash corresponding to the Merkle tree root for witness data.
     pub struct WitnessMerkleNode(sha256d::Hash);
 }
+
+hashes::impl_hex_for_newtype!(TxMerkleNode, WitnessMerkleNode);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(TxMerkleNode, WitnessMerkleNode);
diff --git a/primitives/src/taproot.rs b/primitives/src/taproot.rs
index 452b10af..743980dc 100644
--- a/primitives/src/taproot.rs
+++ b/primitives/src/taproot.rs
@@ -18,6 +18,10 @@
     pub struct TapLeafHash(sha256t::Hash<TapLeafTag>);
 }
 
+hashes::impl_hex_for_newtype!(TapLeafHash);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(TapLeafHash);
+
 sha256t_tag! {
     pub struct TapBranchTag = hash_str("TapBranch");
 }
@@ -29,6 +33,10 @@
     pub struct TapNodeHash(sha256t::Hash<TapBranchTag>);
 }
 
+hashes::impl_hex_for_newtype!(TapNodeHash);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(TapNodeHash);
+
 sha256t_tag! {
     pub struct TapTweakTag = hash_str("TapTweak");
 }
@@ -40,6 +48,10 @@
     pub struct TapTweakHash(sha256t::Hash<TapTweakTag>);
 }
 
+hashes::impl_hex_for_newtype!(TapTweakHash);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(TapTweakHash);
+
 impl From<TapLeafHash> for TapNodeHash {
     fn from(leaf: TapLeafHash) -> TapNodeHash { TapNodeHash::from_byte_array(leaf.to_byte_array()) }
 }
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index 40f1da73..68b7d239 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -113,18 +113,6 @@ impl Transaction {
     /// Maximum transaction weight for Bitcoin Core 25.0.
     pub const MAX_STANDARD_WEIGHT: Weight = Weight::from_wu(400_000);
 
-    /// Returns a reference to the transaction inputs.
-    pub fn inputs(&self) -> &[TxIn] { &self.input }
-
-    /// Returns a mutable reference to the transaction inputs.
-    pub fn inputs_mut(&mut self) -> &mut [TxIn] { &mut self.input }
-
-    /// Returns a reference to the transaction outputs.
-    pub fn outputs(&self) -> &[TxOut] { &self.output }
-
-    /// Returns a mutable reference to the transaction outputs.
-    pub fn outputs_mut(&mut self) -> &mut [TxOut] { &mut self.output }
-
     /// Computes a "normalized TXID" which does not include any signatures.
     ///
     /// This gives a way to identify a transaction that is "the same" as
@@ -488,6 +476,10 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
     pub struct Wtxid(sha256d::Hash);
 }
 
+hashes::impl_hex_for_newtype!(Txid, Wtxid);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(Txid, Wtxid);
+
 impl Txid {
     /// The `Txid` used in a coinbase prevout.
     ///
diff --git a/primitives/src/witness.rs b/primitives/src/witness.rs
index 4eb025bf..95737c7c 100644
--- a/primitives/src/witness.rs
+++ b/primitives/src/witness.rs
@@ -192,15 +192,6 @@ pub fn second_to_last(&self) -> Option<&[u8]> {
         }
     }
 
-    /// Returns the third-to-last element in the witness, if any.
-    pub fn third_to_last(&self) -> Option<&[u8]> {
-        if self.witness_elements <= 2 {
-            None
-        } else {
-            self.nth(self.witness_elements - 3)
-        }
-    }
-
     /// Return the nth element in the witness, if any
     pub fn nth(&self, index: usize) -> Option<&[u8]> {
         let pos = decode_cursor(&self.content, self.indices_start, index)?;
diff --git a/units/src/fee_rate.rs b/units/src/fee_rate.rs
index f6e9bddc..3cf0269b 100644
--- a/units/src/fee_rate.rs
+++ b/units/src/fee_rate.rs
@@ -62,9 +62,6 @@ pub fn from_sat_per_vb(sat_vb: u64) -> Option<Self> {
     /// Constructs a new [`FeeRate`] from satoshis per virtual bytes without overflow check.
     pub const fn from_sat_per_vb_unchecked(sat_vb: u64) -> Self { FeeRate(sat_vb * (1000 / 4)) }
 
-    /// Constructs a new [`FeeRate`] from satoshis per kilo virtual bytes (1,000 vbytes).
-    pub const fn from_sat_per_kvb(sat_kvb: u64) -> Self { FeeRate(sat_kvb / 4) }
-
     /// Returns raw fee rate.
     ///
     /// Can be used instead of `into()` to avoid inference issues.
diff --git a/units/src/weight.rs b/units/src/weight.rs
index 7e1e0b7a..b43d7634 100644
--- a/units/src/weight.rs
+++ b/units/src/weight.rs
@@ -55,12 +55,8 @@ pub const fn from_wu_usize(wu: usize) -> Self { Weight(wu as u64) }
     pub fn from_kwu(wu: u64) -> Option<Self> { wu.checked_mul(1000).map(Weight) }
 
     /// Constructs a new [`Weight`] from virtual bytes, returning [`None`] if an overflow occurred.
-    pub const fn from_vb(vb: u64) -> Option<Self> {
-        // No `map()` in const context.
-        match vb.checked_mul(Self::WITNESS_SCALE_FACTOR) {
-            Some(wu) => Some(Weight::from_wu(wu)),
-            None => None,
-        }
+    pub fn from_vb(vb: u64) -> Option<Self> {
+        vb.checked_mul(Self::WITNESS_SCALE_FACTOR).map(Weight::from_wu)
     }
 
     /// Constructs a new [`Weight`] from virtual bytes panicking if an overflow occurred.
@@ -105,46 +101,22 @@ pub const fn to_vbytes_ceil(self) -> u64 {
     /// Checked addition.
     ///
     /// Computes `self + rhs` returning [`None`] if an overflow occurred.
-    pub const fn checked_add(self, rhs: Self) -> Option<Self> {
-        // No `map()` in const context.
-        match self.0.checked_add(rhs.0) {
-            Some(wu) => Some(Weight::from_wu(wu)),
-            None => None
-        }
-    }
+    pub fn checked_add(self, rhs: Self) -> Option<Self> { self.0.checked_add(rhs.0).map(Self) }
 
     /// Checked subtraction.
     ///
     /// Computes `self - rhs` returning [`None`] if an overflow occurred.
-    pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
-        // No `map()` in const context.
-        match self.0.checked_sub(rhs.0) {
-            Some(wu) => Some(Weight::from_wu(wu)),
-            None => None
-        }
-    }
+    pub fn checked_sub(self, rhs: Self) -> Option<Self> { self.0.checked_sub(rhs.0).map(Self) }
 
     /// Checked multiplication.
     ///
     /// Computes `self * rhs` returning [`None`] if an overflow occurred.
-    pub const fn checked_mul(self, rhs: u64) -> Option<Self> {
-        // No `map()` in const context.
-        match self.0.checked_mul(rhs) {
-            Some(wu) => Some(Weight::from_wu(wu)),
-            None => None
-        }
-    }
+    pub fn checked_mul(self, rhs: u64) -> Option<Self> { self.0.checked_mul(rhs).map(Self) }
 
     /// Checked division.
     ///
     /// Computes `self / rhs` returning [`None`] if `rhs == 0`.
-    pub const fn checked_div(self, rhs: u64) -> Option<Self> {
-        // No `map()` in const context.
-        match self.0.checked_div(rhs) {
-            Some(wu) => Some(Weight::from_wu(wu)),
-            None => None
-        }
-    }
+    pub fn checked_div(self, rhs: u64) -> Option<Self> { self.0.checked_div(rhs).map(Self) }
 }
 
 /// Alternative will display the unit.


Test patch:
diff --git a/hashes/tests/io.rs b/hashes/tests/io.rs
index ffaa8067..9151bba3 100644
--- a/hashes/tests/io.rs
+++ b/hashes/tests/io.rs
@@ -3,6 +3,7 @@
 //! Test the `bitcoin-io` implementations.
 
 #![cfg(feature = "bitcoin-io")]
+#![cfg(feature = "hex")]
 
 use bitcoin_hashes::{
     hash160, hmac, ripemd160, sha1, sha256, sha256d, sha384, sha512, sha512_256, siphash24,
diff --git a/hashes/tests/regression.rs b/hashes/tests/regression.rs
index 360bfd7f..d40998e3 100644
--- a/hashes/tests/regression.rs
+++ b/hashes/tests/regression.rs
@@ -2,6 +2,8 @@
 //!
 //! Note that if `bitcoin-io` is enabled then we get more regression-like testing from `./io.rs`.
 
+#![cfg(feature = "hex")]
+
 use bitcoin_hashes::{
     hash160, ripemd160, sha1, sha256, sha256d, sha256t, sha384, sha512, sha512_256, siphash24,
     GeneralHash as _, HashEngine as _, Hmac, HmacEngine,