Repository: rust-bitcoin/rust-bitcoin
Pull request #4085 (2025-02-20 03:02:52)

Remove the `GeneralHash` trait
This is the done as part of #4051.

Requires some surgery on the `Hmac` and `Hkdf` types as well as a few other patches to maintain the logic that is currently provided by the trait. Final patch is a pure red diff - enjoy.

github-actions[bot]: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.
tcharding: In case its not obvious, the regression test in patch on is to prove that the change to `hash_again` in 82bb884e68204a604c94e68d11c9e4fbe0797f1b is correct.
github-actions[bot]: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.
github-actions[bot]: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.
apoelstra: caac660d52ec18d833c4d50df2fa83268b4ea520 looks great! Started local CI but it might be many hours before it finishes, since today there are many 5+-commit PRs for some reason.

To do in followups:

* Revisit whether we need the `Hash` bound on `HmacEngine::Hash` or whether something weaker (`AsRef<Self::Bytes>`, or, somehow, nothing)
* Revisit the `Bytes` type and see if we can't replace it with an associated constant (I remember we couldn't before, but things have changed now)
* Revisit which functions we should add to hmac and hkdf which are skipped in this PR since they're missing in the existing API
Kixunil: > * Revisit the `Bytes` type and see if we can't replace it with an associated constant (I remember we couldn't before, but things have changed now)

I think it should be possible to remove it entirely.
tcharding: Promoted to #4094
apoelstra: I got 18 hours into this and was 1/3 done. I stopped the build to defragment my nix store and my btrfs extents and will restart in a bit.

The timing here is pretty bad ... I am using btrfs over SATA on a spinning disk drive for my Nix store. I have a new PCIe drive with ext4 which should be 10-1000x faster, but I can't cut over to it when I'm remote ... and I left town yesterday for a couple weeks. I shouldn't have kept procrastinating on it..
tcharding: We can just queue up drafts on top of this for any `hashes` PRs till you are back home. I'm not stressing about `hashes` at the moment because `amounts` is taking so long, all my wrath is taken up already.
apoelstra: Compliing with features `hex serde` and nothing else seems to fail on one commit.
apoelstra: In ed6fa5a5d648784f8a3ce1c7c2d5b3a66fc4e268 (the very first commit):

Your new regression test doesn't compile (yet). If you cd into `hashes/` on this commit and run `cargo test --all-features` it fails.
tcharding: CI should not pass then, I'll investigate. Thanks
apoelstra: @tcharding it does compile by the last commit, so I'm not too surprised that CI passes since it tests only the tip.

It looks like you added the regression then reordered the commits but didn't tweak it, or something.
tcharding: Oh woops, cheers.
github-actions[bot]: :rotating_light: API BREAKING CHANGE DETECTED

To see the changes click details on "Check semver breaks / PR Semver - stable toolchain" job then expand "Run semver checker script" and scroll to the end of the section.

Patch:
diff --git a/bitcoin/src/bip32.rs b/bitcoin/src/bip32.rs
index a25ff424..5b83327e 100644
--- a/bitcoin/src/bip32.rs
+++ b/bitcoin/src/bip32.rs
@@ -10,7 +10,7 @@
 use core::str::FromStr;
 use core::{fmt, slice};
 
-use hashes::{hash160, hash_newtype, sha512, GeneralHash, HashEngine, Hmac, HmacEngine};
+use hashes::{hash160, hash_newtype, sha512, HashEngine, Hmac, HmacEngine};
 use internals::write_err;
 use secp256k1::{Secp256k1, XOnlyPublicKey};
 
