Repository: rust-bitcoin/rust-bitcoin
Pull request #1833 (2023-05-04 04:50:27)

Use new `hex-conservative` crate
Use the newly released `hex-conservative` crate, by doing the following:
    
- Depend on `hex-conservative` in `bitcoin` and `hashes`
- Re-export `hex-conservative` as `hex` from both crate roots.
- Remove all the old hex code from `hashes`
- Remove all the old hex code from `internals`
- Remove the now unused `internals::prelude`
- Fix all the import statements (makes up the bulk of the lines changes in this patch)


apoelstra: I think we should rename it to `Iter` and yeah, we should export it at the crate root.
apoelstra: Nice! This looks good now.
tcharding: Thanks man, looks sweet huh.
tcharding: This will need the lock files updating still, hopefully https://github.com/rust-bitcoin/rust-bitcoin/pull/1916 will be in by then so msrv pinning stuff is resolved.
apoelstra: I think we should do the hex stuff before any other releases.
tcharding: Something is wrong with the `core2` build and I can't seem to see it, does anything cry out?
```diff
git diff @~ hashes/Cargo.toml
diff --git a/hashes/Cargo.toml b/hashes/Cargo.toml
index 0516af97..ac8591b9 100644
--- a/hashes/Cargo.toml
+++ b/hashes/Cargo.toml
@@ -14,9 +14,10 @@ exclude = ["tests", "contrib"]
 
 [features]
 default = ["std"]
-std = ["alloc", "internals/std"]
-alloc = ["internals/alloc"]
+std = ["alloc", "internals/std", "hex/std"]
+alloc = ["internals/alloc", "hex/alloc"]
 serde-std = ["serde/std"]
+core2 = ["actual-core2", "hex/core2"]
 
 [package.metadata.docs.rs]
 all-features = true
@@ -24,12 +25,15 @@ rustdoc-args = ["--cfg", "docsrs"]
 
 [dependencies]
 internals = { path = "../internals", package = "bitcoin-internals", version = "0.1.0" }
+hex = { package = "hex-conservative", version = "0.1.0", default-features = false }
 
-core2 = { version = "0.3.0", default_features = false, optional = true }
 schemars = { version = "0.8.3", optional = true }
 # Only enable this if you explicitly do not want to use "std", otherwise enable "serde-std".
 serde = { version = "1.0", default-features = false, optional = true }
 
+# Do NOT use this feature! Use the `core2` feature instead.
+actual-core2 = { package = "core2", version = "0.3.0", default-features = false, optional = true }
+
```

apoelstra: @tcharding did you update Cargo-recent.lock and Cargo-minimal.lock after making those changes to Cargo.toml?
tcharding: Yeah I did, I couldn't get `hashes` to build even with `cargo check` locally so I figured it was something brain dead and I was just blind to it. I'll play more with it today.
tcharding: FTR the solution was
```rust
extern crate actual_core2 as core2;
```
But yesterday I hade `extern crate core2;`
apoelstra: Awesome, glad CI is passing.

I'm temped though to get hex-conservative 0.1.1 out first (with the new `hex!` macro and fixing the `fmt` impls to respect `{:8}`)
tcharding: Agree, mind if I call it v0.2.0? I've given up understanding Rust semver rules for under v1.0, just feels too trivial increasing only the patch number :)
apoelstra: LGTM. May be a bit easier to review split into multiple commits, one for each crate, and a final one which deletes the hex stuff from -internals.
tcharding: Can do!
tcharding: I had a go but we can't split it up without removing the workspace patch of `hashes` because of the `hash_newtype!` macro.
tcharding: I pulled the `internals` changes out into a separate patch, its all red :) 

