Repository: rust-bitcoin/rust-bitcoin (rust)

Issue:
units: Remove alloc feature gating from `fee` module
If #3813 merges remove the `alloc` feature guarding because it is unnecessary.

Left as a separate issue so that #3813 is an internal change only.

Pull request:
units: Remove alloc from fee module
This PR removes the `alloc` feature gating from fee module

Closes #3815

Patch:
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index f90f321d..5e47891e 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -98,7 +98,6 @@ name = "bitcoin-io"
 version = "0.2.0"
 dependencies = [
  "bitcoin-internals",
- "bitcoin_hashes 0.16.0",
 ]
 
 [[package]]
@@ -108,6 +107,7 @@ dependencies = [
  "arbitrary",
  "bincode",
  "bitcoin-internals",
+ "bitcoin-io",
  "bitcoin-units",
  "bitcoin_hashes 0.16.0",
  "hex-conservative 0.3.0",
@@ -141,6 +141,7 @@ dependencies = [
 name = "bitcoin_hashes"
 version = "0.16.0"
 dependencies = [
+ "bitcoin-io",
  "hex-conservative 0.3.0",
  "serde",
  "serde_json",
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index 8b590d8a..47641c99 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -97,7 +97,6 @@ name = "bitcoin-io"
 version = "0.2.0"
 dependencies = [
  "bitcoin-internals",
- "bitcoin_hashes 0.16.0",
 ]
 
 [[package]]
@@ -107,6 +106,7 @@ dependencies = [
  "arbitrary",
  "bincode",
  "bitcoin-internals",
+ "bitcoin-io",
  "bitcoin-units",
  "bitcoin_hashes 0.16.0",
  "hex-conservative 0.3.0",
@@ -140,6 +140,7 @@ dependencies = [
 name = "bitcoin_hashes"
 version = "0.16.0"
 dependencies = [
+ "bitcoin-io",
  "hex-conservative 0.3.0",
  "serde",
  "serde_json",
diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml
index 9713bf20..cf057502 100644
--- a/bitcoin/Cargo.toml
+++ b/bitcoin/Cargo.toml
@@ -27,10 +27,10 @@ arbitrary = ["dep:arbitrary", "units/arbitrary", "primitives/arbitrary"]
 [dependencies]
 base58 = { package = "base58ck", version = "0.2.0", default-features = false, features = ["alloc"] }
 bech32 = { version = "0.11.0", default-features = false, features = ["alloc"] }
-hashes = { package = "bitcoin_hashes", version = "0.16.0", default-features = false, features = ["alloc", "hex"] }
+hashes = { package = "bitcoin_hashes", version = "0.16.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", "hashes"] }
+io = { package = "bitcoin-io", version = "0.2.0", default-features = false, features = ["alloc"] }
 primitives = { package = "bitcoin-primitives", version = "0.101.0", default-features = false, features = ["alloc"] }
 secp256k1 = { version = "0.29.0", default-features = false, features = ["hashes", "alloc"] }
 units = { package = "bitcoin-units", version = "0.2.0", default-features = false, features = ["alloc"] }
diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index 0a99677e..d9ec07c8 100644
--- a/bitcoin/src/blockdata/transaction.rs
+++ b/bitcoin/src/blockdata/transaction.rs
@@ -631,7 +631,7 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
     /// Extension functionality for the [`Version`] type.
     pub trait VersionExt impl for Version {
         /// Constructs a new non-standard transaction version.
-        fn non_standard(version: u32) -> Version { Self(version) }
+        fn non_standard(version: i32) -> Version { Self(version) }
 
         /// Returns true if this transaction version number is considered standard.
         fn is_standard(&self) -> bool { *self == Version::ONE || *self == Version::TWO || *self == Version::THREE }
@@ -1369,11 +1369,17 @@ fn consensus_serde() {
 
     #[test]
     fn transaction_version() {
-        let tx_bytes = hex!("ffffffff0100000000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000");
+        let tx_bytes = hex!("ffffff7f0100000000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000");
         let tx: Result<Transaction, _> = deserialize(&tx_bytes);
         assert!(tx.is_ok());
         let realtx = tx.unwrap();
-        assert_eq!(realtx.version, Version::non_standard(u32::MAX));
+        assert_eq!(realtx.version, Version::non_standard(2147483647));
+
+        let tx2_bytes = hex!("000000800100000000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000");
+        let tx2: Result<Transaction, _> = deserialize(&tx2_bytes);
+        assert!(tx2.is_ok());
+        let realtx2 = tx2.unwrap();
+        assert_eq!(realtx2.version, Version::non_standard(-2147483648));
     }
 
     #[test]
diff --git a/hashes/Cargo.toml b/hashes/Cargo.toml
index 35760c87..c34f2f04 100644
--- a/hashes/Cargo.toml
+++ b/hashes/Cargo.toml
@@ -15,8 +15,8 @@ exclude = ["tests", "contrib"]
 
 [features]
 default = ["std"]
-std = ["alloc", "hex/std"]
-alloc = ["hex/alloc"]
+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 = []
@@ -24,6 +24,7 @@ small-hash = []
 [dependencies]
 
 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 }
 
 [dev-dependencies]
diff --git a/hashes/contrib/sanitizer.sh b/hashes/contrib/sanitizer.sh
index 495e404c..e3d1e815 100755
--- a/hashes/contrib/sanitizer.sh
+++ b/hashes/contrib/sanitizer.sh
@@ -5,7 +5,7 @@
 set -euox pipefail
 
 # Run the sanitizer with these features.
-FEATURES="std serde"
+FEATURES="std bitcoin-io serde"
 
 cargo clean
 CC='clang -fsanitize=address -fno-omit-frame-pointer'                                        \
diff --git a/hashes/embedded/Cargo.toml b/hashes/embedded/Cargo.toml
index dcf5a7ec..be73fe56 100644
--- a/hashes/embedded/Cargo.toml
+++ b/hashes/embedded/Cargo.toml
@@ -19,8 +19,8 @@ cortex-m-rt = "0.6.10"
 cortex-m-semihosting = "0.3.3"
 panic-halt = "0.2.0"
 alloc-cortex-m = { version = "0.4.1", optional = true }
-bitcoin_hashes = { path="../", default-features = false, features = [] }
-bitcoin-io = { path = "../../io", default_features = false, features = ["hashes"] }
+bitcoin_hashes = { path="../", default-features = false, features = ["bitcoin-io"] }
+bitcoin-io = { path = "../../io", default_features = false }
 
 [[bin]]
 name = "embedded"
@@ -32,9 +32,6 @@ codegen-units = 1 # better optimizations
 debug = true # symbols are nice and they don't increase the size on Flash
 lto = true # better optimizations
 
-[patch.crates-io.bitcoin_hashes]
-path = "../../hashes"
-
 [patch.crates-io.bitcoin-internals]
 path = "../../internals"
 
diff --git a/hashes/src/hmac/mod.rs b/hashes/src/hmac/mod.rs
index eace7f95..5cddfdc5 100644
--- a/hashes/src/hmac/mod.rs
+++ b/hashes/src/hmac/mod.rs
@@ -155,8 +155,7 @@ fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Hmac<T>, D::Error> {
     }
 }
 
-#[cfg(feature = "std")]
-crate::internal_macros::impl_write!(
+crate::internal_macros::impl_io_write!(
     HmacEngine<T>,
     |us: &mut HmacEngine<T>, buf| {
         us.input(buf);
diff --git a/hashes/src/internal_macros.rs b/hashes/src/internal_macros.rs
index 67b9f617..5ae79f11 100644
--- a/hashes/src/internal_macros.rs
+++ b/hashes/src/internal_macros.rs
@@ -91,6 +91,12 @@ pub fn hash_byte_chunks<B, I>(byte_slices: I) -> Self
             {
                 <Self as $crate::GeneralHash>::hash_byte_chunks(byte_slices)
             }
+
+            /// Hashes the entire contents of the `reader`.
+            #[cfg(feature = "bitcoin-io")]
+            pub fn hash_reader<R: io::BufRead>(reader: &mut R) -> Result<Self, io::Error> {
+                <Self as $crate::GeneralHash>::hash_reader(reader)
+            }
         }
     };
 }
@@ -150,7 +156,7 @@ pub fn from_slice(
 
         $crate::internal_macros::hash_trait_impls!($bits, $reverse);
 
-        $crate::internal_macros::impl_write!(
+        $crate::internal_macros::impl_io_write!(
             HashEngine,
             |us: &mut HashEngine, buf| {
                 crate::HashEngine::input(us, buf);
@@ -162,16 +168,29 @@ pub fn from_slice(
 }
 pub(crate) use hash_type_no_default;
 
-macro_rules! impl_write {
+// We do not use the `bitcoin_io::impl_write` macro because we don't have an unconditional
+// dependency on `bitcoin-io` and we want to implement `std:io::Write` even when we don't depend on
+// `bitcoin-io`.
+macro_rules! impl_io_write {
     ($ty: ty, $write_fn: expr, $flush_fn: expr $(, $bounded_ty: ident : $bounds: path),*) => {
-        // `bitcoin_io::Write` is implemented in `bitcoin_io`.
+        #[cfg(feature = "bitcoin-io")]
+        impl<$($bounded_ty: $bounds),*> bitcoin_io::Write for $ty {
+            #[inline]
+            fn write(&mut self, buf: &[u8]) -> bitcoin_io::Result<usize> {
+                $write_fn(self, buf)
+            }
+            #[inline]
+            fn flush(&mut self) -> bitcoin_io::Result<()> {
+                $flush_fn(self)
+            }
+        }
+
         #[cfg(feature = "std")]
         impl<$($bounded_ty: $bounds),*> std::io::Write for $ty {
             #[inline]
             fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
                 $write_fn(self, buf)
             }
-
             #[inline]
             fn flush(&mut self) -> std::io::Result<()> {
                 $flush_fn(self)
@@ -179,7 +198,7 @@ fn flush(&mut self) -> std::io::Result<()> {
         }
     }
 }
-pub(crate) use impl_write;
+pub(crate) use impl_io_write;
 
 macro_rules! engine_input_impl(
     () => (
diff --git a/hashes/src/lib.rs b/hashes/src/lib.rs
index 0324da01..9f0067aa 100644
--- a/hashes/src/lib.rs
+++ b/hashes/src/lib.rs
@@ -75,6 +75,9 @@
 #[cfg(feature = "std")]
 extern crate std;
 
+#[cfg(feature = "bitcoin-io")]
+extern crate bitcoin_io as io;
+
 /// A generic serialization/deserialization framework.
 #[cfg(feature = "serde")]
 pub extern crate serde;
@@ -236,6 +239,28 @@ fn hash_byte_chunks<B, I>(byte_slices: I) -> Self
         }
         Self::from_engine(engine)
     }
+
+    /// Hashes the entire contents of the `reader`.
+    #[cfg(feature = "bitcoin-io")]
+    fn hash_reader<R: io::BufRead>(reader: &mut R) -> Result<Self, io::Error>
+    where
+        Self::Engine: Default,
+    {
+        let mut engine = Self::engine();
+        loop {
+            let bytes = reader.fill_buf()?;
+
+            let read = bytes.len();
+            // Empty slice means EOF.
+            if read == 0 {
+                break;
+            }
+
+            engine.input(bytes);
+            reader.consume(read);
+        }
+        Ok(Self::from_engine(engine))
+    }
 }
 
 /// Trait which applies to hashes of all types.
@@ -342,4 +367,13 @@ fn newtype_fmt_roundtrip() {
         let rinsed = hex.parse::<TestNewtype>().expect("failed to parse hex");
         assert_eq!(rinsed, orig)
     }
+
+    #[test]
+    #[cfg(feature = "bitcoin-io")]
+    fn hash_reader() {
+        use crate::sha256;
+
+        let mut reader: &[u8] = b"hello";
+        assert_eq!(sha256::Hash::hash_reader(&mut reader).unwrap(), sha256::Hash::hash(b"hello"),)
+    }
 }
diff --git a/hashes/src/sha256t/mod.rs b/hashes/src/sha256t/mod.rs
index e2461eeb..5d28b9d9 100644
--- a/hashes/src/sha256t/mod.rs
+++ b/hashes/src/sha256t/mod.rs
@@ -87,6 +87,25 @@ pub fn hash_byte_chunks<B, I>(byte_slices: I) -> Self
         Self::from_engine(engine)
     }
 
+    /// Hashes the entire contents of the `reader`.
+    #[cfg(feature = "bitcoin-io")]
+    pub fn hash_reader<R: io::BufRead>(reader: &mut R) -> Result<Self, io::Error> {
+        let mut engine = Self::engine();
+        loop {
+            let bytes = reader.fill_buf()?;
+
+            let read = bytes.len();
+            // Empty slice means EOF.
+            if read == 0 {
+                break;
+            }
+
+            engine.input(bytes);
+            reader.consume(read);
+        }
+        Ok(Self::from_engine(engine))
+    }
+
     /// Returns the underlying byte array.
     pub const fn to_byte_array(self) -> [u8; 32] { self.0 }
 
@@ -133,7 +152,7 @@ fn input(&mut self, data: &[u8]) { self.0.input(data) }
     fn n_bytes_hashed(&self) -> u64 { self.0.n_bytes_hashed() }
 }
 
-crate::internal_macros::impl_write!(
+crate::internal_macros::impl_io_write!(
     HashEngine<T>,
     |us: &mut HashEngine<T>, buf| {
         us.input(buf);
diff --git a/io/Cargo.toml b/io/Cargo.toml
index d245121d..224d3780 100644
--- a/io/Cargo.toml
+++ b/io/Cargo.toml
@@ -15,14 +15,12 @@ exclude = ["tests", "contrib"]
 
 [features]
 default = ["std"]
-std = ["alloc", "hashes?/std"]
-alloc = ["hashes?/alloc"]
+std = ["alloc"]
+alloc = []
 
 [dependencies]
 internals = { package = "bitcoin-internals", version = "0.4.0" }
 
-hashes = { package = "bitcoin_hashes", version = "0.16.0", default-features = false, optional = true }
-
 [package.metadata.docs.rs]
 all-features = true
 rustdoc-args = ["--cfg", "docsrs"]
diff --git a/io/src/hash.rs b/io/src/hash.rs
deleted file mode 100644
index a6a364e9..00000000
--- a/io/src/hash.rs
+++ /dev/null
@@ -1,378 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! I/O hashing support.
-//!
-//! Support for various hashing related things e.g.
-//!
-//! - Hashing to a writer.
-//! - Implement I/O traits for hash engines.
-
-use hashes::hmac::HmacEngine;
-use hashes::{
-    hash160, ripemd160, sha1, sha256, sha256d, sha256t, sha384, sha512, sha512_256, siphash24,
-    GeneralHash, HashEngine as _,
-};
-
-macro_rules! impl_write {
-    ($ty: ty, $write_fn: expr, $flush_fn: expr $(, $bounded_ty: ident : $bounds: path),*) => {
-        // `std::io::Write` is implemented in `bitcoin_hashes` because of the orphan rule.
-        impl<$($bounded_ty: $bounds),*> crate::Write for $ty {
-            #[inline]
-            fn write(&mut self, buf: &[u8]) -> crate::Result<usize> {
-                $write_fn(self, buf)}
-
-            #[inline]
-            fn flush(&mut self) -> crate::Result<()> {
-                $flush_fn(self)
-            }
-        }
-    }
-}
-pub(crate) use impl_write;
-
-impl_write!(
-    hash160::HashEngine,
-    |us: &mut hash160::HashEngine, buf| {
-        hashes::HashEngine::input(us, buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) }
-);
-
-impl_write!(
-    ripemd160::HashEngine,
-    |us: &mut ripemd160::HashEngine, buf| {
-        hashes::HashEngine::input(us, buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) }
-);
-
-impl_write!(
-    sha1::HashEngine,
-    |us: &mut sha1::HashEngine, buf| {
-        hashes::HashEngine::input(us, buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) }
-);
-
-impl_write!(
-    sha256::HashEngine,
-    |us: &mut sha256::HashEngine, buf| {
-        hashes::HashEngine::input(us, buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) }
-);
-
-impl_write!(
-    sha256d::HashEngine,
-    |us: &mut sha256d::HashEngine, buf| {
-        hashes::HashEngine::input(us, buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) }
-);
-
-impl_write!(
-    sha256t::HashEngine<T>,
-    |us: &mut sha256t::HashEngine<T>, buf| {
-        hashes::HashEngine::input(us, buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) },
-    T: sha256t::Tag
-);
-
-impl_write!(
-    sha384::HashEngine,
-    |us: &mut sha384::HashEngine, buf| {
-        hashes::HashEngine::input(us, buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) }
-);
-
-impl_write!(
-    sha512::HashEngine,
-    |us: &mut sha512::HashEngine, buf| {
-        hashes::HashEngine::input(us, buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) }
-);
-
-impl_write!(
-    sha512_256::HashEngine,
-    |us: &mut sha512_256::HashEngine, buf| {
-        hashes::HashEngine::input(us, buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) }
-);
-
-impl_write!(
-    siphash24::HashEngine,
-    |us: &mut siphash24::HashEngine, buf| {
-        hashes::HashEngine::input(us, buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) }
-);
-
-impl_write!(
-    HmacEngine<T>,
-    |us: &mut HmacEngine<T>, buf| {
-        us.input(buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) },
-    T: hashes::GeneralHash
-);
-
-/// Hashes data from a reader.
-///
-/// Adds functionality to a [`hashes::GeneralHash`] type to support hashing data read from a
-/// buffered reader.
-pub trait GeneralHashExt: GeneralHash + sealed::Sealed {
-    /// Hashes the entire contents of the `reader`.
-    fn hash_reader<R: crate::BufRead>(reader: &mut R) -> Result<Self, crate::Error>
-    where
-        Self::Engine: Default,
-    {
-        let mut engine = Self::engine(); // This calls `Self::Engine::default()`.
-        loop {
-            let bytes = reader.fill_buf()?;
-
-            let read = bytes.len();
-            // Empty slice means EOF.
-            if read == 0 {
-                break;
-            }
-
-            engine.input(bytes);
-            reader.consume(read);
-        }
-        Ok(Self::from_engine(engine))
-    }
-}
-
-impl<T: GeneralHash> GeneralHashExt for T {}
-
-mod sealed {
-    /// Used to seal the `GeneralHashExt` trait.
-    pub trait Sealed {}
-
-    impl<T: hashes::GeneralHash> Sealed for T {}
-}
-
-#[cfg(test)]
-#[cfg(feature = "alloc")]
-mod tests {
-    use alloc::format;
-
-    use hashes::{hmac, Hmac};
-
-    use super::*;
-    use crate::{Cursor, Write as _};
-
-    macro_rules! write_test {
-        ($mod:ident, $exp_empty:expr, $exp_256:expr, $exp_64k:expr,) => {
-            #[test]
-            fn $mod() {
-                let mut engine = $mod::Hash::engine();
-                engine.write_all(&[]).unwrap();
-                assert_eq!(format!("{}", $mod::Hash::from_engine(engine)), $exp_empty);
-
-                let mut engine = $mod::Hash::engine();
-                engine.write_all(&[1; 256]).unwrap();
-                assert_eq!(format!("{}", $mod::Hash::from_engine(engine)), $exp_256);
-
-                let mut engine = $mod::Hash::engine();
-                engine.write_all(&[99; 64000]).unwrap();
-                assert_eq!(format!("{}", $mod::Hash::from_engine(engine)), $exp_64k);
-            }
-        };
-    }
-
-    write_test!(
-        sha1,
-        "da39a3ee5e6b4b0d3255bfef95601890afd80709",
-        "ac458b067c6b021c7e9358229b636e9d1e4cb154",
-        "e4b66838f9f7b6f91e5be32a02ae78094df402e7",
-    );
-
-    write_test!(
-        sha256,
-        "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
-        "2661920f2409dd6c8adeb0c44972959f232b6429afa913845d0fd95e7e768234",
-        "5c5e904f5d4fd587c7a906bf846e08a927286f388c54c39213a4884695271bbc",
-    );
-
-    write_test!(
-        sha256d,
-        "56944c5d3f98413ef45cf54545538103cc9f298e0575820ad3591376e2e0f65d",
-        "374000d830c75d10d9417e493a7652920f30efbd300e3fb092f24c28c20baf64",
-        "0050d4148ad7a0437ca0643fad5bf4614cd95d9ba21fde52370b37dcc3f03307",
-    );
-
-    write_test!(
-        sha384,
-        "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b",
-        "82135637ef6d6dd31a20e2bc9998681a3eecaf8f8c76d45e545214de38439d9a533848ec75f53e4b1a8805709c5124d0",
-        "fb7511d9a98c5686f9c2f55e242397815c9229d8759451e1710b8da6861e08d52f0357176f4b74f8cad9e23ab65411c7",
-    );
-
-    write_test!(
-        sha512,
-        "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce\
-         47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
-        "57ecf739d3a7ca647639adae80a05f4f361304bfcbfa1ceba93296b096e74287\
-         45fc10c142cecdd3bb587a3dba598c072f6f78b31cc0a06a3da0105ee51f75d6",
-        "dd28f78c53f3bc9bd0c2dca9642a1ad402a70412f985c1f6e54fadb98ce9c458\
-         4761df8d04ed04bb734ba48dd2106bb9ea54524f1394cdd18e6da3166e71c3ee",
-    );
-
-    write_test!(
-        sha512_256,
-        "c672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967a",
-        "8d4bb96e7956cf5f08bf5c45f7982630c46b0b022f25cbaf722ae97c06a6e7a2",
-        "3367646f3e264653f7dd664ac2cb6d3b96329e86ffb7a29a1082e2a4ddc9ee7a",
-    );
-
-    write_test!(
-        ripemd160,
-        "9c1185a5c5e9fc54612808977ee8f548b2258d31",
-        "e571a1ca5b780aa52bafdb9ec852544ffca418ba",
-        "ddd2ecce739e823629c7d46ab18918e9c4a51c75",
-    );
-
-    write_test!(
-        hash160,
-        "b472a266d0bd89c13706a4132ccfb16f7c3b9fcb",
-        "671356a1a874695ad3bc20cae440f4360835bd5a",
-        "a9608c952c8dbcc20c53803d2ca5ad31d64d9313",
-    );
-
-    #[test]
-    fn hmac() {
-        let mut engine = hmac::HmacEngine::<sha256::Hash>::new(&[0xde, 0xad, 0xbe, 0xef]);
-        engine.write_all(&[]).unwrap();
-        assert_eq!(
-            format!("{}", hmac::Hmac::from_engine(engine)),
-            "bf5515149cf797955c4d3194cca42472883281951697c8375d9d9b107f384225"
-        );
-
-        let mut engine = hmac::HmacEngine::<sha256::Hash>::new(&[0xde, 0xad, 0xbe, 0xef]);
-        engine.write_all(&[1; 256]).unwrap();
-        assert_eq!(
-            format!("{}", hmac::Hmac::from_engine(engine)),
-            "59c9aca10c81c73cb4c196d94db741b6bf2050e0153d5a45f2526bff34675ac5"
-        );
-
-        let mut engine = hmac::HmacEngine::<sha256::Hash>::new(&[0xde, 0xad, 0xbe, 0xef]);
-        engine.write_all(&[99; 64000]).unwrap();
-        assert_eq!(
-            format!("{}", hmac::Hmac::from_engine(engine)),
-            "30df499717415a395379a1eaabe50038036e4abb5afc94aa55c952f4aa57be08"
-        );
-    }
-
-    #[test]
-    fn siphash24() {
-        let mut engine = siphash24::HashEngine::with_keys(0, 0);
-        engine.write_all(&[]).unwrap();
-        assert_eq!(format!("{}", siphash24::Hash::from_engine(engine)), "d70077739d4b921e");
-
-        let mut engine = siphash24::HashEngine::with_keys(0, 0);
-        engine.write_all(&[1; 256]).unwrap();
-        assert_eq!(format!("{}", siphash24::Hash::from_engine(engine)), "3a3ccefde9b5b1e3");
-
-        let mut engine = siphash24::HashEngine::with_keys(0, 0);
-        engine.write_all(&[99; 64000]).unwrap();
-        assert_eq!(format!("{}", siphash24::Hash::from_engine(engine)), "ce456e4e4ecbc5bf");
-    }
-
-    // Data and expected hashes taken from `bitcoin_hashes/tests/regression.rs`.
-    const DATA: &str = "arbitrary data to hash as a regression test";
-    const HMAC_KEY: &[u8] = b"some key";
-
-    macro_rules! impl_hash_reader_test {
-        ($($test_name:ident, $module:ident, $want:literal);* $(;)?) => {
-            $(
-                #[test]
-                fn $test_name() {
-                    let hash = $module::Hash::hash(DATA.as_bytes());
-                    let got = format!("{}", hash);
-                    assert_eq!(got, $want);
-
-                    let mut reader = Cursor::new(DATA);
-                    let hash_from_reader = $module::Hash::hash_reader(&mut reader).unwrap();
-                    assert_eq!(hash_from_reader, hash)
-                }
-            )*
-        }
-    }
-
-    impl_hash_reader_test! {
-        hash_from_reader_hash160, hash160, "a17909f6d5373b0085c4180ba207126e5040f74d";
-        hash_from_reader_ripemd160, ripemd160, "e6801701c77a1cd85662335258c7869631b4a9a8";
-        hash_from_reader_sha1, sha1, "e1e81eeabadafa3d5d41cc3f405385426b0f47fd";
-        hash_from_reader_sha256, sha256, "d291c6c5a07fa1d9315cdae090ebe14169fbe0a219cd55a48d0d2104eab6ec51";
-        hash_from_reader_sha256d, sha256d, "93a743b022290bde3233a619b21aaebe06c5cf5cc959464c41be35711e37731b";
-        hash_from_reader_sha384, sha384, "f545bd83d297978d47a7f26b858a54188499dfb4d7d570a6a2362c765031d57a29d7e002df5e34d184e70b65a4f47153";
-        hash_from_reader_sha512, sha512, "057d0a37e9e0ac9a93acde0752748da059a27bcf946c7af00692ac1a95db8d21f965f40af22efc4710f100f8d3e43f79f77b1f48e1e400a95b7344b7bc0dfd10";
-        hash_from_reader_sha512_256, sha512_256, "e204244c429b5bca037a2a8a6e7ed8a42b808ceaff182560840bb8c5c8e9a2ec";
-    }
-
-    #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
-    pub struct RegHashTag; // Name comes from regression tests in `bitcoin_hashes`.
-
-    impl sha256t::Tag for RegHashTag {
-        const MIDSTATE: sha256::Midstate = sha256::Midstate::new([0xab; 32], 64);
-    }
-
-    type RegHash = sha256t::Hash<RegHashTag>;
-
-    #[test]
-    fn regression_sha256t() {
-        let hash = RegHash::hash(DATA.as_bytes());
-        let got = format!("{}", hash);
-        let want = "17db326d7c13867376ccca1f8a211377be3cbeaeb372f167822284866ddf14ca";
-        assert_eq!(got, want);
-    }
-
-    #[test]
-    fn regression_hmac_sha256_with_key() {
-        let mut engine = HmacEngine::<sha256::Hash>::new(HMAC_KEY);
-        engine.input(DATA.as_bytes());
-        let hash = Hmac::from_engine(engine);
-
-        let got = format!("{}", hash);
-        let want = "d159cecaf4adf90b6a641bab767e4817d3a51c414acea3682686c35ec0b37b52";
-        assert_eq!(got, want);
-    }
-
-    #[test]
-    fn regression_hmac_sha512_with_key() {
-        let mut engine = HmacEngine::<sha512::Hash>::new(HMAC_KEY);
-        engine.input(DATA.as_bytes());
-        let hash = Hmac::from_engine(engine);
-
-        let got = format!("{}", hash);
-        let want = "8511773748f89ba22c07fb3a2981a12c1823695119de41f4a62aead6b848bd34939acf16475c35ed7956114fead3e794cc162ecd35e447a4dabc3227d55f757b";
-        assert_eq!(got, want);
-    }
-
-    #[test]
-    fn regression_siphash24_with_key() {
-        let mut engine = siphash24::HashEngine::with_keys(0, 0);
-        engine.input(DATA.as_bytes());
-        let hash = siphash24::Hash::from_engine(engine);
-
-        let got = format!("{}", hash);
-        let want = "e823ed82311d601a";
-        assert_eq!(got, want);
-    }
-}
diff --git a/io/src/lib.rs b/io/src/lib.rs
index 3c020d0b..0e1d3312 100644
--- a/io/src/lib.rs
+++ b/io/src/lib.rs
@@ -25,17 +25,11 @@
 #[cfg(feature = "alloc")]
 extern crate alloc;
 
-#[cfg(feature = "hashes")]
-pub extern crate hashes;
-
 #[cfg(feature = "std")]
 mod bridge;
 mod error;
 mod macros;
 
-#[cfg(feature = "hashes")]
-mod hash;
-
 #[cfg(all(not(feature = "std"), feature = "alloc"))]
 use alloc::vec::Vec;
 use core::cmp;
@@ -45,8 +39,6 @@
 
 #[rustfmt::skip]                // Keep public re-exports separate.
 pub use self::error::{Error, ErrorKind};
-#[cfg(feature = "hashes")]
-pub use self::hash::GeneralHashExt;
 
 /// Result type returned by functions in this crate.
 pub type Result<T> = core::result::Result<T, Error>;
diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml
index 241be623..a44c42c6 100644
--- a/primitives/Cargo.toml
+++ b/primitives/Cargo.toml
@@ -16,15 +16,16 @@ exclude = ["tests", "contrib"]
 
 [features]
 default = ["std"]
-std = ["alloc", "hashes/std", "hex/std", "internals/std", "units/std"]
-alloc = ["hashes/alloc", "hex/alloc", "internals/alloc", "units/alloc"]
+std = ["alloc", "hashes/std", "hex/std", "internals/std", "io/std", "units/std"]
+alloc = ["hashes/alloc", "hex/alloc", "internals/alloc", "io/alloc", "units/alloc"]
 serde = ["dep:serde", "hashes/serde", "internals/serde", "units/serde", "alloc"]
 arbitrary = ["dep:arbitrary", "units/arbitrary"]
 
 [dependencies]
-hashes = { package = "bitcoin_hashes", version = "0.16.0", default-features = false, features = ["hex"] }
+hashes = { package = "bitcoin_hashes", version = "0.16.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 }
 units = { package = "bitcoin-units", version = "0.2.0", default-features = false }
 
 arbitrary = { version = "1.4", optional = true }
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index df1bef8a..e91540e5 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -510,7 +510,7 @@ impl Wtxid {
 ///
 /// Currently, as specified by [BIP-68] and [BIP-431], version 1, 2, and 3 are considered standard.
 ///
-/// Standardness of the inner `u32` is not an invariant because you are free to create transactions
+/// Standardness of the inner `i32` is not an invariant because you are free to create transactions
 /// of any version, transactions with non-standard version numbers will not be relayed by the
 /// Bitcoin network.
 ///
@@ -518,7 +518,7 @@ impl Wtxid {
 /// [BIP-431]: https://github.com/bitcoin/bips/blob/master/bip-0431.mediawiki
 #[derive(Copy, PartialEq, Eq, Clone, Debug, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Version(pub u32);
+pub struct Version(pub i32);
 
 impl Version {
     /// The original Bitcoin transaction version (pre-BIP-68).
diff --git a/units/src/fee.rs b/units/src/fee.rs
index 94601db8..472eb937 100644
--- a/units/src/fee.rs
+++ b/units/src/fee.rs
@@ -33,7 +33,6 @@ impl Amount {
     /// let fee_rate = amount.checked_div_by_weight_ceil(weight).expect("Division by weight failed");
     /// assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(34));
     /// ```
-    #[cfg(feature = "alloc")]
     #[must_use]
     pub const fn checked_div_by_weight_ceil(self, weight: Weight) -> Option<FeeRate> {
         let wu = weight.to_wu();
@@ -56,7 +55,6 @@ pub const fn checked_div_by_weight_ceil(self, weight: Weight) -> Option<FeeRate>
     /// can be made. See also [`Self::checked_div_by_weight_ceil`].
     ///
     /// Returns [`None`] if overflow occurred.
-    #[cfg(feature = "alloc")]
     #[must_use]
     pub const fn checked_div_by_weight_floor(self, weight: Weight) -> Option<FeeRate> {
         // No `?` operator in const context.


Test patch:
diff --git a/hashes/contrib/test_vars.sh b/hashes/contrib/test_vars.sh
index efcd3dcf..35360740 100644
--- a/hashes/contrib/test_vars.sh
+++ b/hashes/contrib/test_vars.sh
@@ -5,10 +5,10 @@
 # shellcheck disable=SC2034
 
 # Test all these features with "std" enabled.
-FEATURES_WITH_STD="serde small-hash"
+FEATURES_WITH_STD="bitcoin-io serde small-hash"
 
 # Test all these features without "std" enabled.
-FEATURES_WITHOUT_STD="alloc serde small-hash"
+FEATURES_WITHOUT_STD="alloc bitcoin-io serde small-hash"
 
 # Run these examples.
 EXAMPLES=""
diff --git a/hashes/tests/io.rs b/hashes/tests/io.rs
new file mode 100644
index 00000000..9151bba3
--- /dev/null
+++ b/hashes/tests/io.rs
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: CC0-1.0
+
+//! 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,
+    GeneralHash as _,
+};
+use bitcoin_io::Write;
+
+macro_rules! write_test {
+    ($mod:ident, $exp_empty:expr, $exp_256:expr, $exp_64k:expr,) => {
+        #[test]
+        fn $mod() {
+            let mut engine = $mod::Hash::engine();
+            engine.write_all(&[]).unwrap();
+            assert_eq!(format!("{}", $mod::Hash::from_engine(engine)), $exp_empty);
+
+            let mut engine = $mod::Hash::engine();
+            engine.write_all(&[1; 256]).unwrap();
+            assert_eq!(format!("{}", $mod::Hash::from_engine(engine)), $exp_256);
+
+            let mut engine = $mod::Hash::engine();
+            engine.write_all(&[99; 64000]).unwrap();
+            assert_eq!(format!("{}", $mod::Hash::from_engine(engine)), $exp_64k);
+        }
+    };
+}
+
+write_test!(
+    sha1,
+    "da39a3ee5e6b4b0d3255bfef95601890afd80709",
+    "ac458b067c6b021c7e9358229b636e9d1e4cb154",
+    "e4b66838f9f7b6f91e5be32a02ae78094df402e7",
+);
+
+write_test!(
+    sha256,
+    "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+    "2661920f2409dd6c8adeb0c44972959f232b6429afa913845d0fd95e7e768234",
+    "5c5e904f5d4fd587c7a906bf846e08a927286f388c54c39213a4884695271bbc",
+);
+
+write_test!(
+    sha256d,
+    "56944c5d3f98413ef45cf54545538103cc9f298e0575820ad3591376e2e0f65d",
+    "374000d830c75d10d9417e493a7652920f30efbd300e3fb092f24c28c20baf64",
+    "0050d4148ad7a0437ca0643fad5bf4614cd95d9ba21fde52370b37dcc3f03307",
+);
+
+write_test!(
+    sha384,
+    "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b",
+    "82135637ef6d6dd31a20e2bc9998681a3eecaf8f8c76d45e545214de38439d9a533848ec75f53e4b1a8805709c5124d0",
+    "fb7511d9a98c5686f9c2f55e242397815c9229d8759451e1710b8da6861e08d52f0357176f4b74f8cad9e23ab65411c7",
+);
+
+write_test!(
+    sha512,
+    "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce\
+     47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
+    "57ecf739d3a7ca647639adae80a05f4f361304bfcbfa1ceba93296b096e74287\
+     45fc10c142cecdd3bb587a3dba598c072f6f78b31cc0a06a3da0105ee51f75d6",
+    "dd28f78c53f3bc9bd0c2dca9642a1ad402a70412f985c1f6e54fadb98ce9c458\
+     4761df8d04ed04bb734ba48dd2106bb9ea54524f1394cdd18e6da3166e71c3ee",
+);
+
+write_test!(
+    sha512_256,
+    "c672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967a",
+    "8d4bb96e7956cf5f08bf5c45f7982630c46b0b022f25cbaf722ae97c06a6e7a2",
+    "3367646f3e264653f7dd664ac2cb6d3b96329e86ffb7a29a1082e2a4ddc9ee7a",
+);
+
+write_test!(
+    ripemd160,
+    "9c1185a5c5e9fc54612808977ee8f548b2258d31",
+    "e571a1ca5b780aa52bafdb9ec852544ffca418ba",
+    "ddd2ecce739e823629c7d46ab18918e9c4a51c75",
+);
+
+write_test!(
+    hash160,
+    "b472a266d0bd89c13706a4132ccfb16f7c3b9fcb",
+    "671356a1a874695ad3bc20cae440f4360835bd5a",
+    "a9608c952c8dbcc20c53803d2ca5ad31d64d9313",
+);
+
+#[test]
+fn hmac() {
+    let mut engine = hmac::HmacEngine::<sha256::Hash>::new(&[0xde, 0xad, 0xbe, 0xef]);
+    engine.write_all(&[]).unwrap();
+    assert_eq!(
+        format!("{}", hmac::Hmac::from_engine(engine)),
+        "bf5515149cf797955c4d3194cca42472883281951697c8375d9d9b107f384225"
+    );
+
+    let mut engine = hmac::HmacEngine::<sha256::Hash>::new(&[0xde, 0xad, 0xbe, 0xef]);
+    engine.write_all(&[1; 256]).unwrap();
+    assert_eq!(
+        format!("{}", hmac::Hmac::from_engine(engine)),
+        "59c9aca10c81c73cb4c196d94db741b6bf2050e0153d5a45f2526bff34675ac5"
+    );
+
+    let mut engine = hmac::HmacEngine::<sha256::Hash>::new(&[0xde, 0xad, 0xbe, 0xef]);
+    engine.write_all(&[99; 64000]).unwrap();
+    assert_eq!(
+        format!("{}", hmac::Hmac::from_engine(engine)),
+        "30df499717415a395379a1eaabe50038036e4abb5afc94aa55c952f4aa57be08"
+    );
+}
+
+#[test]
+fn siphash24() {
+    let mut engine = siphash24::HashEngine::with_keys(0, 0);
+    engine.write_all(&[]).unwrap();
+    assert_eq!(format!("{}", siphash24::Hash::from_engine(engine)), "d70077739d4b921e");
+
+    let mut engine = siphash24::HashEngine::with_keys(0, 0);
+    engine.write_all(&[1; 256]).unwrap();
+    assert_eq!(format!("{}", siphash24::Hash::from_engine(engine)), "3a3ccefde9b5b1e3");
+
+    let mut engine = siphash24::HashEngine::with_keys(0, 0);
+    engine.write_all(&[99; 64000]).unwrap();
+    assert_eq!(format!("{}", siphash24::Hash::from_engine(engine)), "ce456e4e4ecbc5bf");
+}
diff --git a/hashes/tests/regression.rs b/hashes/tests/regression.rs
index 319d6ecf..a4ffddc2 100644
--- a/hashes/tests/regression.rs
+++ b/hashes/tests/regression.rs
@@ -1,8 +1,6 @@
 //! Regression tests for each hash type.
 //!
 //! Note that if `bitcoin-io` is enabled then we get more regression-like testing from `./io.rs`.
-//!
-//! Test input data and expected hashes is the same as in `io/src/hash.rs`.
 
 #![cfg(feature = "hex")]
 
diff --git a/io/contrib/test_vars.sh b/io/contrib/test_vars.sh
index 279c2507..92dc0940 100644
--- a/io/contrib/test_vars.sh
+++ b/io/contrib/test_vars.sh
@@ -5,10 +5,10 @@
 # shellcheck disable=SC2034
 
 # Test all these features with "std" enabled.
-FEATURES_WITH_STD="hashes"
+FEATURES_WITH_STD=""
 
 # Test all these features without "std" enabled.
-FEATURES_WITHOUT_STD="alloc hashes"
+FEATURES_WITHOUT_STD="alloc"
 
 # Run these examples.
 EXAMPLES=""