@@ -584,9 +584,9 @@ fn from(e: InvalidBase58PayloadLengthError) -> Error { Self::InvalidBase58Payloa
 impl Xpriv {
     /// Constructs a new master key from a seed value
     pub fn new_master(network: impl Into<NetworkKind>, seed: &[u8]) -> Result<Xpriv, Error> {
-        let mut hmac_engine: HmacEngine<sha512::Hash> = HmacEngine::new(b"Bitcoin seed");
-        hmac_engine.input(seed);
-        let hmac_result: Hmac<sha512::Hash> = Hmac::from_engine(hmac_engine);
+        let mut engine = HmacEngine::<sha512::HashEngine>::new(b"Bitcoin seed");
+        engine.input(seed);
+        let hmac = engine.finalize();
 
         Ok(Xpriv {
             network: network.into(),
@@ -594,9 +594,9 @@ pub fn new_master(network: impl Into<NetworkKind>, seed: &[u8]) -> Result<Xpriv,
             parent_fingerprint: Default::default(),
             child_number: ChildNumber::ZERO_NORMAL,
             private_key: secp256k1::SecretKey::from_byte_array(
-                &hmac_result.as_ref()[..32].try_into().expect("Slice should be exactly 32 bytes"),
+                &hmac.as_ref()[..32].try_into().expect("Slice should be exactly 32 bytes"),
             )?,
-            chain_code: ChainCode::from_hmac(hmac_result),
+            chain_code: ChainCode::from_hmac(hmac),
         })
     }
 
@@ -650,25 +650,25 @@ pub fn derive_xpriv<C: secp256k1::Signing, P: AsRef<[ChildNumber]>>(
 
     /// Private->Private child key derivation
     fn ckd_priv<C: secp256k1::Signing>(&self, secp: &Secp256k1<C>, i: ChildNumber) -> Xpriv {
-        let mut hmac_engine: HmacEngine<sha512::Hash> = HmacEngine::new(&self.chain_code[..]);
+        let mut engine = HmacEngine::<sha512::HashEngine>::new(&self.chain_code[..]);
         match i {
             ChildNumber::Normal { .. } => {
                 // Non-hardened key: compute public data and use that
-                hmac_engine.input(
+                engine.input(
                     &secp256k1::PublicKey::from_secret_key(secp, &self.private_key).serialize()[..],
                 );
             }
             ChildNumber::Hardened { .. } => {
                 // Hardened key: use only secret data to prevent public derivation
-                hmac_engine.input(&[0u8]);
-                hmac_engine.input(&self.private_key[..]);
+                engine.input(&[0u8]);
+                engine.input(&self.private_key[..]);
             }
         }
 
-        hmac_engine.input(&u32::from(i).to_be_bytes());
-        let hmac_result: Hmac<sha512::Hash> = Hmac::from_engine(hmac_engine);
+        engine.input(&u32::from(i).to_be_bytes());
+        let hmac: Hmac<sha512::Hash> = engine.finalize();
         let sk = secp256k1::SecretKey::from_byte_array(
-            &hmac_result.as_ref()[..32].try_into().expect("statistically impossible to hit"),
+            &hmac.as_ref()[..32].try_into().expect("statistically impossible to hit"),
         )
         .expect("statistically impossible to hit");
         let tweaked =
@@ -680,7 +680,7 @@ fn ckd_priv<C: secp256k1::Signing>(&self, secp: &Secp256k1<C>, i: ChildNumber) -
             parent_fingerprint: self.fingerprint(secp),
             child_number: i,
             private_key: tweaked,
-            chain_code: ChainCode::from_hmac(hmac_result),
+            chain_code: ChainCode::from_hmac(hmac),
         }
     }
 
@@ -812,18 +812,15 @@ pub fn ckd_pub_tweak(
         match i {
             ChildNumber::Hardened { .. } => Err(Error::CannotDeriveFromHardenedKey),
             ChildNumber::Normal { index: n } => {
-                let mut hmac_engine: HmacEngine<sha512::Hash> =
-                    HmacEngine::new(&self.chain_code[..]);
-                hmac_engine.input(&self.public_key.serialize()[..]);
-                hmac_engine.input(&n.to_be_bytes());
+                let mut engine = HmacEngine::<sha512::HashEngine>::new(&self.chain_code[..]);
+                engine.input(&self.public_key.serialize()[..]);
+                engine.input(&n.to_be_bytes());
 
-                let hmac_result: Hmac<sha512::Hash> = Hmac::from_engine(hmac_engine);
+                let hmac = engine.finalize();
                 let private_key = secp256k1::SecretKey::from_byte_array(
-                    &hmac_result.as_ref()[..32]
-                        .try_into()
-                        .expect("Slice should be exactly 32 bytes"),
+                    &hmac.as_ref()[..32].try_into().expect("Slice should be exactly 32 bytes"),
                 )?;
-                let chain_code = ChainCode::from_hmac(hmac_result);
+                let chain_code = ChainCode::from_hmac(hmac);
                 Ok((private_key, chain_code))
             }
         }
diff --git a/bitcoin/src/consensus/encode.rs b/bitcoin/src/consensus/encode.rs
index e2946b16..a8637c2c 100644
--- a/bitcoin/src/consensus/encode.rs
+++ b/bitcoin/src/consensus/encode.rs
@@ -16,7 +16,7 @@
 
 use core::mem;
 
-use hashes::{sha256, sha256d, GeneralHash, Hash};
+use hashes::{sha256, sha256d, Hash};
 use hex::DisplayHex as _;
 use internals::{compact_size, ToU64};
 use io::{BufRead, Cursor, Read, Write};
@@ -629,7 +629,7 @@ fn consensus_decode_from_finite_reader<R: BufRead + ?Sized>(r: &mut R) -> Result
 
 /// Does a double-SHA256 on `data` and returns the first 4 bytes.
 fn sha2_checksum(data: &[u8]) -> [u8; 4] {
-    let checksum = <sha256d::Hash as GeneralHash>::hash(data);
+    let checksum = sha256d::hash(data);
     let checksum = checksum.to_byte_array();
     [checksum[0], checksum[1], checksum[2], checksum[3]]
 }
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index c1d67bb6..7a045cfb 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -459,22 +459,17 @@ pub fn to_bytes(self) -> Vec<u8> { self.to_vec() }
     /// Serializes the private key to bytes.
     pub fn to_vec(self) -> Vec<u8> { self.inner[..].to_vec() }
 
-    /// Deserializes a private key from a byte array.
-    pub fn from_byte_array(
-        data: [u8; 32],
-        network: impl Into<NetworkKind>
-    ) -> Result<PrivateKey, secp256k1::Error> {
-        Ok(PrivateKey::new(secp256k1::SecretKey::from_byte_array(&data)?, network))
-    }
-
     /// Deserializes a private key from a slice.
-    #[deprecated(since = "TBD", note = "use from_byte_array instead")]
     pub fn from_slice(
         data: &[u8],
         network: impl Into<NetworkKind>,
     ) -> Result<PrivateKey, secp256k1::Error> {
-        let array = data.try_into().map_err(|_| secp256k1::Error::InvalidSecretKey)?;
-        Self::from_byte_array(array, network)
+        Ok(PrivateKey::new(
+            secp256k1::SecretKey::from_byte_array(
+                data[..32].try_into().expect("Slice should be exactly 32 bytes"),
+            )?,
+            network,
+        ))
     }
 
     /// Formats the private key to WIF format.
@@ -1605,13 +1600,4 @@ fn public_key_from_str_invalid_str() {
             panic!("Expected Invalid char error");
         }
     }
-
-    #[test]
-    #[allow(deprecated)] // tests the deprecated function
-    #[allow(deprecated_in_future)]
-    fn invalid_private_key_len() {
-        use crate::Network;
-        assert!(PrivateKey::from_slice(&[1u8; 31], Network::Regtest).is_err());
-        assert!(PrivateKey::from_slice(&[1u8; 33], Network::Regtest).is_err());
-    }
 }
diff --git a/bitcoin/src/psbt/macros.rs b/bitcoin/src/psbt/macros.rs
index 089d705b..4044a5e5 100644
--- a/bitcoin/src/psbt/macros.rs
+++ b/bitcoin/src/psbt/macros.rs
@@ -113,15 +113,15 @@ macro_rules! impl_psbt_insert_pair {
 
 #[rustfmt::skip]
 macro_rules! psbt_insert_hash_pair {
-    (&mut $slf:ident.$map:ident <= $raw_key:ident|$raw_value:ident|$hash:path|$hash_type_error:path) => {
+    (&mut $slf:ident.$map:ident <= $raw_key:ident|$raw_value:ident|$hash:ident|$hash_type_error:path) => {
         if $raw_key.key_data.is_empty() {
             return Err($crate::psbt::Error::InvalidKey($raw_key));
         }
-        let key_val: $hash = Deserialize::deserialize(&$raw_key.key_data)?;
+        let key_val: $hash::Hash = Deserialize::deserialize(&$raw_key.key_data)?;
         match $slf.$map.entry(key_val) {
             btree_map::Entry::Vacant(empty_key) => {
                 let val: Vec<u8> = Deserialize::deserialize(&$raw_value)?;
-                if <$hash as hashes::GeneralHash>::hash(&val) != key_val {
+                if $hash::hash(&val) != key_val {
                     return Err($crate::psbt::Error::InvalidPreimageHashPair {
                         preimage: val.into_boxed_slice(),
                         hash: Box::from(key_val.borrow()),
diff --git a/bitcoin/src/psbt/map/input.rs b/bitcoin/src/psbt/map/input.rs
index 401bc34b..66802623 100644
--- a/bitcoin/src/psbt/map/input.rs
+++ b/bitcoin/src/psbt/map/input.rs
@@ -317,22 +317,22 @@ pub(super) fn insert_pair(&mut self, pair: raw::Pair) -> Result<(), Error> {
             }
             PSBT_IN_RIPEMD160 => {
                 psbt_insert_hash_pair! {
-                    &mut self.ripemd160_preimages <= raw_key|raw_value|ripemd160::Hash|error::PsbtHash::Ripemd
+                    &mut self.ripemd160_preimages <= raw_key|raw_value|ripemd160|error::PsbtHash::Ripemd
                 }
             }
             PSBT_IN_SHA256 => {
                 psbt_insert_hash_pair! {
-                    &mut self.sha256_preimages <= raw_key|raw_value|sha256::Hash|error::PsbtHash::Sha256
+                    &mut self.sha256_preimages <= raw_key|raw_value|sha256|error::PsbtHash::Sha256
                 }
             }
             PSBT_IN_HASH160 => {
                 psbt_insert_hash_pair! {
-                    &mut self.hash160_preimages <= raw_key|raw_value|hash160::Hash|error::PsbtHash::Hash160
+                    &mut self.hash160_preimages <= raw_key|raw_value|hash160|error::PsbtHash::Hash160
                 }
             }
             PSBT_IN_HASH256 => {
                 psbt_insert_hash_pair! {
-                    &mut self.hash256_preimages <= raw_key|raw_value|sha256d::Hash|error::PsbtHash::Hash256
+                    &mut self.hash256_preimages <= raw_key|raw_value|sha256d|error::PsbtHash::Hash256
                 }
             }
             PSBT_IN_TAP_KEY_SIG => {
diff --git a/bitcoin/src/psbt/serialize.rs b/bitcoin/src/psbt/serialize.rs
index 89de08dc..26bfc9b3 100644
--- a/bitcoin/src/psbt/serialize.rs
+++ b/bitcoin/src/psbt/serialize.rs
@@ -261,7 +261,7 @@ fn serialize(&self) -> Vec<u8> { XOnlyPublicKey::serialize(self).to_vec() }
 impl Deserialize for XOnlyPublicKey {
     fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
         XOnlyPublicKey::from_byte_array(
-            bytes.try_into().map_err(|_| Error::InvalidXOnlyPublicKey)?,
+            bytes[..32].try_into().expect("statistically impossible to hit"),
         )
         .map_err(|_| Error::InvalidXOnlyPublicKey)
     }
@@ -461,12 +461,6 @@ fn can_deserialize_non_standard_psbt_sighash_type() {
         assert!(sighash.is_ok())
     }
 
-    #[test]
-    fn deserialize_xonly_public_key_len() {
-        assert!(XOnlyPublicKey::deserialize(&[1; 31]).is_err());
-        assert!(XOnlyPublicKey::deserialize(&[1; 33]).is_err());
-    }
-
     #[test]
     #[should_panic(expected = "InvalidMagic")]
     fn invalid_vector_1() {
diff --git a/hashes/src/hash160/mod.rs b/hashes/src/hash160/mod.rs
index 282d653d..3c457c1a 100644
--- a/hashes/src/hash160/mod.rs
+++ b/hashes/src/hash160/mod.rs
@@ -38,9 +38,13 @@ fn default() -> Self { Self::new() }
 }
 
 impl crate::HashEngine for HashEngine {
+    type Hash = Hash;
+    type Bytes = [u8; 20];
     const BLOCK_SIZE: usize = 64; // Same as sha256::HashEngine::BLOCK_SIZE;
+
     fn input(&mut self, data: &[u8]) { self.0.input(data) }
     fn n_bytes_hashed(&self) -> u64 { self.0.n_bytes_hashed() }
+    fn finalize(self) -> Self::Hash { Hash::from_engine(self) }
 }
 
 #[cfg(test)]
@@ -128,7 +132,7 @@ fn ripemd_serde() {
 mod benches {
     use test::Bencher;
 
-    use crate::{hash160, GeneralHash as _, Hash as _, HashEngine};
+    use crate::{hash160, Hash as _, HashEngine};
 
     #[bench]
     pub fn hash160_10(bh: &mut Bencher) {
diff --git a/hashes/src/hkdf/mod.rs b/hashes/src/hkdf/mod.rs
index f02e0dd4..c381422d 100644
--- a/hashes/src/hkdf/mod.rs
+++ b/hashes/src/hkdf/mod.rs
@@ -11,7 +11,7 @@
 use alloc::vec::Vec;
 use core::fmt;
 
-use crate::{GeneralHash, HashEngine, Hmac, HmacEngine, IsByteArray};
+use crate::{HashEngine, Hmac, HmacEngine, IsByteArray};
 
 /// Output keying material max length multiple.
 const MAX_OUTPUT_BLOCKS: usize = 255;
@@ -33,20 +33,20 @@ impl std::error::Error for MaxLengthError {}
 
 /// HMAC-based Extract-and-Expand Key Derivation Function (HKDF).
 #[derive(Copy, Clone)]
-pub struct Hkdf<T: GeneralHash> {
+pub struct Hkdf<T: HashEngine> {
     /// Pseudorandom key based on the extract step.
-    prk: Hmac<T>,
+    prk: Hmac<T::Hash>,
 }
 
-impl<T: GeneralHash> Hkdf<T>
+impl<T: HashEngine> Hkdf<T>
 where
-    <T as GeneralHash>::Engine: Default,
+    T: Default,
 {
     /// Initialize a HKDF by performing the extract step.
     pub fn new(salt: &[u8], ikm: &[u8]) -> Self {
-        let mut hmac_engine: HmacEngine<T> = HmacEngine::new(salt);
-        hmac_engine.input(ikm);
-        Self { prk: Hmac::from_engine(hmac_engine) }
+        let mut engine: HmacEngine<T> = HmacEngine::new(salt);
+        engine.input(ikm);
+        Self { prk: engine.finalize() }
     }
 
     /// Expand the key to generate output key material in okm.
@@ -65,19 +65,19 @@ pub fn expand(&self, info: &[u8], okm: &mut [u8]) -> Result<(), MaxLengthError>
         let total_blocks = (okm.len() + T::Bytes::LEN - 1) / T::Bytes::LEN;
 
         while counter <= total_blocks as u8 {
-            let mut hmac_engine: HmacEngine<T> = HmacEngine::new(self.prk.as_ref());
+            let mut engine: HmacEngine<T> = HmacEngine::new(self.prk.as_ref());
 
             // First block does not have a previous block,
             // all other blocks include last block in the HMAC input.
             if counter != 1u8 {
                 let previous_start_index = (counter as usize - 2) * T::Bytes::LEN;
                 let previous_end_index = (counter as usize - 1) * T::Bytes::LEN;
-                hmac_engine.input(&okm[previous_start_index..previous_end_index]);
+                engine.input(&okm[previous_start_index..previous_end_index]);
             }
-            hmac_engine.input(info);
-            hmac_engine.input(&[counter]);
+            engine.input(info);
+            engine.input(&[counter]);
 
-            let t = Hmac::from_engine(hmac_engine);
+            let t = engine.finalize();
             let start_index = (counter as usize - 1) * T::Bytes::LEN;
             // Last block might not take full hash length.
             let end_index = if counter == (total_blocks as u8) {
@@ -106,7 +106,7 @@ pub fn expand_to_len(&self, info: &[u8], len: usize) -> Result<Vec<u8>, MaxLengt
     }
 }
 
-impl<T: GeneralHash> fmt::Debug for Hkdf<T> {
+impl<T: HashEngine> fmt::Debug for Hkdf<T> {
     fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
         use crate::{sha256t, sha256t_tag};
 
@@ -141,7 +141,7 @@ fn rfc5869_basic() {
         let ikm = Vec::from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
         let info = Vec::from_hex("f0f1f2f3f4f5f6f7f8f9").unwrap();
 
-        let hkdf = Hkdf::<sha256::Hash>::new(&salt, &ikm);
+        let hkdf = Hkdf::<sha256::HashEngine>::new(&salt, &ikm);
         let mut okm = [0u8; 42];
         hkdf.expand(&info, &mut okm).unwrap();
 
@@ -163,7 +163,7 @@ fn rfc5869_longer_inputs_outputs() {
             "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
         ).unwrap();
 
-        let hkdf = Hkdf::<sha256::Hash>::new(&salt, &ikm);
+        let hkdf = Hkdf::<sha256::HashEngine>::new(&salt, &ikm);
         let mut okm = [0u8; 82];
         hkdf.expand(&info, &mut okm).unwrap();
 
@@ -179,7 +179,7 @@ fn too_long_okm() {
         let ikm = Vec::from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
         let info = Vec::from_hex("f0f1f2f3f4f5f6f7f8f9").unwrap();
 
-        let hkdf = Hkdf::<sha256::Hash>::new(&salt, &ikm);
+        let hkdf = Hkdf::<sha256::HashEngine>::new(&salt, &ikm);
         let mut okm = [0u8; 256 * 32];
         let e = hkdf.expand(&info, &mut okm);
 
@@ -192,7 +192,7 @@ fn short_okm() {
         let ikm = Vec::from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
         let info = Vec::from_hex("f0f1f2f3f4f5f6f7f8f9").unwrap();
 
-        let hkdf = Hkdf::<sha256::Hash>::new(&salt, &ikm);
+        let hkdf = Hkdf::<sha256::HashEngine>::new(&salt, &ikm);
         let mut okm = [0u8; 1];
         hkdf.expand(&info, &mut okm).unwrap();
 
@@ -205,7 +205,7 @@ fn alloc_wrapper() {
         let ikm = Vec::from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
         let info = Vec::from_hex("f0f1f2f3f4f5f6f7f8f9").unwrap();
 
-        let hkdf = Hkdf::<sha256::Hash>::new(&salt, &ikm);
+        let hkdf = Hkdf::<sha256::HashEngine>::new(&salt, &ikm);
         let okm = hkdf.expand_to_len(&info, 42).unwrap();
 
         assert_eq!(
@@ -219,7 +219,7 @@ fn debug() {
         let salt = Vec::from_hex("000102030405060708090a0b0c").unwrap();
         let ikm = Vec::from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
 
-        let hkdf = Hkdf::<sha256::Hash>::new(&salt, &ikm);
+        let hkdf = Hkdf::<sha256::HashEngine>::new(&salt, &ikm);
         let debug = alloc::format!("{:?}", hkdf);
 
         assert_eq!(debug, "Hkdf(#ec7bd36ab2ed4045)");
diff --git a/hashes/src/hmac/mod.rs b/hashes/src/hmac/mod.rs
index 8215e48a..2d70253f 100644
--- a/hashes/src/hmac/mod.rs
+++ b/hashes/src/hmac/mod.rs
@@ -12,36 +12,26 @@
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 
-use crate::{GeneralHash, Hash, HashEngine};
+use crate::{Hash, HashEngine};
 
 /// A hash computed from a RFC 2104 HMAC. Parameterized by the underlying hash function.
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[repr(transparent)]
-pub struct Hmac<T: GeneralHash>(T);
+pub struct Hmac<T: Hash>(T);
 
-impl<T: GeneralHash> Hmac<T> {
-    /// Constructs a new keyed HMAC engine from `key`.
-    pub fn engine(key: &[u8]) -> HmacEngine<T>
-    where
-        <T as GeneralHash>::Engine: Default,
-    {
-        HmacEngine::new(key)
-    }
-}
-
-impl<T: GeneralHash + str::FromStr> str::FromStr for Hmac<T> {
+impl<T: Hash + str::FromStr> str::FromStr for Hmac<T> {
     type Err = <T as str::FromStr>::Err;
     fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(Hmac(str::FromStr::from_str(s)?)) }
 }
 
 /// Pair of underlying hash engines, used for the inner and outer hash of HMAC.
 #[derive(Debug, Clone)]
-pub struct HmacEngine<T: GeneralHash> {
-    iengine: T::Engine,
-    oengine: T::Engine,
+pub struct HmacEngine<T: HashEngine> {
+    iengine: T,
+    oengine: T,
 }
 
-impl<T: GeneralHash> HmacEngine<T> {
+impl<T: HashEngine> HmacEngine<T> {
     /// Constructs a new keyed HMAC engine from `key`.
     ///
     /// We only support underlying hashes whose block sizes are ≤ 128 bytes.
@@ -51,24 +41,23 @@ impl<T: GeneralHash> HmacEngine<T> {
     /// Larger hashes will result in a panic.
     pub fn new(key: &[u8]) -> HmacEngine<T>
     where
-        <T as GeneralHash>::Engine: Default,
+        T: Default,
     {
-        debug_assert!(T::Engine::BLOCK_SIZE <= 128);
+        debug_assert!(T::BLOCK_SIZE <= 128);
 
         let mut ipad = [0x36u8; 128];
         let mut opad = [0x5cu8; 128];
-        let mut ret = HmacEngine {
-            iengine: <T as GeneralHash>::engine(),
-            oengine: <T as GeneralHash>::engine(),
-        };
+        let mut ret = HmacEngine { iengine: T::default(), oengine: T::default() };
+
+        if key.len() > T::BLOCK_SIZE {
+            let mut engine = T::default();
+            engine.input(key);
+            let hash = engine.finalize();
 
-        if key.len() > T::Engine::BLOCK_SIZE {
-            let hash = <T as GeneralHash>::hash(key);
-            let hash = hash.as_byte_array().as_ref();
-            for (b_i, b_h) in ipad.iter_mut().zip(hash) {
+            for (b_i, b_h) in ipad.iter_mut().zip(hash.as_ref()) {
                 *b_i ^= *b_h;
             }
-            for (b_o, b_h) in opad.iter_mut().zip(hash) {
+            for (b_o, b_h) in opad.iter_mut().zip(hash.as_ref()) {
                 *b_o ^= *b_h;
             }
         } else {
@@ -80,54 +69,49 @@ pub fn new(key: &[u8]) -> HmacEngine<T>
             }
         };
 
-        HashEngine::input(&mut ret.iengine, &ipad[..T::Engine::BLOCK_SIZE]);
-        HashEngine::input(&mut ret.oengine, &opad[..T::Engine::BLOCK_SIZE]);
+        ret.iengine.input(&ipad[..T::BLOCK_SIZE]);
+        ret.oengine.input(&opad[..T::BLOCK_SIZE]);
         ret
     }
 
     /// A special constructor giving direct access to the underlying "inner" and "outer" engines.
-    pub fn from_inner_engines(iengine: T::Engine, oengine: T::Engine) -> HmacEngine<T> {
+    pub fn from_inner_engines(iengine: T, oengine: T) -> HmacEngine<T> {
         HmacEngine { iengine, oengine }
     }
 }
 
-impl<T: GeneralHash> HashEngine for HmacEngine<T> {
-    const BLOCK_SIZE: usize = T::Engine::BLOCK_SIZE;
+impl<T: HashEngine> HashEngine for HmacEngine<T> {
+    type Hash = Hmac<T::Hash>;
+    type Bytes = T::Bytes;
+    const BLOCK_SIZE: usize = T::BLOCK_SIZE;
 
     fn n_bytes_hashed(&self) -> u64 { self.iengine.n_bytes_hashed() }
-
     fn input(&mut self, buf: &[u8]) { self.iengine.input(buf) }
+    fn finalize(mut self) -> Self::Hash {
+        let ihash = self.iengine.finalize();
+        self.oengine.input(ihash.as_ref());
+        Hmac(self.oengine.finalize())
+    }
 }
 
-impl<T: GeneralHash + fmt::Debug> fmt::Debug for Hmac<T> {
+impl<T: Hash + fmt::Debug> fmt::Debug for Hmac<T> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(&self.0, f) }
 }
 
-impl<T: GeneralHash + fmt::Display> fmt::Display for Hmac<T> {
+impl<T: Hash + fmt::Display> fmt::Display for Hmac<T> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
 }
 
-impl<T: GeneralHash + fmt::LowerHex> fmt::LowerHex for Hmac<T> {
+impl<T: Hash + fmt::LowerHex> fmt::LowerHex for Hmac<T> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(&self.0, f) }
 }
 
-impl<T: GeneralHash> convert::AsRef<[u8]> for Hmac<T> {
+impl<T: Hash> convert::AsRef<[u8]> for Hmac<T> {
     // Calling as_byte_array is more reliable
     fn as_ref(&self) -> &[u8] { self.0.as_byte_array().as_ref() }
 }
 
-impl<T: GeneralHash> GeneralHash for Hmac<T> {
-    type Engine = HmacEngine<T>;
-
-    fn from_engine(mut e: HmacEngine<T>) -> Hmac<T> {
-        let ihash = T::from_engine(e.iengine);
-        e.oengine.input(ihash.as_byte_array().as_ref());
-        let ohash = T::from_engine(e.oengine);
-        Hmac(ohash)
-    }
-}
-
-impl<T: GeneralHash> Hash for Hmac<T> {
+impl<T: Hash> Hash for Hmac<T> {
     type Bytes = T::Bytes;
 
     fn from_byte_array(bytes: T::Bytes) -> Self { Hmac(T::from_byte_array(bytes)) }
@@ -143,14 +127,14 @@ fn as_byte_array(&self) -> &Self::Bytes { self.0.as_byte_array() }
 }
 
 #[cfg(feature = "serde")]
-impl<T: GeneralHash + Serialize> Serialize for Hmac<T> {
+impl<T: Hash + Serialize> Serialize for Hmac<T> {
     fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
         Serialize::serialize(&self.0, s)
     }
 }
 
 #[cfg(feature = "serde")]
-impl<'de, T: GeneralHash + Deserialize<'de>> Deserialize<'de> for Hmac<T> {
+impl<'de, T: Hash + Deserialize<'de>> Deserialize<'de> for Hmac<T> {
     fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Hmac<T>, D::Error> {
         let bytes = Deserialize::deserialize(d)?;
         Ok(Hmac(bytes))
@@ -165,14 +149,14 @@ fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Hmac<T>, D::Error> {
         Ok(buf.len())
     },
     |_us| { Ok(()) },
-    T: crate::GeneralHash
+    T: crate::HashEngine
 );
 
 #[cfg(test)]
 mod tests {
     #[test]
     fn test() {
-        use crate::{sha256, GeneralHash as _, Hash as _, HashEngine, Hmac, HmacEngine};
+        use crate::{sha256, Hash as _, HashEngine, HmacEngine};
 
         #[derive(Clone)]
         struct Test {
@@ -287,9 +271,9 @@ struct Test {
         ];
 
         for test in tests {
-            let mut engine = HmacEngine::<sha256::Hash>::new(test.key);
+            let mut engine = HmacEngine::<sha256::HashEngine>::new(test.key);
             engine.input(test.input);
-            let hash = Hmac::<sha256::Hash>::from_engine(engine);
+            let hash = engine.finalize();
             assert_eq!(hash.as_ref(), test.output);
             assert_eq!(hash.to_byte_array(), test.output);
         }
@@ -330,11 +314,11 @@ fn hmac_sha512_serde() {
 mod benches {
     use test::Bencher;
 
-    use crate::{sha256, GeneralHash as _, HashEngine, Hmac};
+    use crate::{sha256, HashEngine as _, HmacEngine};
 
     #[bench]
     pub fn hmac_sha256_10(bh: &mut Bencher) {
-        let mut engine = Hmac::<sha256::Hash>::engine(&[]);
+        let mut engine = HmacEngine::<sha256::HashEngine>::new(&[]);
         let bytes = [1u8; 10];
         bh.iter(|| {
             engine.input(&bytes);
@@ -344,7 +328,7 @@ pub fn hmac_sha256_10(bh: &mut Bencher) {
 
     #[bench]
     pub fn hmac_sha256_1k(bh: &mut Bencher) {
-        let mut engine = Hmac::<sha256::Hash>::engine(&[]);
+        let mut engine = HmacEngine::<sha256::HashEngine>::new(&[]);
         let bytes = [1u8; 1024];
         bh.iter(|| {
             engine.input(&bytes);
@@ -354,7 +338,7 @@ pub fn hmac_sha256_1k(bh: &mut Bencher) {
 
     #[bench]
     pub fn hmac_sha256_64k(bh: &mut Bencher) {
-        let mut engine = Hmac::<sha256::Hash>::engine(&[]);
+        let mut engine = HmacEngine::<sha256::HashEngine>::new(&[]);
         let bytes = [1u8; 65536];
         bh.iter(|| {
             engine.input(&bytes);
diff --git a/hashes/src/internal_macros.rs b/hashes/src/internal_macros.rs
index fe5d6aa7..5e959932 100644
--- a/hashes/src/internal_macros.rs
+++ b/hashes/src/internal_macros.rs
@@ -26,12 +26,6 @@ macro_rules! hash_trait_impls {
         #[cfg(not(feature = "hex"))]
         $crate::impl_debug_only!(Hash, { $bits / 8 }, $reverse $(, $gen: $gent)*);
 
-        impl<$($gen: $gent),*> $crate::GeneralHash for Hash<$($gen),*> {
-            type Engine = HashEngine<$($gen),*>;
-
-            fn from_engine(e: Self::Engine) -> Hash<$($gen),*> { Self::from_engine(e) }
-        }
-
         #[cfg(feature = "serde")]
         $crate::serde_impl!(Hash, { $bits / 8} $(, $gen: $gent)*);
 
@@ -71,6 +65,30 @@ fn as_byte_array(&self) -> &Self::Bytes { self.as_byte_array() }
 /// [`hash_trait_impls`].
 macro_rules! general_hash_type {
     ($bits:expr, $reverse:expr, $doc:literal) => {
+        /// Hashes some bytes.
+        pub fn hash(data: &[u8]) -> Hash {
+            use crate::HashEngine as _;
+
+            let mut engine = Hash::engine();
+            engine.input(data);
+            engine.finalize()
+        }
+
+        /// Hashes all the byte slices retrieved from the iterator together.
+        pub fn hash_byte_chunks<B, I>(byte_slices: I) -> Hash
+        where
+            B: AsRef<[u8]>,
+            I: IntoIterator<Item = B>,
+        {
+            use crate::HashEngine as _;
+
+            let mut engine = Hash::engine();
+            for slice in byte_slices {
+                engine.input(slice.as_ref());
+            }
+            engine.finalize()
+        }
+
         $crate::internal_macros::hash_type_no_default!($bits, $reverse, $doc);
 
         impl Hash {
@@ -82,7 +100,7 @@ pub fn engine() -> HashEngine { Default::default() }
 
             /// Hashes some bytes.
             #[allow(clippy::self_named_constructors)] // Hash is a noun and a verb.
-            pub fn hash(data: &[u8]) -> Self { <Self as $crate::GeneralHash>::hash(data) }
+            pub fn hash(data: &[u8]) -> Self { hash(data) }
 
             /// Hashes all the byte slices retrieved from the iterator together.
             pub fn hash_byte_chunks<B, I>(byte_slices: I) -> Self
@@ -90,7 +108,7 @@ pub fn hash_byte_chunks<B, I>(byte_slices: I) -> Self
                 B: AsRef<[u8]>,
                 I: IntoIterator<Item = B>,
             {
-                <Self as $crate::GeneralHash>::hash_byte_chunks(byte_slices)
+                hash_byte_chunks(byte_slices)
             }
         }
     };
diff --git a/hashes/src/lib.rs b/hashes/src/lib.rs
index 06cfbcda..b3962214 100644
--- a/hashes/src/lib.rs
+++ b/hashes/src/lib.rs
@@ -186,6 +186,15 @@ pub mod serde_details {
 
 /// A hashing engine which bytes can be serialized into.
 pub trait HashEngine: Clone {
+    /// The `Hash` type returned when finalizing this engine.
+    type Hash: Hash;
+
+    /// The byte array that is used internally in `finalize`.
+    type Bytes: Copy + IsByteArray;
+
+    /// Length of the hash, in bytes.
+    const LEN: usize = Self::Bytes::LEN;
+
     /// Length of the hash's internal block size, in bytes.
     const BLOCK_SIZE: usize;
 
@@ -194,52 +203,9 @@ pub trait HashEngine: Clone {
 
     /// Return the number of bytes already input into the engine.
     fn n_bytes_hashed(&self) -> u64;
-}
 
-/// Trait describing hash digests which can be constructed by hashing arbitrary data.
-///
-/// Some methods have been bound to engines which implement Default, which is
-/// generally an unkeyed hash function.
-pub trait GeneralHash: Hash {
-    /// A hashing engine which bytes can be serialized into. It is expected
-    /// to implement the `io::Write` trait, and to never return errors under
-    /// any conditions.
-    type Engine: HashEngine;
-
-    /// Constructs a new engine.
-    fn engine() -> Self::Engine
-    where
-        Self::Engine: Default,
-    {
-        Self::Engine::default()
-    }
-
-    /// Produces a hash from the current state of a given engine.
-    fn from_engine(e: Self::Engine) -> Self;
-
-    /// Hashes some bytes.
-    fn hash(data: &[u8]) -> Self
-    where
-        Self::Engine: Default,
-    {
-        let mut engine = Self::engine();
-        engine.input(data);
-        Self::from_engine(engine)
-    }
-
-    /// Hashes all the byte slices retrieved from the iterator together.
-    fn hash_byte_chunks<B, I>(byte_slices: I) -> Self
-    where
-        B: AsRef<[u8]>,
-        I: IntoIterator<Item = B>,
-        Self::Engine: Default,
-    {
-        let mut engine = Self::engine();
-        for slice in byte_slices {
-            engine.input(slice.as_ref());
-        }
-        Self::from_engine(engine)
-    }
+    /// Finalizes this engine.
+    fn finalize(self) -> Self::Hash;
 }
 
 /// Trait which applies to hashes of all types.
diff --git a/hashes/src/ripemd160/mod.rs b/hashes/src/ripemd160/mod.rs
index e6be1753..d6ec93f9 100644
--- a/hashes/src/ripemd160/mod.rs
+++ b/hashes/src/ripemd160/mod.rs
@@ -87,9 +87,11 @@ fn default() -> Self { Self::new() }
 }
 
 impl crate::HashEngine for HashEngine {
+    type Hash = Hash;
+    type Bytes = [u8; 20];
     const BLOCK_SIZE: usize = 64;
 
     fn n_bytes_hashed(&self) -> u64 { self.bytes_hashed }
-
     crate::internal_macros::engine_input_impl!();
+    fn finalize(self) -> Self::Hash { Hash::from_engine(self) }
 }
diff --git a/hashes/src/sha1/mod.rs b/hashes/src/sha1/mod.rs
index dc32d31d..18f62025 100644
--- a/hashes/src/sha1/mod.rs
+++ b/hashes/src/sha1/mod.rs
@@ -79,9 +79,13 @@ fn default() -> Self { Self::new() }
 }
 
 impl crate::HashEngine for HashEngine {
+    type Hash = Hash;
+    type Bytes = [u8; 20];
     const BLOCK_SIZE: usize = 64;
 
     fn n_bytes_hashed(&self) -> u64 { self.bytes_hashed }
 
     crate::internal_macros::engine_input_impl!();
+
+    fn finalize(self) -> Self::Hash { Hash::from_engine(self) }
 }
diff --git a/hashes/src/sha256/mod.rs b/hashes/src/sha256/mod.rs
index 6ae83b87..5d8fe125 100644
--- a/hashes/src/sha256/mod.rs
+++ b/hashes/src/sha256/mod.rs
@@ -128,19 +128,19 @@ fn default() -> Self { Self::new() }
 }
 
 impl crate::HashEngine for HashEngine {
+    type Hash = Hash;
+    type Bytes = [u8; 32];
     const BLOCK_SIZE: usize = 64;
 
     fn n_bytes_hashed(&self) -> u64 { self.bytes_hashed }
-
     crate::internal_macros::engine_input_impl!();
+    fn finalize(self) -> Self::Hash { Hash::from_engine(self) }
 }
 
 impl Hash {
     /// Iterate the sha256 algorithm to turn a sha256 hash into a sha256d hash
     #[must_use]
-    pub fn hash_again(&self) -> sha256d::Hash {
-        crate::Hash::from_byte_array(<Self as crate::GeneralHash>::hash(&self.0).0)
-    }
+    pub fn hash_again(&self) -> sha256d::Hash { sha256d::Hash::from_byte_array(hash(&self.0).0) }
 
     /// Computes hash from `bytes` in `const` context.
     ///
diff --git a/hashes/src/sha256d/mod.rs b/hashes/src/sha256d/mod.rs
index adb2ffa7..69bb8a72 100644
--- a/hashes/src/sha256d/mod.rs
+++ b/hashes/src/sha256d/mod.rs
@@ -33,9 +33,13 @@ fn default() -> Self { Self::new() }
 }
 
 impl crate::HashEngine for HashEngine {
+    type Hash = Hash;
+    type Bytes = [u8; 32];
     const BLOCK_SIZE: usize = 64; // Same as sha256::HashEngine::BLOCK_SIZE;
+
     fn input(&mut self, data: &[u8]) { self.0.input(data) }
     fn n_bytes_hashed(&self) -> u64 { self.0.n_bytes_hashed() }
+    fn finalize(self) -> Self::Hash { Hash::from_engine(self) }
 }
 
 #[cfg(test)]
diff --git a/hashes/src/sha256t/mod.rs b/hashes/src/sha256t/mod.rs
index 19a6ef76..d5acd5ed 100644
--- a/hashes/src/sha256t/mod.rs
+++ b/hashes/src/sha256t/mod.rs
@@ -9,6 +9,34 @@
 use crate::sha256::Midstate;
 use crate::{sha256, HashEngine as _};
 
+/// Hashes some bytes.
+pub fn hash<T>(data: &[u8]) -> Hash<T>
+where
+    T: Tag,
+{
+    use crate::HashEngine as _;
+
+    let mut engine = HashEngine::default();
+    engine.input(data);
+    engine.finalize()
+}
+
+/// Hashes all the byte slices retrieved from the iterator together.
+pub fn hash_byte_chunks<B, I, T>(byte_slices: I) -> Hash<T>
+where
+    B: AsRef<[u8]>,
+    I: IntoIterator<Item = B>,
+    T: Tag,
+{
+    use crate::HashEngine as _;
+
+    let mut engine = HashEngine::default();
+    for slice in byte_slices {
+        engine.input(slice.as_ref());
+    }
+    engine.finalize()
+}
+
 /// Trait representing a tag that can be used as a context for SHA256t hashes.
 pub trait Tag {
     /// The [`Midstate`] after pre-tagging the hash engine.
@@ -133,9 +161,13 @@ fn clone(&self) -> Self { Self(self.0.clone(), PhantomData) }
 }
 
 impl<T: Tag> crate::HashEngine for HashEngine<T> {
+    type Hash = Hash<T>;
+    type Bytes = [u8; 32];
     const BLOCK_SIZE: usize = 64; // Same as sha256::HashEngine::BLOCK_SIZE;
+
     fn input(&mut self, data: &[u8]) { self.0.input(data) }
     fn n_bytes_hashed(&self) -> u64 { self.0.n_bytes_hashed() }
+    fn finalize(self) -> Self::Hash { Hash::from_engine(self) }
 }
 
 crate::internal_macros::impl_write!(
diff --git a/hashes/src/sha384/mod.rs b/hashes/src/sha384/mod.rs
index c20b356d..3d6538af 100644
--- a/hashes/src/sha384/mod.rs
+++ b/hashes/src/sha384/mod.rs
@@ -30,11 +30,13 @@ fn default() -> Self { Self::new() }
 }
 
 impl crate::HashEngine for HashEngine {
+    type Hash = Hash;
+    type Bytes = [u8; 48];
     const BLOCK_SIZE: usize = sha512::BLOCK_SIZE;
 
     fn n_bytes_hashed(&self) -> u64 { self.0.n_bytes_hashed() }
-
     fn input(&mut self, inp: &[u8]) { self.0.input(inp); }
+    fn finalize(self) -> Self::Hash { Hash::from_engine(self) }
 }
 
 #[cfg(test)]
diff --git a/hashes/src/sha512/mod.rs b/hashes/src/sha512/mod.rs
index 65bf75c1..0256cde2 100644
--- a/hashes/src/sha512/mod.rs
+++ b/hashes/src/sha512/mod.rs
@@ -120,9 +120,11 @@ pub(crate) const fn sha384() -> Self {
 }
 
 impl crate::HashEngine for HashEngine {
+    type Hash = Hash;
+    type Bytes = [u8; 64];
     const BLOCK_SIZE: usize = 128;
 
     fn n_bytes_hashed(&self) -> u64 { self.bytes_hashed }
-
     crate::internal_macros::engine_input_impl!();
+    fn finalize(self) -> Self::Hash { Hash::from_engine(self) }
 }
diff --git a/hashes/src/sha512_256/mod.rs b/hashes/src/sha512_256/mod.rs
index 40b4ca65..cf12d460 100644
--- a/hashes/src/sha512_256/mod.rs
+++ b/hashes/src/sha512_256/mod.rs
@@ -40,11 +40,13 @@ fn default() -> Self { Self::new() }
 }
 
 impl crate::HashEngine for HashEngine {
+    type Hash = Hash;
+    type Bytes = [u8; 64];
     const BLOCK_SIZE: usize = sha512::BLOCK_SIZE;
 
     fn n_bytes_hashed(&self) -> u64 { self.0.n_bytes_hashed() }
-
     fn input(&mut self, inp: &[u8]) { self.0.input(inp); }
+    fn finalize(self) -> Self::Hash { Hash::from_engine(self) }
 }
 
 #[cfg(test)]
diff --git a/hashes/src/siphash24/mod.rs b/hashes/src/siphash24/mod.rs
index 1db74773..50abe857 100644
--- a/hashes/src/siphash24/mod.rs
+++ b/hashes/src/siphash24/mod.rs
@@ -121,6 +121,8 @@ fn d_rounds(state: &mut State) {
 }
 
 impl crate::HashEngine for HashEngine {
+    type Hash = Hash;
+    type Bytes = [u8; 8];
     const BLOCK_SIZE: usize = 8;
 
     #[inline]
@@ -165,6 +167,8 @@ fn input(&mut self, msg: &[u8]) {
     }
 
     fn n_bytes_hashed(&self) -> u64 { self.bytes_hashed }
+
+    fn finalize(self) -> Self::Hash { Hash::from_engine(self) }
 }
 
 impl Hash {
diff --git a/io/src/hash.rs b/io/src/hash.rs
index a6a364e9..859f035b 100644
--- a/io/src/hash.rs
+++ b/io/src/hash.rs
@@ -10,9 +10,11 @@
 use hashes::hmac::HmacEngine;
 use hashes::{
     hash160, ripemd160, sha1, sha256, sha256d, sha256t, sha384, sha512, sha512_256, siphash24,
-    GeneralHash, HashEngine as _,
+    HashEngine as _,
 };
 
+use crate::BufRead;
+
 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.
@@ -128,43 +130,28 @@ fn flush(&mut self) -> crate::Result<()> {
         Ok(buf.len())
     },
     |_us| { Ok(()) },
-    T: hashes::GeneralHash
+    T: hashes::HashEngine
 );
 
 /// 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);
+pub fn hash_reader<T>(reader: &mut impl BufRead) -> Result<T::Hash, crate::Error>
+where
+    T: hashes::HashEngine + Default,
+{
+    let mut engine = T::default();
+    loop {
+        let bytes = reader.fill_buf()?;
+
+        let read = bytes.len();
+        // Empty slice means EOF.
+        if read == 0 {
+            break;
         }
-        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 {}
+        engine.input(bytes);
+        reader.consume(read);
+    }
+    Ok(engine.finalize())
 }
 
 #[cfg(test)]
@@ -172,7 +159,7 @@ impl<T: hashes::GeneralHash> Sealed for T {}
 mod tests {
     use alloc::format;
 
-    use hashes::{hmac, Hmac};
+    use hashes::hmac;
 
     use super::*;
     use crate::{Cursor, Write as _};
@@ -257,24 +244,24 @@ fn $mod() {
 
     #[test]
     fn hmac() {
-        let mut engine = hmac::HmacEngine::<sha256::Hash>::new(&[0xde, 0xad, 0xbe, 0xef]);
+        let mut engine = hmac::HmacEngine::<sha256::HashEngine>::new(&[0xde, 0xad, 0xbe, 0xef]);
         engine.write_all(&[]).unwrap();
         assert_eq!(
-            format!("{}", hmac::Hmac::from_engine(engine)),
+            format!("{}", engine.finalize()),
             "bf5515149cf797955c4d3194cca42472883281951697c8375d9d9b107f384225"
         );
 
-        let mut engine = hmac::HmacEngine::<sha256::Hash>::new(&[0xde, 0xad, 0xbe, 0xef]);
+        let mut engine = hmac::HmacEngine::<sha256::HashEngine>::new(&[0xde, 0xad, 0xbe, 0xef]);
         engine.write_all(&[1; 256]).unwrap();
         assert_eq!(
-            format!("{}", hmac::Hmac::from_engine(engine)),
+            format!("{}", engine.finalize()),
             "59c9aca10c81c73cb4c196d94db741b6bf2050e0153d5a45f2526bff34675ac5"
         );
 
-        let mut engine = hmac::HmacEngine::<sha256::Hash>::new(&[0xde, 0xad, 0xbe, 0xef]);
+        let mut engine = hmac::HmacEngine::<sha256::HashEngine>::new(&[0xde, 0xad, 0xbe, 0xef]);
         engine.write_all(&[99; 64000]).unwrap();
         assert_eq!(
-            format!("{}", hmac::Hmac::from_engine(engine)),
+            format!("{}", engine.finalize()),
             "30df499717415a395379a1eaabe50038036e4abb5afc94aa55c952f4aa57be08"
         );
     }
@@ -308,7 +295,7 @@ fn $test_name() {
                     assert_eq!(got, $want);
 
                     let mut reader = Cursor::new(DATA);
-                    let hash_from_reader = $module::Hash::hash_reader(&mut reader).unwrap();
+                    let hash_from_reader = $crate::hash_reader::<$module::HashEngine>(&mut reader).unwrap();
                     assert_eq!(hash_from_reader, hash)
                 }
             )*
@@ -345,9 +332,9 @@ fn regression_sha256t() {
 
     #[test]
     fn regression_hmac_sha256_with_key() {
-        let mut engine = HmacEngine::<sha256::Hash>::new(HMAC_KEY);
+        let mut engine = HmacEngine::<sha256::HashEngine>::new(HMAC_KEY);
         engine.input(DATA.as_bytes());
-        let hash = Hmac::from_engine(engine);
+        let hash = engine.finalize();
 
         let got = format!("{}", hash);
         let want = "d159cecaf4adf90b6a641bab767e4817d3a51c414acea3682686c35ec0b37b52";
@@ -356,9 +343,9 @@ fn regression_hmac_sha256_with_key() {
 
     #[test]
     fn regression_hmac_sha512_with_key() {
-        let mut engine = HmacEngine::<sha512::Hash>::new(HMAC_KEY);
+        let mut engine = HmacEngine::<sha512::HashEngine>::new(HMAC_KEY);
         engine.input(DATA.as_bytes());
-        let hash = Hmac::from_engine(engine);
+        let hash = engine.finalize();
 
         let got = format!("{}", hash);
         let want = "8511773748f89ba22c07fb3a2981a12c1823695119de41f4a62aead6b848bd34939acf16475c35ed7956114fead3e794cc162ecd35e447a4dabc3227d55f757b";
diff --git a/io/src/lib.rs b/io/src/lib.rs
index 0b7ee509..8da2d108 100644
--- a/io/src/lib.rs
+++ b/io/src/lib.rs
@@ -46,7 +46,7 @@
 #[rustfmt::skip]                // Keep public re-exports separate.
 pub use self::error::{Error, ErrorKind};
 #[cfg(feature = "hashes")]
-pub use self::hash::GeneralHashExt;
+pub use self::hash::hash_reader;
 
 /// Result type returned by functions in this crate.
 pub type Result<T> = core::result::Result<T, Error>;


Test patch:
diff --git a/hashes/tests/regression.rs b/hashes/tests/regression.rs
index 319d6ecf..31aeec5f 100644
--- a/hashes/tests/regression.rs
+++ b/hashes/tests/regression.rs
@@ -8,7 +8,7 @@
 
 use bitcoin_hashes::{
     hash160, ripemd160, sha1, sha256, sha256d, sha256t, sha384, sha512, sha512_256, siphash24,
-    GeneralHash as _, HashEngine as _, Hmac, HmacEngine,
+    HashEngine as _, HmacEngine,
 };
 
 const DATA: &str = "arbitrary data to hash as a regression test";
@@ -57,9 +57,9 @@ fn regression_sha256t() {
 
 #[test]
 fn regression_hmac_sha256_with_key() {
-    let mut engine = HmacEngine::<sha256::Hash>::new(HMAC_KEY);
+    let mut engine = HmacEngine::<sha256::HashEngine>::new(HMAC_KEY);
     engine.input(DATA.as_bytes());
-    let hash = Hmac::from_engine(engine);
+    let hash = engine.finalize();
 
     let got = format!("{}", hash);
     let want = "d159cecaf4adf90b6a641bab767e4817d3a51c414acea3682686c35ec0b37b52";
@@ -68,9 +68,9 @@ fn regression_hmac_sha256_with_key() {
 
 #[test]
 fn regression_hmac_sha512_with_key() {
-    let mut engine = HmacEngine::<sha512::Hash>::new(HMAC_KEY);
+    let mut engine = HmacEngine::<sha512::HashEngine>::new(HMAC_KEY);
     engine.input(DATA.as_bytes());
-    let hash = Hmac::from_engine(engine);
+    let hash = engine.finalize();
 
     let got = format!("{}", hash);
     let want = "8511773748f89ba22c07fb3a2981a12c1823695119de41f4a62aead6b848bd34939acf16475c35ed7956114fead3e794cc162ecd35e447a4dabc3227d55f757b";
@@ -87,3 +87,13 @@ fn regression_siphash24_with_key() {
     let want = "e823ed82311d601a";
     assert_eq!(got, want);
 }
+
+#[test]
+fn regression_sha256_hash_again() {
+    let hash = sha256::Hash::hash(b"Don't explain your philosophy. Embody it.");
+    let again = hash.hash_again();
+
+    let got = format!("{}", again);
+    let want = "28273103bcd88ab99e2b1007174770ff3f0ea91ee4b3ac942879ed1a2d264b4c";
+    assert_eq!(got, want);
+}