Patch:
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index 2a6ea499..ea1e7cee 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -41,6 +41,7 @@ dependencies = [
  "bitcoin_hashes",
  "bitcoinconsensus",
  "core2",
+ "hex-conservative",
  "hex_lit",
  "mutagen",
  "secp256k1",
@@ -74,6 +75,7 @@ version = "0.12.0"
 dependencies = [
  "bitcoin-internals",
  "core2",
+ "hex-conservative",
  "schemars",
  "serde",
  "serde_json",
@@ -163,6 +165,15 @@ version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
 
+[[package]]
+name = "hex-conservative"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
+dependencies = [
+ "core2",
+]
+
 [[package]]
 name = "hex_lit"
 version = "0.1.1"
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index 9f8f80e7..31b69aaa 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -40,6 +40,7 @@ dependencies = [
  "bitcoin_hashes",
  "bitcoinconsensus",
  "core2",
+ "hex-conservative",
  "hex_lit",
  "mutagen",
  "secp256k1",
@@ -73,6 +74,7 @@ version = "0.12.0"
 dependencies = [
  "bitcoin-internals",
  "core2",
+ "hex-conservative",
  "schemars",
  "serde",
  "serde_json",
@@ -156,6 +158,15 @@ version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
 
+[[package]]
+name = "hex-conservative"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
+dependencies = [
+ "core2",
+]
+
 [[package]]
 name = "hex_lit"
 version = "0.1.1"
diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml
index 0b5f47fe..2c33b1b3 100644
--- a/bitcoin/Cargo.toml
+++ b/bitcoin/Cargo.toml
@@ -26,8 +26,8 @@ bitcoinconsensus-std = ["bitcoinconsensus/std", "std"]
 # The no-std feature doesn't disable std - you need to turn off the std feature for that by disabling default.
 # Instead no-std enables additional features required for this crate to be usable without std.
 # As a result, both can be enabled without conflict.
-std = ["secp256k1/std", "hashes/std", "bech32/std", "internals/std"]
-no-std = ["core2", "hashes/alloc", "hashes/core2", "secp256k1/alloc"]
+std = ["secp256k1/std", "hashes/std", "bech32/std", "internals/std", "hex/std"]
+no-std = ["core2", "hashes/alloc", "hashes/core2", "secp256k1/alloc", "hex/alloc", "hex/core2"]
 
 [package.metadata.docs.rs]
 all-features = true
@@ -35,6 +35,7 @@ rustdoc-args = ["--cfg", "docsrs"]
 
 [dependencies]
 internals = { package = "bitcoin-internals", version = "0.2.0" }
+hex = { package = "hex-conservative", version = "0.1.1", default-features = false }
 bech32 = { version = "0.9.0", default-features = false }
 hashes = { package = "bitcoin_hashes", version = "0.12.0", default-features = false }
 secp256k1 = { version = "0.27.0", default-features = false, features = ["bitcoin_hashes"] }
@@ -42,6 +43,8 @@ hex_lit = "0.1.1"
 
 base64 = { version = "0.13.0", optional = true }
 bitcoinconsensus = { version = "0.20.2-0.5.0", default-features = false, optional = true }
+
+# There is no reason to use this dependency directly, it is activated by the "no-std" feature.
 core2 = { version = "0.3.2", default-features = false, features = ["alloc"], optional = true }
 # Do NOT use this as a feature! Use the `serde` feature instead.
 actual-serde = { package = "serde", version = "1.0.103", default-features = false, features = [ "derive", "alloc" ], optional = true }
diff --git a/bitcoin/examples/bip32.rs b/bitcoin/examples/bip32.rs
index 7904c5f6..f9c81343 100644
--- a/bitcoin/examples/bip32.rs
+++ b/bitcoin/examples/bip32.rs
@@ -5,7 +5,7 @@
 
 use bitcoin::address::Address;
 use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey};
-use bitcoin::hashes::hex::FromHex;
+use bitcoin::hex::FromHex;
 use bitcoin::secp256k1::ffi::types::AlignedType;
 use bitcoin::secp256k1::Secp256k1;
 use bitcoin::PublicKey;
diff --git a/bitcoin/src/bip152.rs b/bitcoin/src/bip152.rs
index 380a8402..b04f352e 100644
--- a/bitcoin/src/bip152.rs
+++ b/bitcoin/src/bip152.rs
@@ -371,7 +371,7 @@ pub fn from_request(
 
 #[cfg(test)]
 mod test {
-    use hashes::hex::FromHex;
+    use hex::FromHex;
 
     use super::*;
     use crate::blockdata::locktime::absolute;
diff --git a/bitcoin/src/bip32.rs b/bitcoin/src/bip32.rs
index dc381dc0..036b56af 100644
--- a/bitcoin/src/bip32.rs
+++ b/bitcoin/src/bip32.rs
@@ -12,7 +12,7 @@
 use core::str::FromStr;
 use core::{fmt, slice};
 
-use hashes::{hex, sha512, Hash, HashEngine, Hmac, HmacEngine};
+use hashes::{sha512, Hash, HashEngine, Hmac, HmacEngine};
 use internals::{impl_array_newtype, write_err};
 use secp256k1::{self, Secp256k1, XOnlyPublicKey};
 #[cfg(feature = "serde")]
@@ -468,7 +468,7 @@ pub enum Error {
     /// Base58 encoding error
     Base58(base58::Error),
     /// Hexadecimal decoding error
-    Hex(hex::Error),
+    Hex(hex::HexToArrayError),
     /// `PublicKey` hex should be 66 or 130 digits long.
     InvalidPublicKeyHexLength(usize),
 }
diff --git a/bitcoin/src/blockdata/block.rs b/bitcoin/src/blockdata/block.rs
index dcacffec..921565e6 100644
--- a/bitcoin/src/blockdata/block.rs
+++ b/bitcoin/src/blockdata/block.rs
@@ -423,7 +423,7 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
 
 #[cfg(test)]
 mod tests {
-    use hashes::hex::FromHex;
+    use hex::FromHex;
 
     use super::*;
     use crate::consensus::encode::{deserialize, serialize};
diff --git a/bitcoin/src/blockdata/script/mod.rs b/bitcoin/src/blockdata/script/mod.rs
index bc4c41a6..acb94039 100644
--- a/bitcoin/src/blockdata/script/mod.rs
+++ b/bitcoin/src/blockdata/script/mod.rs
@@ -513,7 +513,7 @@ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     {
         use core::fmt::Formatter;
 
-        use hashes::hex::FromHex;
+        use hex::FromHex;
 
         if deserializer.is_human_readable() {
             struct Visitor;
diff --git a/bitcoin/src/blockdata/script/owned.rs b/bitcoin/src/blockdata/script/owned.rs
index eb6190ca..db917225 100644
--- a/bitcoin/src/blockdata/script/owned.rs
+++ b/bitcoin/src/blockdata/script/owned.rs
@@ -3,7 +3,6 @@
 #[cfg(doc)]
 use core::ops::Deref;
 
-use hashes::hex;
 use secp256k1::{Secp256k1, Verification};
 
 use crate::blockdata::opcodes::all::*;
@@ -156,8 +155,8 @@ pub fn new_op_return<T: AsRef<PushBytes>>(data: &T) -> Self {
     }
 
     /// Creates a [`ScriptBuf`] from a hex string.
-    pub fn from_hex(s: &str) -> Result<Self, hex::Error> {
-        use hashes::hex::FromHex;
+    pub fn from_hex(s: &str) -> Result<Self, hex::HexToBytesError> {
+        use hex::FromHex;
 
         let v = Vec::from_hex(s)?;
         Ok(ScriptBuf::from_bytes(v))
diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index 8418fbe6..fd95813a 100644
--- a/bitcoin/src/blockdata/transaction.rs
+++ b/bitcoin/src/blockdata/transaction.rs
@@ -95,7 +95,7 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 #[non_exhaustive]
 pub enum ParseOutPointError {
     /// Error in TXID part.
-    Txid(hashes::hex::Error),
+    Txid(hex::HexToArrayError),
     /// Error in vout part.
     Vout(crate::error::ParseIntError),
     /// Error in general format.
@@ -1420,7 +1420,7 @@ pub const fn from_slice(input_script_len: usize, witness_element_lengths: &[usiz
 mod tests {
     use core::str::FromStr;
 
-    use hashes::hex::FromHex;
+    use hex::FromHex;
 
     use super::*;
     use crate::blockdata::constants::WITNESS_SCALE_FACTOR;
diff --git a/bitcoin/src/blockdata/witness.rs b/bitcoin/src/blockdata/witness.rs
index d0b7408c..892de5fa 100644
--- a/bitcoin/src/blockdata/witness.rs
+++ b/bitcoin/src/blockdata/witness.rs
@@ -462,8 +462,8 @@ fn visit_seq<A: serde::de::SeqAccess<'de>>(
                 self,
                 mut a: A,
             ) -> Result<Self::Value, A::Error> {
-                use hashes::hex::Error::*;
-                use hashes::hex::FromHex;
+                use hex::FromHex;
+                use hex::HexToBytesError::*;
                 use serde::de::{self, Unexpected};
 
                 let mut ret = match a.size_hint() {
@@ -485,10 +485,6 @@ fn visit_seq<A: serde::de::SeqAccess<'de>>(
                         },
                         OddLengthString(len) =>
                             de::Error::invalid_length(len, &"an even length string"),
-                        InvalidLength(expected, got) => {
-                            let exp = format!("expected length: {}", expected);
-                            de::Error::invalid_length(got, &exp.as_str())
-                        }
                     })?;
                     ret.push(vec);
                 }
diff --git a/bitcoin/src/consensus/serde.rs b/bitcoin/src/consensus/serde.rs
index c2393635..2b857547 100644
--- a/bitcoin/src/consensus/serde.rs
+++ b/bitcoin/src/consensus/serde.rs
@@ -38,7 +38,7 @@ pub mod hex {
     use core::fmt;
     use core::marker::PhantomData;
 
-    use internals::hex::BufEncoder;
+    use hex::buf_encoder::BufEncoder;
 
     /// Marker for upper/lower case type-level flags ("type-level enum").
     ///
@@ -54,15 +54,15 @@ pub enum Upper {}
     mod sealed {
         pub trait Case {
             /// Internal detail, don't depend on it!!!
-            const INTERNAL_CASE: internals::hex::Case;
+            const INTERNAL_CASE: hex::Case;
         }
 
         impl Case for super::Lower {
-            const INTERNAL_CASE: internals::hex::Case = internals::hex::Case::Lower;
+            const INTERNAL_CASE: hex::Case = hex::Case::Lower;
         }
 
         impl Case for super::Upper {
-            const INTERNAL_CASE: internals::hex::Case = internals::hex::Case::Upper;
+            const INTERNAL_CASE: hex::Case = hex::Case::Upper;
         }
     }
 
@@ -102,18 +102,18 @@ fn flush<W: fmt::Write>(&mut self, writer: &mut W) -> fmt::Result {
 
     /// Error returned when a hex string decoder can't be created.
     #[derive(Debug)]
-    pub struct DecodeInitError(hashes::hex::Error);
+    pub struct DecodeInitError(hex::HexToBytesError);
 
     /// Error returned when a hex string contains invalid characters.
     #[derive(Debug)]
-    pub struct DecodeError(hashes::hex::Error);
+    pub struct DecodeError(hex::HexToBytesError);
 
     /// Hex decoder state.
-    pub struct Decoder<'a>(hashes::hex::HexIterator<'a>);
+    pub struct Decoder<'a>(hex::HexToBytesIter<'a>);
 
     impl<'a> Decoder<'a> {
         fn new(s: &'a str) -> Result<Self, DecodeInitError> {
-            match hashes::hex::HexIterator::new(s) {
+            match hex::HexToBytesIter::new(s) {
                 Ok(iter) => Ok(Decoder(iter)),
                 Err(error) => Err(DecodeInitError(error)),
             }
@@ -138,10 +138,10 @@ fn from_str(s: &'a str) -> Result<Self::Decoder, Self::InitError> { Decoder::new
 
     impl super::IntoDeError for DecodeInitError {
         fn into_de_error<E: serde::de::Error>(self) -> E {
-            use hashes::hex::Error;
+            use hex::HexToBytesError;
 
             match self.0 {
-                Error::OddLengthString(len) =>
+                HexToBytesError::OddLengthString(len) =>
                     E::invalid_length(len, &"an even number of ASCII-encoded hex digits"),
                 error => panic!("unexpected error: {:?}", error),
             }
@@ -150,15 +150,15 @@ fn into_de_error<E: serde::de::Error>(self) -> E {
 
     impl super::IntoDeError for DecodeError {
         fn into_de_error<E: serde::de::Error>(self) -> E {
-            use hashes::hex::Error;
+            use hex::HexToBytesError;
             use serde::de::Unexpected;
 
             const EXPECTED_CHAR: &str = "an ASCII-encoded hex digit";
 
             match self.0 {
-                Error::InvalidChar(c) if c.is_ascii() =>
+                HexToBytesError::InvalidChar(c) if c.is_ascii() =>
                     E::invalid_value(Unexpected::Char(c as _), &EXPECTED_CHAR),
-                Error::InvalidChar(c) =>
+                HexToBytesError::InvalidChar(c) =>
                     E::invalid_value(Unexpected::Unsigned(c.into()), &EXPECTED_CHAR),
                 error => panic!("unexpected error: {:?}", error),
             }
diff --git a/bitcoin/src/crypto/ecdsa.rs b/bitcoin/src/crypto/ecdsa.rs
index 65f94446..7f0e6d15 100644
--- a/bitcoin/src/crypto/ecdsa.rs
+++ b/bitcoin/src/crypto/ecdsa.rs
@@ -7,8 +7,7 @@
 use core::str::FromStr;
 use core::{fmt, iter};
 
-use hashes::hex::{self, FromHex};
-use internals::hex::display::DisplayHex;
+use hex::FromHex;
 use internals::write_err;
 use secp256k1;
 
@@ -188,8 +187,8 @@ fn into_iter(self) -> Self::IntoIter { (*self).iter() }
 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
 #[non_exhaustive]
 pub enum Error {
-    /// Hex encoding error
-    HexEncoding(hex::Error),
+    /// Hex decoding error
+    Hex(hex::HexToBytesError),
     /// Base58 encoding error
     NonStandardSighashType(u32),
     /// Empty Signature
@@ -201,7 +200,7 @@ pub enum Error {
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match *self {
-            Error::HexEncoding(ref e) => write_err!(f, "Signature hex encoding error"; e),
+            Error::Hex(ref e) => write_err!(f, "Signature hex decoding error"; e),
             Error::NonStandardSighashType(hash_ty) =>
                 write!(f, "Non standard signature hash type {}", hash_ty),
             Error::EmptySignature => write!(f, "Empty ECDSA signature"),
@@ -216,7 +215,7 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         use self::Error::*;
 
         match self {
-            HexEncoding(e) => Some(e),
+            Hex(e) => Some(e),
             Secp256k1(e) => Some(e),
             NonStandardSighashType(_) | EmptySignature => None,
         }
@@ -231,6 +230,6 @@ impl From<NonStandardSighashType> for Error {
     fn from(err: NonStandardSighashType) -> Self { Error::NonStandardSighashType(err.0) }
 }
 
-impl From<hex::Error> for Error {
-    fn from(err: hex::Error) -> Self { Error::HexEncoding(err) }
+impl From<hex::HexToBytesError> for Error {
+    fn from(err: hex::HexToBytesError) -> Self { Error::Hex(err) }
 }
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index 5108940e..216bff51 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -9,8 +9,8 @@
 use core::ops;
 use core::str::FromStr;
 
-use hashes::hex::FromHex;
-use hashes::{hash160, hex, Hash};
+use hashes::{hash160, Hash};
+use hex::FromHex;
 use internals::write_err;
 #[cfg(feature = "rand-std")]
 pub use secp256k1::rand;
@@ -34,7 +34,7 @@ pub enum Error {
     /// Invalid key prefix error
     InvalidKeyPrefix(u8),
     /// Hex decoding error
-    Hex(hex::Error),
+    Hex(hex::HexToArrayError),
     /// `PublicKey` hex should be 66 or 130 digits long.
     InvalidHexLength(usize),
 }
@@ -74,8 +74,8 @@ impl From<secp256k1::Error> for Error {
     fn from(e: secp256k1::Error) -> Error { Error::Secp256k1(e) }
 }
 
-impl From<hex::Error> for Error {
-    fn from(e: hex::Error) -> Self { Error::Hex(e) }
+impl From<hex::HexToArrayError> for Error {
+    fn from(e: hex::HexToArrayError) -> Self { Error::Hex(e) }
 }
 
 /// A Bitcoin ECDSA public key
@@ -731,7 +731,7 @@ fn from(pair: TweakedKeyPair) -> Self { TweakedPublicKey::from_keypair(pair) }
 mod tests {
     use std::str::FromStr;
 
-    use hashes::hex::FromHex;
+    use hex::FromHex;
     use secp256k1::Secp256k1;
 
     use super::*;
diff --git a/bitcoin/src/crypto/sighash.rs b/bitcoin/src/crypto/sighash.rs
index fe4f757d..93c00da9 100644
--- a/bitcoin/src/crypto/sighash.rs
+++ b/bitcoin/src/crypto/sighash.rs
@@ -1110,8 +1110,8 @@ fn is_invalid_use_of_sighash_single(sighash: u32, input_index: usize, output_len
 mod tests {
     use std::str::FromStr;
 
-    use hashes::hex::FromHex;
     use hashes::HashEngine;
+    use hex::FromHex;
 
     use super::*;
     use crate::address::Address;
diff --git a/bitcoin/src/internal_macros.rs b/bitcoin/src/internal_macros.rs
index ae0e54f6..52c446f8 100644
--- a/bitcoin/src/internal_macros.rs
+++ b/bitcoin/src/internal_macros.rs
@@ -51,7 +51,7 @@ fn consensus_decode<R: $crate::io::Read + ?Sized>(
 #[cfg(test)]
 mod test_macros {
 
-    macro_rules! hex (($hex:expr) => (<Vec<u8> as hashes::hex::FromHex>::from_hex($hex).unwrap()));
+    macro_rules! hex (($hex:expr) => (<Vec<u8> as $crate::hex::FromHex>::from_hex($hex).unwrap()));
     pub(crate) use hex;
 }
 
@@ -61,7 +61,7 @@ mod test_macros {
 /// - core::fmt::UpperHex
 /// - core::fmt::Display
 /// - core::str::FromStr
-/// - hashes::hex::FromHex
+/// - hex::FromHex
 macro_rules! impl_bytes_newtype {
     ($t:ident, $len:literal) => {
         impl $t {
@@ -83,14 +83,14 @@ fn check_copy<T: Copy>() {}
 
         impl core::fmt::LowerHex for $t {
             fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-                use internals::hex::{display, Case};
+                use $crate::hex::{display, Case};
                 display::fmt_hex_exact!(f, $len, &self.0, Case::Lower)
             }
         }
 
         impl core::fmt::UpperHex for $t {
             fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-                use internals::hex::{display, Case};
+                use $crate::hex::{display, Case};
                 display::fmt_hex_exact!(f, $len, &self.0, Case::Upper)
             }
         }
@@ -107,30 +107,22 @@ fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
             }
         }
 
-        impl $crate::hashes::hex::FromHex for $t {
-            fn from_byte_iter<I>(iter: I) -> Result<Self, $crate::hashes::hex::Error>
+        impl $crate::hex::FromHex for $t {
+            type Err = $crate::hex::HexToArrayError;
+
+            fn from_byte_iter<I>(iter: I) -> Result<Self, $crate::hex::HexToArrayError>
             where
-                I: core::iter::Iterator<Item = Result<u8, $crate::hashes::hex::Error>>
+                I: core::iter::Iterator<Item = Result<u8, $crate::hex::HexToBytesError>>
                     + core::iter::ExactSizeIterator
                     + core::iter::DoubleEndedIterator,
             {
-                if iter.len() == $len {
-                    let mut ret = [0; $len];
-                    for (n, byte) in iter.enumerate() {
-                        ret[n] = byte?;
-                    }
-                    Ok($t(ret))
-                } else {
-                    Err($crate::hashes::hex::Error::InvalidLength(2 * $len, 2 * iter.len()))
-                }
+                Ok($t($crate::hex::FromHex::from_byte_iter(iter)?))
             }
         }
 
         impl core::str::FromStr for $t {
-            type Err = $crate::hashes::hex::Error;
-            fn from_str(s: &str) -> Result<Self, Self::Err> {
-                $crate::hashes::hex::FromHex::from_hex(s)
-            }
+            type Err = $crate::hex::HexToArrayError;
+            fn from_str(s: &str) -> Result<Self, Self::Err> { $crate::hex::FromHex::from_hex(s) }
         }
 
         #[cfg(feature = "serde")]
@@ -164,7 +156,7 @@ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
                             use $crate::serde::de::Unexpected;
 
                             if let Ok(hex) = core::str::from_utf8(v) {
-                                $crate::hashes::hex::FromHex::from_hex(hex).map_err(E::custom)
+                                $crate::hex::FromHex::from_hex(hex).map_err(E::custom)
                             } else {
                                 return Err(E::invalid_value(Unexpected::Bytes(v), &self));
                             }
@@ -174,7 +166,7 @@ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
                         where
                             E: $crate::serde::de::Error,
                         {
-                            $crate::hashes::hex::FromHex::from_hex(v).map_err(E::custom)
+                            $crate::hex::FromHex::from_hex(v).map_err(E::custom)
                         }
                     }
 
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index 5b3ef180..af6e9ed2 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -66,9 +66,15 @@
 /// Bitcoin's libbitcoinconsensus with Rust binding.
 pub extern crate bitcoinconsensus;
 
+#[cfg(not(feature = "std"))]
+extern crate core2;
+
 /// Rust implementation of cryptographic hash function algorithems.
 pub extern crate hashes;
 
+/// Re-export the `hex-conservative` crate.
+pub extern crate hex;
+
 /// Rust wrapper library for Pieter Wuille's libsecp256k1.  Implements ECDSA and BIP 340 signatures
 /// for the SECG elliptic curve group secp256k1 and related utilities.
 pub extern crate secp256k1;
@@ -186,7 +192,7 @@ mod prelude {
     #[cfg(not(feature = "std"))]
     pub use crate::io_extras::sink;
 
-    pub use internals::hex::display::DisplayHex;
+    pub use hex::DisplayHex;
 }
 
 #[cfg(bench)]
diff --git a/bitcoin/src/merkle_tree/block.rs b/bitcoin/src/merkle_tree/block.rs
index 8d031ef8..1a45eb1e 100644
--- a/bitcoin/src/merkle_tree/block.rs
+++ b/bitcoin/src/merkle_tree/block.rs
@@ -13,7 +13,7 @@
 //!
 //! ```rust
 //! use bitcoin::hash_types::Txid;
-//! use bitcoin::hashes::hex::FromHex;
+//! use bitcoin::hex::FromHex;
 //! use bitcoin::{Block, MerkleBlock};
 //!
 //! // Get the proof from a bitcoind by running in the terminal:
@@ -73,7 +73,7 @@ impl MerkleBlock {
     ///
     /// ```rust
     /// use bitcoin::hash_types::Txid;
-    /// use bitcoin::hashes::hex::FromHex;
+    /// use bitcoin::hex::FromHex;
     /// use bitcoin::{Block, MerkleBlock};
     ///
     /// // Block 80000
@@ -223,7 +223,7 @@ pub fn hashes(&self) -> &Vec<TxMerkleNode> { &self.hashes }
     ///
     /// ```rust
     /// use bitcoin::hash_types::Txid;
-    /// use bitcoin::hashes::hex::FromHex;
+    /// use bitcoin::hex::FromHex;
     /// use bitcoin::merkle_tree::{MerkleBlock, PartialMerkleTree};
     ///
     /// // Block 80000
@@ -751,7 +751,7 @@ fn damage(&mut self, rng: &mut ThreadRng) {
     /// Returns a real block (0000000000013b8ab2cd513b0261a14096412195a72a0c4827d229dcc7e0f7af)
     /// with 9 txs.
     fn get_block_13b8a() -> Block {
-        use hashes::hex::FromHex;
+        use hex::FromHex;
         let block_hex = include_str!("../../tests/data/block_13b8a.hex");
         deserialize(&Vec::from_hex(block_hex).unwrap()).unwrap()
     }
diff --git a/bitcoin/src/network/address.rs b/bitcoin/src/network/address.rs
index 3dec4f14..f20907c3 100644
--- a/bitcoin/src/network/address.rs
+++ b/bitcoin/src/network/address.rs
@@ -306,7 +306,7 @@ mod test {
     use core::str::FromStr;
     use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
 
-    use hashes::hex::FromHex;
+    use hex::FromHex;
 
     use super::{AddrV2, AddrV2Message, Address};
     use crate::consensus::encode::{deserialize, serialize};
diff --git a/bitcoin/src/network/constants.rs b/bitcoin/src/network/constants.rs
index 202c2d69..882592a2 100644
--- a/bitcoin/src/network/constants.rs
+++ b/bitcoin/src/network/constants.rs
@@ -31,7 +31,7 @@
 use core::str::FromStr;
 use core::{fmt, ops};
 
-use hashes::hex::{Error, FromHex};
+use hex::FromHex;
 use internals::{debug_from_display, write_err};
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Serialize};
@@ -313,7 +313,7 @@ impl Magic {
 #[derive(Debug, PartialEq, Eq, Clone)]
 pub struct ParseMagicError {
     /// The error that occurred when parsing the string.
-    error: Error,
+    error: hex::HexToArrayError,
     /// The byte string that failed to parse.
     magic: String,
 }
@@ -362,7 +362,7 @@ fn try_from(magic: Magic) -> Result<Self, Self::Error> {
 
 impl fmt::Display for Magic {
     fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
-        internals::fmt_hex_exact!(f, 4, &self.0, internals::hex::Case::Lower)?;
+        hex::fmt_hex_exact!(f, 4, &self.0, hex::Case::Lower)?;
         Ok(())
     }
 }
@@ -370,14 +370,14 @@ fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
 
 impl fmt::LowerHex for Magic {
     fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
-        internals::fmt_hex_exact!(f, 4, &self.0, internals::hex::Case::Lower)?;
+        hex::fmt_hex_exact!(f, 4, &self.0, hex::Case::Lower)?;
         Ok(())
     }
 }
 
 impl fmt::UpperHex for Magic {
     fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
-        internals::fmt_hex_exact!(f, 4, &self.0, internals::hex::Case::Upper)?;
+        hex::fmt_hex_exact!(f, 4, &self.0, hex::Case::Upper)?;
         Ok(())
     }
 }
diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs
index d190bda8..2e647909 100644
--- a/bitcoin/src/pow.rs
+++ b/bitcoin/src/pow.rs
@@ -790,13 +790,13 @@ macro_rules! impl_hex {
     ($hex:ident, $case:expr) => {
         impl $hex for U256 {
             fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                internals::hex::display::fmt_hex_exact!(f, 32, &self.to_be_bytes(), $case)
+                hex::fmt_hex_exact!(f, 32, &self.to_be_bytes(), $case)
             }
         }
     };
 }
-impl_hex!(LowerHex, internals::hex::Case::Lower);
-impl_hex!(UpperHex, internals::hex::Case::Upper);
+impl_hex!(LowerHex, hex::Case::Lower);
+impl_hex!(UpperHex, hex::Case::Upper);
 
 #[cfg(feature = "serde")]
 impl crate::serde::Serialize for U256 {
@@ -825,7 +825,7 @@ impl<'de> crate::serde::Deserialize<'de> for U256 {
     fn deserialize<D: crate::serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
         use core::convert::TryInto;
 
-        use hashes::hex::FromHex;
+        use hex::FromHex;
 
         use crate::serde::de;
 
diff --git a/bitcoin/src/psbt/macros.rs b/bitcoin/src/psbt/macros.rs
index 02bbd8bf..328bdb4d 100644
--- a/bitcoin/src/psbt/macros.rs
+++ b/bitcoin/src/psbt/macros.rs
@@ -4,7 +4,7 @@
 macro_rules! hex_psbt {
     ($s:expr) => {
         <$crate::psbt::Psbt>::deserialize(
-            &<$crate::prelude::Vec<u8> as $crate::hashes::hex::FromHex>::from_hex($s).unwrap(),
+            &<$crate::prelude::Vec<u8> as $crate::hex::FromHex>::from_hex($s).unwrap(),
         )
     };
 }
diff --git a/bitcoin/src/psbt/serialize.rs b/bitcoin/src/psbt/serialize.rs
index 28ac1d85..dfca1b6c 100644
--- a/bitcoin/src/psbt/serialize.rs
+++ b/bitcoin/src/psbt/serialize.rs
@@ -178,9 +178,7 @@ fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
             ecdsa::Error::EmptySignature => Error::InvalidEcdsaSignature(e),
             ecdsa::Error::NonStandardSighashType(flag) => Error::NonStandardSighashType(flag),
             ecdsa::Error::Secp256k1(..) => Error::InvalidEcdsaSignature(e),
-            ecdsa::Error::HexEncoding(..) => {
-                unreachable!("Decoding from slice, not hex")
-            }
+            ecdsa::Error::Hex(..) => unreachable!("Decoding from slice, not hex"),
         })
     }
 }
diff --git a/bitcoin/src/serde_utils.rs b/bitcoin/src/serde_utils.rs
index e3cdee7a..c0789e0c 100644
--- a/bitcoin/src/serde_utils.rs
+++ b/bitcoin/src/serde_utils.rs
@@ -12,7 +12,7 @@ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: serde::Serializer,
     {
-        use internals::hex::display::DisplayHex;
+        use hex::DisplayHex;
 
         serializer.collect_str(&format_args!("{:x}", self.0.as_hex()))
     }
@@ -24,7 +24,7 @@ pub mod btreemap_byte_values {
 
     // NOTE: This module can be exactly copied to use with HashMap.
 
-    use hashes::hex::FromHex;
+    use hex::FromHex;
     use serde;
 
     use crate::prelude::*;
@@ -248,7 +248,7 @@ pub mod hex_bytes {
     //! Module for serialization of byte arrays as hex strings.
     #![allow(missing_docs)]
 
-    use hashes::hex::FromHex;
+    use hex::FromHex;
     use serde;
 
     pub fn serialize<T, S>(bytes: &T, s: S) -> Result<S::Ok, S::Error>
diff --git a/bitcoin/src/taproot.rs b/bitcoin/src/taproot.rs
index ae1b3f45..9a90757f 100644
--- a/bitcoin/src/taproot.rs
+++ b/bitcoin/src/taproot.rs
@@ -1561,9 +1561,9 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
 mod test {
     use core::str::FromStr;
 
-    use hashes::hex::FromHex;
     use hashes::sha256t::Tag;
     use hashes::{sha256, Hash, HashEngine};
+    use hex::FromHex;
     use secp256k1::{VerifyOnly, XOnlyPublicKey};
 
     use super::*;
diff --git a/hashes/Cargo.toml b/hashes/Cargo.toml
index 4ce146d6..30313e14 100644
--- a/hashes/Cargo.toml
+++ b/hashes/Cargo.toml
@@ -14,9 +14,11 @@ exclude = ["tests", "contrib"]
 
 [features]
 default = ["std"]
-std = ["alloc", "internals/std"]
-alloc = ["internals/alloc"]
+std = ["alloc", "internals/std", "hex/std"]
+alloc = ["internals/alloc", "hex/alloc"]
 serde-std = ["serde/std"]
+# If you want I/O you must enable either "std" or "core2".
+core2 = ["actual-core2", "hex/core2"]
 
 [package.metadata.docs.rs]
 all-features = true
@@ -24,12 +26,15 @@ rustdoc-args = ["--cfg", "docsrs"]
 
 [dependencies]
 internals = { package = "bitcoin-internals", version = "0.2.0" }
+hex = { package = "hex-conservative", version = "0.1.1", default-features = false }
 
-core2 = { version = "0.3.0", default_features = false, optional = true }
 schemars = { version = "0.8.3", optional = true }
 # Only enable this if you explicitly do not want to use "std", otherwise enable "serde-std".
 serde = { version = "1.0", default-features = false, optional = true }
 
+# Do NOT use this feature! Use the "core2" feature instead.
+actual-core2 = { package = "core2", version = "0.3.2", default-features = false, optional = true }
+
 [dev-dependencies]
 serde_test = "1.0"
 serde_json = "1.0"
diff --git a/hashes/src/hex.rs b/hashes/src/hex.rs
deleted file mode 100644
index 31845a75..00000000
--- a/hashes/src/hex.rs
+++ /dev/null
@@ -1,225 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Hex encoding and decoding.
-//!
-
-use core::{fmt, str};
-#[cfg(feature = "std")]
-use std::io;
-
-#[cfg(all(feature = "core2", not(feature = "std")))]
-use core2::io;
-
-#[cfg(all(feature = "alloc", not(feature = "std")))]
-use crate::alloc::vec::Vec;
-
-/// Hex decoding error.
-#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum Error {
-    /// Non-hexadecimal character.
-    InvalidChar(u8),
-    /// Purported hex string had odd length.
-    OddLengthString(usize),
-    /// Tried to parse fixed-length hash from a string with the wrong type (expected, got).
-    InvalidLength(usize, usize),
-}
-
-impl fmt::Display for Error {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match *self {
-            Error::InvalidChar(ch) => write!(f, "invalid hex character {}", ch),
-            Error::OddLengthString(ell) => write!(f, "odd hex string length {}", ell),
-            Error::InvalidLength(ell, ell2) =>
-                write!(f, "bad hex string length {} (expected {})", ell2, ell),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for Error {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        use self::Error::*;
-
-        match self {
-            InvalidChar(_) | OddLengthString(_) | InvalidLength(_, _) => None,
-        }
-    }
-}
-
-/// Trait for objects that can be deserialized from hex strings.
-pub trait FromHex: Sized {
-    /// Produces an object from a byte iterator.
-    fn from_byte_iter<I>(iter: I) -> Result<Self, Error>
-    where
-        I: Iterator<Item = Result<u8, Error>> + ExactSizeIterator + DoubleEndedIterator;
-
-    /// Produces an object from a hex string.
-    fn from_hex(s: &str) -> Result<Self, Error> { Self::from_byte_iter(HexIterator::new(s)?) }
-}
-
-/// Iterator over a hex-encoded string slice which decodes hex and yields bytes.
-pub struct HexIterator<'a> {
-    /// The `Bytes` iterator whose next two bytes will be decoded to yield
-    /// the next byte.
-    iter: str::Bytes<'a>,
-}
-
-impl<'a> HexIterator<'a> {
-    /// Constructs a new `HexIterator` from a string slice.
-    ///
-    /// # Errors
-    ///
-    /// If the input string is of odd length.
-    pub fn new(s: &'a str) -> Result<HexIterator<'a>, Error> {
-        if s.len() % 2 != 0 {
-            Err(Error::OddLengthString(s.len()))
-        } else {
-            Ok(HexIterator { iter: s.bytes() })
-        }
-    }
-}
-
-fn chars_to_hex(hi: u8, lo: u8) -> Result<u8, Error> {
-    let hih = (hi as char).to_digit(16).ok_or(Error::InvalidChar(hi))?;
-    let loh = (lo as char).to_digit(16).ok_or(Error::InvalidChar(lo))?;
-
-    let ret = (hih << 4) + loh;
-    Ok(ret as u8)
-}
-
-impl<'a> Iterator for HexIterator<'a> {
-    type Item = Result<u8, Error>;
-
-    fn next(&mut self) -> Option<Result<u8, Error>> {
-        let hi = self.iter.next()?;
-        let lo = self.iter.next().unwrap();
-        Some(chars_to_hex(hi, lo))
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        let (min, max) = self.iter.size_hint();
-        (min / 2, max.map(|x| x / 2))
-    }
-}
-
-#[cfg(any(feature = "std", feature = "core2"))]
-impl<'a> io::Read for HexIterator<'a> {
-    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
-        let mut bytes_read = 0usize;
-        for dst in buf {
-            match self.next() {
-                Some(Ok(src)) => {
-                    *dst = src;
-                    bytes_read += 1;
-                }
-                _ => break,
-            }
-        }
-        Ok(bytes_read)
-    }
-}
-
-impl<'a> DoubleEndedIterator for HexIterator<'a> {
-    fn next_back(&mut self) -> Option<Result<u8, Error>> {
-        let lo = self.iter.next_back()?;
-        let hi = self.iter.next_back().unwrap();
-        Some(chars_to_hex(hi, lo))
-    }
-}
-
-impl<'a> ExactSizeIterator for HexIterator<'a> {}
-
-#[cfg(any(test, feature = "std", feature = "alloc"))]
-impl FromHex for Vec<u8> {
-    fn from_byte_iter<I>(iter: I) -> Result<Self, Error>
-    where
-        I: Iterator<Item = Result<u8, Error>> + ExactSizeIterator + DoubleEndedIterator,
-    {
-        iter.collect()
-    }
-}
-
-macro_rules! impl_fromhex_array {
-    ($len:expr) => {
-        impl FromHex for [u8; $len] {
-            fn from_byte_iter<I>(iter: I) -> Result<Self, Error>
-            where
-                I: Iterator<Item = Result<u8, Error>> + ExactSizeIterator + DoubleEndedIterator,
-            {
-                if iter.len() == $len {
-                    let mut ret = [0; $len];
-                    for (n, byte) in iter.enumerate() {
-                        ret[n] = byte?;
-                    }
-                    Ok(ret)
-                } else {
-                    Err(Error::InvalidLength(2 * $len, 2 * iter.len()))
-                }
-            }
-        }
-    };
-}
-
-impl_fromhex_array!(2);
-impl_fromhex_array!(4);
-impl_fromhex_array!(6);
-impl_fromhex_array!(8);
-impl_fromhex_array!(10);
-impl_fromhex_array!(12);
-impl_fromhex_array!(14);
-impl_fromhex_array!(16);
-impl_fromhex_array!(20);
-impl_fromhex_array!(24);
-impl_fromhex_array!(28);
-impl_fromhex_array!(32);
-impl_fromhex_array!(33);
-impl_fromhex_array!(64);
-impl_fromhex_array!(65);
-impl_fromhex_array!(128);
-impl_fromhex_array!(256);
-impl_fromhex_array!(384);
-impl_fromhex_array!(512);
-
-#[cfg(test)]
-#[cfg(feature = "alloc")]
-mod tests {
-    use internals::hex::exts::DisplayHex;
-
-    use super::*;
-
-    #[test]
-    fn hex_roundtrip() {
-        let expected = "0123456789abcdef";
-        let expected_up = "0123456789ABCDEF";
-
-        let parse: Vec<u8> = FromHex::from_hex(expected).expect("parse lowercase string");
-        assert_eq!(parse, vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]);
-        let ser = parse.to_lower_hex_string();
-        assert_eq!(ser, expected);
-
-        let parse: Vec<u8> = FromHex::from_hex(expected_up).expect("parse uppercase string");
-        assert_eq!(parse, vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]);
-        let ser = parse.to_lower_hex_string();
-        assert_eq!(ser, expected);
-
-        let parse: [u8; 8] = FromHex::from_hex(expected_up).expect("parse uppercase string");
-        assert_eq!(parse, [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]);
-        let ser = parse.to_lower_hex_string();
-        assert_eq!(ser, expected);
-    }
-
-    #[test]
-    fn hex_error() {
-        let oddlen = "0123456789abcdef0";
-        let badchar1 = "Z123456789abcdef";
-        let badchar2 = "012Y456789abcdeb";
-        let badchar3 = "«23456789abcdef";
-
-        assert_eq!(Vec::<u8>::from_hex(oddlen), Err(Error::OddLengthString(17)));
-        assert_eq!(<[u8; 4]>::from_hex(oddlen), Err(Error::OddLengthString(17)));
-        assert_eq!(<[u8; 8]>::from_hex(oddlen), Err(Error::OddLengthString(17)));
-        assert_eq!(Vec::<u8>::from_hex(badchar1), Err(Error::InvalidChar(b'Z')));
-        assert_eq!(Vec::<u8>::from_hex(badchar2), Err(Error::InvalidChar(b'Y')));
-        assert_eq!(Vec::<u8>::from_hex(badchar3), Err(Error::InvalidChar(194)));
-    }
-}
diff --git a/hashes/src/impls.rs b/hashes/src/impls.rs
index 5ff0179d..088a7f1f 100644
--- a/hashes/src/impls.rs
+++ b/hashes/src/impls.rs
@@ -5,13 +5,7 @@
 //! Implementations of traits defined in `std` / `core2` and not in `core`.
 //!
 
-#[cfg(feature = "std")]
-use std::io;
-
-#[cfg(not(feature = "std"))]
-use core2::io;
-
-use crate::{hmac, ripemd160, sha1, sha256, sha512, siphash24, HashEngine};
+use crate::{hmac, io, ripemd160, sha1, sha256, sha512, siphash24, HashEngine};
 
 impl io::Write for sha1::HashEngine {
     fn flush(&mut self) -> io::Result<()> { Ok(()) }
diff --git a/hashes/src/internal_macros.rs b/hashes/src/internal_macros.rs
index 13ef0be1..32a52dbf 100644
--- a/hashes/src/internal_macros.rs
+++ b/hashes/src/internal_macros.rs
@@ -9,11 +9,11 @@ impl<$($gen: $gent),*> $crate::_export::_core::fmt::LowerHex for $ty<$($gen),*>
             fn fmt(&self, f: &mut $crate::_export::_core::fmt::Formatter) -> $crate::_export::_core::fmt::Result {
                 #[allow(unused)]
                 use crate::Hash as _;
-                let case = internals::hex::Case::Lower;
+                let case = $crate::hex::Case::Lower;
                 if <$ty<$($gen),*>>::DISPLAY_BACKWARD {
-                    internals::hex::display::fmt_hex_exact!(f, $bytes, self.0.iter().rev(), case)
+                    $crate::hex::fmt_hex_exact!(f, $bytes, self.0.iter().rev(), case)
                 } else {
-                    internals::hex::display::fmt_hex_exact!(f, $bytes, self.0.iter(), case)
+                    $crate::hex::fmt_hex_exact!(f, $bytes, self.0.iter(), case)
                 }
             }
         }
@@ -23,11 +23,11 @@ impl<$($gen: $gent),*> $crate::_export::_core::fmt::UpperHex for $ty<$($gen),*>
             fn fmt(&self, f: &mut $crate::_export::_core::fmt::Formatter) -> $crate::_export::_core::fmt::Result {
                 #[allow(unused)]
                 use crate::Hash as _;
-                let case = internals::hex::Case::Upper;
+                let case = $crate::hex::Case::Upper;
                 if <$ty<$($gen),*>>::DISPLAY_BACKWARD {
-                    internals::hex::display::fmt_hex_exact!(f, $bytes, self.0.iter().rev(), case)
+                    $crate::hex::fmt_hex_exact!(f, $bytes, self.0.iter().rev(), case)
                 } else {
-                    internals::hex::display::fmt_hex_exact!(f, $bytes, self.0.iter(), case)
+                    $crate::hex::fmt_hex_exact!(f, $bytes, self.0.iter(), case)
                 }
             }
         }
@@ -76,7 +76,7 @@ impl<$($gen: $gent),*> Hash<$($gen),*> {
             /// This is mainly intended as an internal method and you shouldn't need it unless
             /// you're doing something special.
             pub fn forward_hex(&self) -> impl '_ + core::fmt::LowerHex + core::fmt::UpperHex {
-                internals::hex::display::DisplayHex::as_hex(&self.0)
+                $crate::hex::DisplayHex::as_hex(&self.0)
             }
 
             /// Displays hex backwards, regardless of how this type would display it naturally.
@@ -84,7 +84,7 @@ pub fn forward_hex(&self) -> impl '_ + core::fmt::LowerHex + core::fmt::UpperHex
             /// This is mainly intended as an internal method and you shouldn't need it unless
             /// you're doing something special.
             pub fn backward_hex(&self) -> impl '_ + core::fmt::LowerHex + core::fmt::UpperHex {
-                internals::hex::display::DisplayArray::<_, [u8; $bits / 8 * 2]>::new(self.0.iter().rev())
+                $crate::hex::display::DisplayArray::<_, [u8; $bits / 8 * 2]>::new(self.0.iter().rev())
             }
 
             /// Zero cost conversion between a fixed length byte array shared reference and
@@ -103,15 +103,15 @@ pub fn backward_hex(&self) -> impl '_ + core::fmt::LowerHex + core::fmt::UpperHe
         }
 
         impl<$($gen: $gent),*> str::FromStr for Hash<$($gen),*> {
-            type Err = $crate::hex::Error;
+            type Err = $crate::hex::HexToArrayError;
             fn from_str(s: &str) -> Result<Self, Self::Err> {
-                use $crate::hex::{FromHex, HexIterator};
+                use $crate::hex::{FromHex, HexToBytesIter};
                 use $crate::Hash;
 
                 let inner: [u8; $bits / 8] = if $reverse {
-                    FromHex::from_byte_iter(HexIterator::new(s)?.rev())?
+                    FromHex::from_byte_iter(HexToBytesIter::new(s)?.rev())?
                 } else {
-                    FromHex::from_byte_iter(HexIterator::new(s)?)?
+                    FromHex::from_byte_iter(HexToBytesIter::new(s)?)?
                 };
                 Ok(Self::from_byte_array(inner))
             }
diff --git a/hashes/src/lib.rs b/hashes/src/lib.rs
index b54feb2d..fa92397a 100644
--- a/hashes/src/lib.rs
+++ b/hashes/src/lib.rs
@@ -79,12 +79,12 @@
 // Instead of littering the codebase for non-fuzzing code just globally allow.
 #![cfg_attr(hashes_fuzz, allow(dead_code, unused_imports))]
 
+#[cfg(all(not(test), not(feature = "std"), feature = "core2"))]
+extern crate actual_core2 as core2;
 #[cfg(all(feature = "alloc", not(feature = "std")))]
 extern crate alloc;
 #[cfg(any(test, feature = "std"))]
 extern crate core;
-#[cfg(feature = "core2")]
-extern crate core2;
 
 #[cfg(feature = "serde")]
 /// A generic serialization/deserialization framework.
@@ -95,6 +95,9 @@
 #[cfg(bench)]
 extern crate test;
 
+/// Re-export the `hex-conservative` crate.
+pub extern crate hex;
+
 #[doc(hidden)]
 pub mod _export {
     /// A re-export of core::*
@@ -113,9 +116,8 @@ pub mod _core {
 pub mod serde_macros;
 pub mod cmp;
 pub mod hash160;
-pub mod hex;
 pub mod hmac;
-#[cfg(any(feature = "std", feature = "core2"))]
+#[cfg(any(test, feature = "std", feature = "core2"))]
 mod impls;
 pub mod ripemd160;
 pub mod sha1;
@@ -127,7 +129,12 @@ pub mod _core {
 pub mod siphash24;
 
 use core::{borrow, fmt, hash, ops};
+// You get I/O if you enable "std" or "core2" (as well as during testing).
+#[cfg(any(test, feature = "std"))]
+use std::io;
 
+#[cfg(all(not(test), not(feature = "std"), feature = "core2"))]
+use core2::io;
 pub use hmac::{Hmac, HmacEngine};
 
 /// A hashing engine which bytes can be serialized into.
diff --git a/hashes/src/sha256.rs b/hashes/src/sha256.rs
index e34e3646..5bafe883 100644
--- a/hashes/src/sha256.rs
+++ b/hashes/src/sha256.rs
@@ -125,7 +125,7 @@ fn index(&self, index: I) -> &Self::Output { &self.0[index] }
 }
 
 impl str::FromStr for Midstate {
-    type Err = hex::Error;
+    type Err = hex::HexToArrayError;
     fn from_str(s: &str) -> Result<Self, Self::Err> { hex::FromHex::from_hex(s) }
 }
 
@@ -174,9 +174,12 @@ pub const fn hash_tag(tag: &[u8]) -> Self {
 }
 
 impl hex::FromHex for Midstate {
-    fn from_byte_iter<I>(iter: I) -> Result<Self, hex::Error>
+    type Err = hex::HexToArrayError;
+    fn from_byte_iter<I>(iter: I) -> Result<Self, Self::Err>
     where
-        I: Iterator<Item = Result<u8, hex::Error>> + ExactSizeIterator + DoubleEndedIterator,
+        I: Iterator<Item = Result<u8, hex::HexToBytesError>>
+            + ExactSizeIterator
+            + DoubleEndedIterator,
     {
         // DISPLAY_BACKWARD is true
         Ok(Midstate::from_byte_array(hex::FromHex::from_byte_iter(iter.rev())?))
diff --git a/hashes/src/util.rs b/hashes/src/util.rs
index 39eb33b6..b8c23c74 100644
--- a/hashes/src/util.rs
+++ b/hashes/src/util.rs
@@ -268,15 +268,15 @@ fn all_zeros() -> Self {
         }
 
         impl $crate::_export::_core::str::FromStr for $newtype {
-            type Err = $crate::hex::Error;
+            type Err = $crate::hex::HexToArrayError;
             fn from_str(s: &str) -> $crate::_export::_core::result::Result<$newtype, Self::Err> {
-                use $crate::hex::{HexIterator, FromHex};
+                use $crate::hex::{FromHex, HexToBytesIter};
                 use $crate::Hash;
 
                 let inner: <$hash as Hash>::Bytes = if <Self as $crate::Hash>::DISPLAY_BACKWARD {
-                    FromHex::from_byte_iter(HexIterator::new(s)?.rev())?
+                    FromHex::from_byte_iter(HexToBytesIter::new(s)?.rev())?
                 } else {
-                    FromHex::from_byte_iter(HexIterator::new(s)?)?
+                    FromHex::from_byte_iter(HexToBytesIter::new(s)?)?
                 };
                 Ok($newtype(<$hash>::from_byte_array(inner)))
             }
diff --git a/internals/src/hex/buf_encoder.rs b/internals/src/hex/buf_encoder.rs
deleted file mode 100644
index 2d6793d9..00000000
--- a/internals/src/hex/buf_encoder.rs
+++ /dev/null
@@ -1,396 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Implements a buffered encoder.
-//!
-//! The main type of this module is [`BufEncoder`] which provides buffered hex encoding. Such is
-//! faster than the usual `write!(f, "{02x}", b)?` in a for loop because it reduces dynamic
-//! dispatch and decreases the number of allocations if a `String` is being created.
-
-use core::borrow::Borrow;
-
-pub use out_bytes::OutBytes;
-
-use super::Case;
-
-/// Trait for types that can be soundly converted to `OutBytes`.
-///
-/// To protect the API from future breakage this sealed trait guards which types can be used with
-/// the `Encoder`. Currently it is implemented for byte arrays of various interesting lengths.
-///
-/// ## Safety
-///
-/// This is not `unsafe` yet but the `as_out_bytes` should always return the same reference if the
-/// same reference is supplied. IOW the returned memory address and length should be the same if
-/// the input memory address and length are the same.
-///
-/// If the trait ever becomes `unsafe` this will be required for soundness.
-pub trait AsOutBytes: out_bytes::Sealed {
-    /// Performs the conversion.
-    fn as_out_bytes(&self) -> &OutBytes;
-
-    /// Performs the conversion.
-    fn as_mut_out_bytes(&mut self) -> &mut OutBytes;
-}
-
-/// A buffer with compile-time-known length.
-///
-/// This is essentially `Default + AsOutBytes` but supports lengths 1.41 doesn't.
-pub trait FixedLenBuf: Sized + AsOutBytes {
-    /// Creates an uninitialized buffer.
-    ///
-    /// The current implementtions initialize the buffer with zeroes but it should be treated a
-    /// uninitialized anyway.
-    fn uninit() -> Self;
-}
-
-/// Implements `OutBytes`
-///
-/// This prevents the rest of the crate from accessing the field of `OutBytes`.
-mod out_bytes {
-    use super::AsOutBytes;
-
-    /// A byte buffer that can only be written-into.
-    ///
-    /// You shouldn't concern yourself with this, just call `BufEncoder::new` with your array.
-    ///
-    /// This prepares the API for potential future support of `[MaybeUninit<u8>]`. We don't want to use
-    /// `unsafe` until it's proven to be needed but if it does we have an easy, compatible upgrade
-    /// option.
-    ///
-    /// Warning: `repr(transparent)` is an internal implementation detail and **must not** be
-    /// relied on!
-    #[repr(transparent)]
-    pub struct OutBytes([u8]);
-
-    impl OutBytes {
-        /// Returns the first `len` bytes as initialized.
-        ///
-        /// Not `unsafe` because we don't use `unsafe` (yet).
-        ///
-        /// ## Panics
-        ///
-        /// The method panics if `len` is out of bounds.
-        #[track_caller]
-        pub(crate) fn assume_init(&self, len: usize) -> &[u8] { &self.0[..len] }
-
-        /// Writes given bytes into the buffer.
-        ///
-        /// ## Panics
-        ///
-        /// The method panics if pos is out of bounds or `bytes` don't fit into the buffer.
-        #[track_caller]
-        pub(crate) fn write(&mut self, pos: usize, bytes: &[u8]) {
-            self.0[pos..(pos + bytes.len())].copy_from_slice(bytes);
-        }
-
-        /// Returns the length of the buffer.
-        pub(crate) fn len(&self) -> usize { self.0.len() }
-
-        fn from_bytes(slice: &[u8]) -> &Self {
-            // SAFETY: copied from std
-            // conversion of reference to pointer of the same referred type is always sound,
-            // including in unsized types.
-            // Thanks to repr(transparent) the types have the same layout making the other
-            // conversion sound.
-            // The pointer was just created from a reference that's still alive so dereferencing is
-            // sound.
-            unsafe { &*(slice as *const [u8] as *const Self) }
-        }
-
-        fn from_mut_bytes(slice: &mut [u8]) -> &mut Self {
-            // SAFETY: copied from std
-            // conversion of reference to pointer of the same referred type is always sound,
-            // including in unsized types.
-            // Thanks to repr(transparent) the types have the same layout making the other
-            // conversion sound.
-            // The pointer was just created from a reference that's still alive so dereferencing is
-            // sound.
-            unsafe { &mut *(slice as *mut [u8] as *mut Self) }
-        }
-    }
-
-    macro_rules! impl_from_array {
-        ($($len:expr),* $(,)?) => {
-            $(
-                impl super::FixedLenBuf for [u8; $len] {
-                    fn uninit() -> Self {
-                        [0u8; $len]
-                    }
-                }
-
-                impl AsOutBytes for [u8; $len] {
-                    fn as_out_bytes(&self) -> &OutBytes {
-                        OutBytes::from_bytes(self)
-                    }
-
-                    fn as_mut_out_bytes(&mut self) -> &mut OutBytes {
-                        OutBytes::from_mut_bytes(self)
-                    }
-                }
-
-                impl Sealed for [u8; $len] {}
-
-                impl<'a> super::super::display::DisplayHex for &'a [u8; $len / 2] {
-                    type Display = super::super::display::DisplayArray<core::slice::Iter<'a, u8>, [u8; $len]>;
-                    fn as_hex(self) -> Self::Display {
-                        super::super::display::DisplayArray::new(self.iter())
-                    }
-
-                    fn hex_reserve_suggestion(self) -> usize {
-                        $len
-                    }
-                }
-            )*
-        }
-    }
-
-    impl<T: AsOutBytes + ?Sized> AsOutBytes for &'_ mut T {
-        fn as_out_bytes(&self) -> &OutBytes { (**self).as_out_bytes() }
-
-        fn as_mut_out_bytes(&mut self) -> &mut OutBytes { (**self).as_mut_out_bytes() }
-    }
-
-    impl<T: AsOutBytes + ?Sized> Sealed for &'_ mut T {}
-
-    impl AsOutBytes for OutBytes {
-        fn as_out_bytes(&self) -> &OutBytes { self }
-
-        fn as_mut_out_bytes(&mut self) -> &mut OutBytes { self }
-    }
-
-    impl Sealed for OutBytes {}
-
-    // As a sanity check we only provide conversions for even, non-empty arrays.
-    // Weird lengths 66 and 130 are provided for serialized public keys.
-    impl_from_array!(
-        2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 40, 64, 66, 128, 130, 256, 512,
-        1024, 2048, 4096, 8192
-    );
-
-    /// Prevents outside crates from implementing the trait
-    pub trait Sealed {}
-}
-
-/// Hex-encodes bytes into the provided buffer.
-///
-/// This is an important building block for fast hex-encoding. Because string writing tools
-/// provided by `core::fmt` involve dynamic dispatch and don't allow reserving capacity in strings
-/// buffering the hex and then formatting it is significantly faster.
-pub struct BufEncoder<T: AsOutBytes> {
-    buf: T,
-    pos: usize,
-}
-
-impl<T: AsOutBytes> BufEncoder<T> {
-    /// Creates an empty `BufEncoder`.
-    ///
-    /// This is usually used with uninitialized (zeroed) byte array allocated on stack.
-    /// This can only be constructed with an even-length, non-empty array.
-    #[inline]
-    pub fn new(buf: T) -> Self { BufEncoder { buf, pos: 0 } }
-
-    /// Encodes `byte` as hex in given `case` and appends it to the buffer.
-    ///
-    /// ## Panics
-    ///
-    /// The method panics if the buffer is full.
-    #[inline]
-    #[track_caller]
-    pub fn put_byte(&mut self, byte: u8, case: Case) {
-        self.buf.as_mut_out_bytes().write(self.pos, &super::byte_to_hex(byte, case.table()));
-        self.pos += 2;
-    }
-
-    /// Encodes `bytes` as hex in given `case` and appends them to the buffer.
-    ///
-    /// ## Panics
-    ///
-    /// The method panics if the bytes wouldn't fit the buffer.
-    #[inline]
-    #[track_caller]
-    pub fn put_bytes<I>(&mut self, bytes: I, case: Case)
-    where
-        I: IntoIterator,
-        I::Item: Borrow<u8>,
-    {
-        self.put_bytes_inner(bytes.into_iter(), case)
-    }
-
-    #[inline]
-    #[track_caller]
-    fn put_bytes_inner<I>(&mut self, bytes: I, case: Case)
-    where
-        I: Iterator,
-        I::Item: Borrow<u8>,
-    {
-        // May give the compiler better optimization opportunity
-        if let Some(max) = bytes.size_hint().1 {
-            assert!(max <= self.space_remaining());
-        }
-        for byte in bytes {
-            self.put_byte(*byte.borrow(), case);
-        }
-    }
-
-    /// Encodes as many `bytes` as fit into the buffer as hex and return the remainder.
-    ///
-    /// This method works just like `put_bytes` but instead of panicking it returns the unwritten
-    /// bytes. The method returns an empty slice if all bytes were written
-    #[must_use = "this may write only part of the input buffer"]
-    #[inline]
-    #[track_caller]
-    pub fn put_bytes_min<'a>(&mut self, bytes: &'a [u8], case: Case) -> &'a [u8] {
-        let to_write = self.space_remaining().min(bytes.len());
-        self.put_bytes(&bytes[..to_write], case);
-        &bytes[to_write..]
-    }
-
-    /// Returns true if no more bytes can be written into the buffer.
-    #[inline]
-    pub fn is_full(&self) -> bool { self.pos == self.buf.as_out_bytes().len() }
-
-    /// Returns the written bytes as a hex `str`.
-    #[inline]
-    pub fn as_str(&self) -> &str {
-        core::str::from_utf8(self.buf.as_out_bytes().assume_init(self.pos))
-            .expect("we only write ASCII")
-    }
-
-    /// Resets the buffer to become empty.
-    #[inline]
-    pub fn clear(&mut self) { self.pos = 0; }
-
-    /// How many bytes can be written to this buffer.
-    ///
-    /// Note that this returns the number of bytes before encoding, not number of hex digits.
-    #[inline]
-    pub fn space_remaining(&self) -> usize { (self.buf.as_out_bytes().len() - self.pos) / 2 }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn empty() {
-        let mut buf = [0u8; 2];
-        let encoder = BufEncoder::new(&mut buf);
-        assert_eq!(encoder.as_str(), "");
-        assert!(!encoder.is_full());
-    }
-
-    #[test]
-    fn single_byte_exact_buf() {
-        let mut buf = [0u8; 2];
-        let mut encoder = BufEncoder::new(&mut buf);
-        assert_eq!(encoder.space_remaining(), 1);
-        encoder.put_byte(42, Case::Lower);
-        assert_eq!(encoder.as_str(), "2a");
-        assert_eq!(encoder.space_remaining(), 0);
-        assert!(encoder.is_full());
-        encoder.clear();
-        assert_eq!(encoder.space_remaining(), 1);
-        assert!(!encoder.is_full());
-        encoder.put_byte(42, Case::Upper);
-        assert_eq!(encoder.as_str(), "2A");
-        assert_eq!(encoder.space_remaining(), 0);
-        assert!(encoder.is_full());
-    }
-
-    #[test]
-    fn single_byte_oversized_buf() {
-        let mut buf = [0u8; 4];
-        let mut encoder = BufEncoder::new(&mut buf);
-        assert_eq!(encoder.space_remaining(), 2);
-        encoder.put_byte(42, Case::Lower);
-        assert_eq!(encoder.space_remaining(), 1);
-        assert_eq!(encoder.as_str(), "2a");
-        assert!(!encoder.is_full());
-        encoder.clear();
-        assert_eq!(encoder.space_remaining(), 2);
-        encoder.put_byte(42, Case::Upper);
-        assert_eq!(encoder.as_str(), "2A");
-        assert_eq!(encoder.space_remaining(), 1);
-        assert!(!encoder.is_full());
-    }
-
-    #[test]
-    fn two_bytes() {
-        let mut buf = [0u8; 4];
-        let mut encoder = BufEncoder::new(&mut buf);
-        encoder.put_byte(42, Case::Lower);
-        assert_eq!(encoder.space_remaining(), 1);
-        encoder.put_byte(255, Case::Lower);
-        assert_eq!(encoder.space_remaining(), 0);
-        assert_eq!(encoder.as_str(), "2aff");
-        assert!(encoder.is_full());
-        encoder.clear();
-        assert!(!encoder.is_full());
-        encoder.put_byte(42, Case::Upper);
-        encoder.put_byte(255, Case::Upper);
-        assert_eq!(encoder.as_str(), "2AFF");
-        assert!(encoder.is_full());
-    }
-
-    #[test]
-    fn put_bytes_min() {
-        let mut buf = [0u8; 2];
-        let mut encoder = BufEncoder::new(&mut buf);
-        let remainder = encoder.put_bytes_min(b"", Case::Lower);
-        assert_eq!(remainder, b"");
-        assert_eq!(encoder.as_str(), "");
-        let remainder = encoder.put_bytes_min(b"*", Case::Lower);
-        assert_eq!(remainder, b"");
-        assert_eq!(encoder.as_str(), "2a");
-        encoder.clear();
-        let remainder = encoder.put_bytes_min(&[42, 255], Case::Lower);
-        assert_eq!(remainder, &[255]);
-        assert_eq!(encoder.as_str(), "2a");
-    }
-
-    #[test]
-    fn same_as_fmt() {
-        use core::fmt::{self, Write};
-
-        struct Writer {
-            buf: [u8; 2],
-            pos: usize,
-        }
-
-        impl Writer {
-            fn as_str(&self) -> &str { core::str::from_utf8(&self.buf[..self.pos]).unwrap() }
-        }
-
-        impl Write for Writer {
-            fn write_str(&mut self, s: &str) -> fmt::Result {
-                assert!(self.pos <= 2);
-                if s.len() > 2 - self.pos {
-                    Err(fmt::Error)
-                } else {
-                    self.buf[self.pos..(self.pos + s.len())].copy_from_slice(s.as_bytes());
-                    self.pos += s.len();
-                    Ok(())
-                }
-            }
-        }
-
-        let mut writer = Writer { buf: [0u8; 2], pos: 0 };
-        let mut buf = [0u8; 2];
-        let mut encoder = BufEncoder::new(&mut buf);
-
-        for i in 0..=255 {
-            write!(writer, "{:02x}", i).unwrap();
-            encoder.put_byte(i, Case::Lower);
-            assert_eq!(encoder.as_str(), writer.as_str());
-            writer.pos = 0;
-            encoder.clear();
-        }
-        for i in 0..=255 {
-            write!(writer, "{:02X}", i).unwrap();
-            encoder.put_byte(i, Case::Upper);
-            assert_eq!(encoder.as_str(), writer.as_str());
-            writer.pos = 0;
-            encoder.clear();
-        }
-    }
-}
diff --git a/internals/src/hex/display.rs b/internals/src/hex/display.rs
deleted file mode 100644
index 85da1684..00000000
--- a/internals/src/hex/display.rs
+++ /dev/null
@@ -1,303 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Helpers for displaying bytes as hex strings.
-//!
-//! This module provides a trait for displaying things as hex as well as an implementation for
-//! `&[u8]`.
-
-use core::borrow::Borrow;
-use core::fmt;
-
-use super::buf_encoder::{BufEncoder, OutBytes};
-use super::Case;
-use crate::hex::buf_encoder::FixedLenBuf;
-#[cfg(feature = "alloc")]
-use crate::prelude::*;
-
-/// Extension trait for types that can be displayed as hex.
-///
-/// Types that have a single, obvious text representation being hex should **not** implement this
-/// trait and simply implement `Display` instead.
-///
-/// This trait should be generally implemented for references only. We would prefer to use GAT but
-/// that is beyond our MSRV. As a lint we require the `IsRef` trait which is implemented for all
-/// references.
-pub trait DisplayHex: Copy + sealed::IsRef {
-    /// The type providing [`fmt::Display`] implementation.
-    ///
-    /// This is usually a wrapper type holding a reference to `Self`.
-    type Display: fmt::LowerHex + fmt::UpperHex;
-
-    /// Display `Self` as a continuous sequence of ASCII hex chars.
-    fn as_hex(self) -> Self::Display;
-
-    /// Create a lower-hex-encoded string.
-    ///
-    /// A shorthand for `to_hex_string(Case::Lower)`, so that `Case` doesn't need to be imported.
-    ///
-    /// This may be faster than `.display_hex().to_string()` because it uses `reserve_suggestion`.
-    #[cfg(feature = "alloc")]
-    fn to_lower_hex_string(self) -> String { self.to_hex_string(Case::Lower) }
-
-    /// Create an upper-hex-encoded string.
-    ///
-    /// A shorthand for `to_hex_string(Case::Upper)`, so that `Case` doesn't need to be imported.
-    ///
-    /// This may be faster than `.display_hex().to_string()` because it uses `reserve_suggestion`.
-    #[cfg(feature = "alloc")]
-    fn to_upper_hex_string(self) -> String { self.to_hex_string(Case::Upper) }
-
-    /// Create a hex-encoded string.
-    ///
-    /// This may be faster than `.display_hex().to_string()` because it uses `reserve_suggestion`.
-    #[cfg(feature = "alloc")]
-    fn to_hex_string(self, case: Case) -> String {
-        let mut string = String::new();
-        self.append_hex_to_string(case, &mut string);
-        string
-    }
-
-    /// Appends hex-encoded content to an existing `String`.
-    ///
-    /// This may be faster than `write!(string, "{:x}", self.display_hex())` because it uses
-    /// `reserve_sugggestion`.
-    #[cfg(feature = "alloc")]
-    fn append_hex_to_string(self, case: Case, string: &mut String) {
-        use fmt::Write;
-
-        string.reserve(self.hex_reserve_suggestion());
-        match case {
-            Case::Lower => write!(string, "{:x}", self.as_hex()),
-            Case::Upper => write!(string, "{:X}", self.as_hex()),
-        }
-        .unwrap_or_else(|_| {
-            let name = core::any::type_name::<Self::Display>();
-            // We don't expect `std` to ever be buggy, so the bug is most likely in the `Display`
-            // impl of `Self::Display`.
-            panic!("The implementation of Display for {} returned an error when it shouldn't", name)
-        })
-    }
-
-    /// Hints how much bytes to reserve when creating a `String`.
-    ///
-    /// Implementors that know the number of produced bytes upfront should override this.
-    /// Defaults to 0.
-    ///
-    // We prefix the name with `hex_` to avoid potential collision with other methods.
-    fn hex_reserve_suggestion(self) -> usize { 0 }
-}
-
-mod sealed {
-    /// Trait marking a shared reference.
-    pub trait IsRef: Copy {}
-
-    impl<T: ?Sized> IsRef for &'_ T {}
-}
-
-impl<'a> DisplayHex for &'a [u8] {
-    type Display = DisplayByteSlice<'a>;
-
-    #[inline]
-    fn as_hex(self) -> Self::Display { DisplayByteSlice { bytes: self } }
-
-    #[inline]
-    fn hex_reserve_suggestion(self) -> usize {
-        // Since the string wouldn't fit into address space if this overflows (actually even for
-        // smaller amounts) it's better to panic right away. It should also give the optimizer
-        // better opportunities.
-        self.len().checked_mul(2).expect("the string wouldn't fit into address space")
-    }
-}
-
-#[cfg(feature = "alloc")]
-impl<'a> DisplayHex for &'a alloc::vec::Vec<u8> {
-    type Display = DisplayByteSlice<'a>;
-
-    #[inline]
-    fn as_hex(self) -> Self::Display { DisplayByteSlice { bytes: self } }
-
-    #[inline]
-    fn hex_reserve_suggestion(self) -> usize {
-        // Since the string wouldn't fit into address space if this overflows (actually even for
-        // smaller amounts) it's better to panic right away. It should also give the optimizer
-        // better opportunities.
-        self.len().checked_mul(2).expect("the string wouldn't fit into address space")
-    }
-}
-
-/// Displays byte slice as hex.
-///
-/// Created by [`<&[u8] as DisplayHex>::as_hex`](DisplayHex::as_hex).
-pub struct DisplayByteSlice<'a> {
-    // pub because we want to keep lengths in sync
-    pub(crate) bytes: &'a [u8],
-}
-
-impl<'a> DisplayByteSlice<'a> {
-    fn display(&self, f: &mut fmt::Formatter, case: Case) -> fmt::Result {
-        let mut buf = [0u8; 1024];
-        let mut encoder = super::BufEncoder::new(&mut buf);
-
-        let mut chunks = self.bytes.chunks_exact(512);
-        for chunk in &mut chunks {
-            encoder.put_bytes(chunk, case);
-            f.write_str(encoder.as_str())?;
-            encoder.clear();
-        }
-        encoder.put_bytes(chunks.remainder(), case);
-        f.write_str(encoder.as_str())
-    }
-}
-
-impl<'a> fmt::LowerHex for DisplayByteSlice<'a> {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.display(f, Case::Lower) }
-}
-
-impl<'a> fmt::UpperHex for DisplayByteSlice<'a> {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.display(f, Case::Upper) }
-}
-
-/// Displays byte array as hex.
-///
-/// Created by [`<&[u8; LEN] as DisplayHex>::as_hex`](DisplayHex::as_hex).
-pub struct DisplayArray<A: Clone + IntoIterator, B: FixedLenBuf>
-where
-    A::Item: Borrow<u8>,
-{
-    array: A,
-    _buffer_marker: core::marker::PhantomData<B>,
-}
-
-impl<A: Clone + IntoIterator, B: FixedLenBuf> DisplayArray<A, B>
-where
-    A::Item: Borrow<u8>,
-{
-    /// Creates the wrapper.
-    pub fn new(array: A) -> Self { DisplayArray { array, _buffer_marker: Default::default() } }
-
-    fn display(&self, f: &mut fmt::Formatter, case: Case) -> fmt::Result {
-        let mut buf = B::uninit();
-        let mut encoder = super::BufEncoder::new(&mut buf);
-        encoder.put_bytes(self.array.clone(), case);
-        f.pad_integral(true, "0x", encoder.as_str())
-    }
-}
-
-impl<A: Clone + IntoIterator, B: FixedLenBuf> fmt::LowerHex for DisplayArray<A, B>
-where
-    A::Item: Borrow<u8>,
-{
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.display(f, Case::Lower) }
-}
-
-impl<A: Clone + IntoIterator, B: FixedLenBuf> fmt::UpperHex for DisplayArray<A, B>
-where
-    A::Item: Borrow<u8>,
-{
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.display(f, Case::Upper) }
-}
-
-/// Format known-length array as hex.
-///
-/// This supports all formatting options of formatter and may be faster than calling
-/// `display_as_hex()` on an arbitrary `&[u8]`. Note that the implementation intentionally keeps
-/// leading zeros even when not requested. This is designed to display values such as hashes and
-/// keys and removing leading zeros would be confusing.
-///
-/// ## Parameters
-///
-/// * `$formatter` - a [`fmt::Formatter`].
-/// * `$len` known length of `$bytes`, must be a const expression.
-/// * `$bytes` - bytes to be encoded, most likely a reference to an array.
-/// * `$case` - value of type [`Case`] determining whether to format as lower or upper case.
-///
-/// ## Panics
-///
-/// This macro panics if `$len` is not equal to `$bytes.len()`. It also fails to compile if `$len`
-/// is more than half of `usize::MAX`.
-#[macro_export]
-macro_rules! fmt_hex_exact {
-    ($formatter:expr, $len:expr, $bytes:expr, $case:expr) => {{
-        // statically check $len
-        #[allow(deprecated)]
-        const _: () = [()][($len > usize::MAX / 2) as usize];
-        assert_eq!($bytes.len(), $len);
-        let mut buf = [0u8; $len * 2];
-        let buf = $crate::hex::buf_encoder::AsOutBytes::as_mut_out_bytes(&mut buf);
-        $crate::hex::display::fmt_hex_exact_fn($formatter, buf, $bytes, $case)
-    }};
-}
-pub use fmt_hex_exact;
-
-// Implementation detail of `write_hex_exact` macro to de-duplicate the code
-#[doc(hidden)]
-#[inline]
-pub fn fmt_hex_exact_fn<I>(
-    f: &mut fmt::Formatter,
-    buf: &mut OutBytes,
-    bytes: I,
-    case: Case,
-) -> fmt::Result
-where
-    I: IntoIterator,
-    I::Item: Borrow<u8>,
-{
-    let mut encoder = BufEncoder::new(buf);
-    encoder.put_bytes(bytes, case);
-    f.pad_integral(true, "0x", encoder.as_str())
-}
-
-#[cfg(test)]
-mod tests {
-    #[cfg(feature = "alloc")]
-    use super::*;
-
-    #[cfg(feature = "alloc")]
-    mod alloc {
-        use super::*;
-
-        fn check_encoding(bytes: &[u8]) {
-            use core::fmt::Write;
-
-            let s1 = bytes.to_lower_hex_string();
-            let mut s2 = String::with_capacity(bytes.len() * 2);
-            for b in bytes {
-                write!(s2, "{:02x}", b).unwrap();
-            }
-            assert_eq!(s1, s2);
-        }
-
-        #[test]
-        fn empty() { check_encoding(b""); }
-
-        #[test]
-        fn single() { check_encoding(b"*"); }
-
-        #[test]
-        fn two() { check_encoding(b"*x"); }
-
-        #[test]
-        fn just_below_boundary() { check_encoding(&[42; 512]); }
-
-        #[test]
-        fn just_above_boundary() { check_encoding(&[42; 513]); }
-
-        #[test]
-        fn just_above_double_boundary() { check_encoding(&[42; 1025]); }
-
-        #[test]
-        fn fmt_exact_macro() {
-            use crate::alloc::string::ToString;
-
-            struct Dummy([u8; 32]);
-
-            impl fmt::Display for Dummy {
-                fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                    fmt_hex_exact!(f, 32, &self.0, Case::Lower)
-                }
-            }
-
-            assert_eq!(Dummy([42; 32]).to_string(), "2a".repeat(32));
-        }
-    }
-}
diff --git a/internals/src/hex/mod.rs b/internals/src/hex/mod.rs
deleted file mode 100644
index 54fd0465..00000000
--- a/internals/src/hex/mod.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Helpers for encoding bytes as hex strings.
-
-pub mod buf_encoder;
-pub mod display;
-
-pub use buf_encoder::BufEncoder;
-
-/// Reexports of extension traits.
-pub mod exts {
-    pub use super::display::DisplayHex;
-}
-
-/// Possible case of hex.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Case {
-    /// Produce lower-case chars (`[0-9a-f]`).
-    ///
-    /// This is the default.
-    Lower,
-
-    /// Produce upper-case chars (`[0-9A-F]`).
-    Upper,
-}
-
-impl Default for Case {
-    fn default() -> Self { Case::Lower }
-}
-
-impl Case {
-    /// Returns the encoding table.
-    ///
-    /// The returned table may only contain displayable ASCII chars.
-    #[inline]
-    #[rustfmt::skip]
-    pub(crate) fn table(self) -> &'static [u8; 16] {
-        static LOWER: [u8; 16] = [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f'];
-        static UPPER: [u8; 16] = [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'A', b'B', b'C', b'D', b'E', b'F'];
-
-        match self {
-            Case::Lower => &LOWER,
-            Case::Upper => &UPPER,
-        }
-    }
-}
-
-/// Encodes single byte as two ASCII chars using the given table.
-///
-/// The function guarantees only returning values from the provided table.
-#[inline]
-pub(crate) fn byte_to_hex(byte: u8, table: &[u8; 16]) -> [u8; 2] {
-    [table[usize::from(byte.wrapping_shr(4))], table[usize::from(byte & 0x0F)]]
-}
diff --git a/internals/src/lib.rs b/internals/src/lib.rs
index f85e0b8f..a024e273 100644
--- a/internals/src/lib.rs
+++ b/internals/src/lib.rs
@@ -19,13 +19,6 @@
 extern crate std;
 
 pub mod error;
-pub mod hex;
 pub mod macros;
 mod parse;
 pub mod serde;
-
-/// Mainly reexports based on features.
-pub(crate) mod prelude {
-    #[cfg(feature = "alloc")]
-    pub(crate) use alloc::string::String;
-}


Test patch:
diff --git a/bitcoin/tests/psbt.rs b/bitcoin/tests/psbt.rs
index 692cc9b6..5271c59f 100644
--- a/bitcoin/tests/psbt.rs
+++ b/bitcoin/tests/psbt.rs
@@ -9,7 +9,7 @@
 use bitcoin::blockdata::opcodes::OP_0;
 use bitcoin::blockdata::script;
 use bitcoin::consensus::encode::{deserialize, serialize_hex};
-use bitcoin::hashes::hex::FromHex;
+use bitcoin::hex::FromHex;
 use bitcoin::psbt::{Psbt, PsbtSighashType};
 use bitcoin::script::PushBytes;
 use bitcoin::secp256k1::{self, Secp256k1};
diff --git a/bitcoin/tests/serde.rs b/bitcoin/tests/serde.rs
index fc125975..e4a85060 100644
--- a/bitcoin/tests/serde.rs
+++ b/bitcoin/tests/serde.rs
@@ -31,8 +31,8 @@
 use bitcoin::blockdata::locktime::{absolute, relative};
 use bitcoin::blockdata::witness::Witness;
 use bitcoin::consensus::encode::deserialize;
-use bitcoin::hashes::hex::FromHex;
 use bitcoin::hashes::{hash160, ripemd160, sha256, sha256d, Hash};
+use bitcoin::hex::FromHex;
 use bitcoin::psbt::raw::{self, Key, Pair, ProprietaryKey};
 use bitcoin::psbt::{Input, Output, Psbt, PsbtSighashType};
 use bitcoin::sighash::{EcdsaSighashType, TapSighashType};