Repository: rust-bitcoin/rust-bitcoin
Pull request #4007 (2025-02-04 22:25:40)

Introduce monadic `NumOpResult` type
We would like to return an error when doing math ops on amount types. We cannot however use the stdlib `Result` or `Option` because we want to implement ops on the result type.

Add an `NumOpResult` type. Return this type from all math operations on `Amount` and `SignedAmount`.

Implement `core::iter::Sum` for the new type to allow summing iterators of amounts. 

Note please this removes `AddAssign` impls for amount types.

apoelstra: > Why do we support ops::Add at all if using it is so verbose/painful?

Because `+` is 10 characters less than `checked_add`, uses infix notation rather than postfix, and it's also the first thing users reach for.

It's also canonical for `checked_add` to return an `Option<Self>` not an `AmountResult`. (Ok, it's also canonical for `+` to return a `Self` rather than an `AmountResult`. But the type errors for somebody getting an `AmountResult` instead of an `Amount` will be much clearer than those for somebody getting an `AmountResult` instead of an `Option<Amount>`.)

But it's true that if you're just adding two amounts and doing nothing else, the benefit of this type is not very clear. It becomes much more useful when you're summing stuff (there is no `checked_sum` equivalent for iterators) or when you're chaining multiple operations.
tcharding: > It becomes much more useful when you're summing stuff (there is no checked_sum equivalent for iterators) or when you're chaining multiple operations.

oooo, this makes sense. Thanks
tcharding: What am I missing?

This fails to build because `Sum` must return `Self`
```rust
impl<'a> core::iter::Sum<&'a Amount> for Amount {
    fn sum<I>(iter: I) -> AmountOpResult<Amount>
    where
        I: Iterator<Item = &'a Amount>,
    {
        iter.fold(AmountOpResult::Valid(Amount::ZERO), |acc, amount| {
            match acc {
                AmountOpResult::Valid(lhs) => lhs + amount,
                AmountOpResult::Error(e) => AmountOpResult::Error(e),
            }
        })
    }
}
```

But we cannot sum the amounts as sats then convert to an `AmountOpResult` because it would still be possible to overflow?

Any tips please?
Kixunil: > I don't see how that is useful when one can just write:

Because you missed those other traits. It makes sense with larger expressions. And yes, they are common.

> This fails to build because `Sum` must return `Self`

Of course, because you need to `impl Sum<Amount> for AmountOpResult<Amount>` etc. I might have confused you by complaining about `Sum` being removed. It's not about `Sum` on `Amount` specifically but about having something that can use the standardized `Sum` trait. Yes, writing `iter.sum::<AmountOpResult<_>>()` is quite a bit more annoying than `iter.sum::<Amount>()` but I don't think we can do anything about it.
Kixunil: Also, we want this for `Weight` and `FeeRate`. Which actually suggests the name `NumOpResult` (I'm really happy with a shorter one.)

And I'd also love to see `impl Div<NonZeroU64> for Amount { type Output = Amount; ... }` & co, but it's orthogonal.
apoelstra: It's hard for me to imagine `Weight` or `FeeRate` ever coming close to overflowing a u32, let alone a u64. For those I think we should keep the "panic on overflow" behavior that std has.

`Weight` also has the issue that it's created by various weight-estimating methods rather than by users doing arithmetic (you *can* do arithmetic and this makes sense, it's just not a typical thing to do). We'd have to consider what happens when we "overflow" because the user has a `Transaction` with 2^60 inputs or a script of size 2^63 or whatever.

Similarly it's not too common to do arithmetic with `FeeRate`s. Typically you create one by dividing an amount by a kwu or vb or whatever (BTW, we should maybe deprecate the `from_sat_per_X` methods in favor of new `from_amount_per_X`. The new `from_sat_per_vb`, which currently returns an `Option` due to overflow, could then be infallible, as long as `Amount` is limited below u64::MAX/250. (Another API benefit of capping `Amount` to MAX_MONEY, btw.))

The other thing you do with a `FeeRate` is multiply it by `Weight` to get an `Amount`. You should get an `AmountResult` here.  (I wouldn't be totally opposed to turning `FeeRate` internally into something like `AmountResult` which would be "poisoned" if you did something crazy, such that you could no longer get a fee `Amount` out of it. But changing the arithmetic opcodes to do non-idiomatic things, on types that (a) don't represent money and (b) are typically an order of magnitude of order of magnitudes away from overflowing, I feel is a bridge too far.)

> And I'd also love to see `impl Div<NonZeroU64> for Amount { type Output = Amount; ... }` & co, but it's orthogonal.

Ooh, yeah, I wouldn't mind having that on everything that we currently allow numeric division on. But yes, it's orthogonal and can be done post-1.0 so let's try to keep it out of scope here.
Kixunil: > It's hard for me to imagine `Weight` or `FeeRate` ever coming close to overflowing a u32, let alone a u64.

> The other thing you do with a `FeeRate` is multiply it by `Weight` to get an `Amount`. You should get an `AmountResult` here.

I think you're assuming `Weight` is computed by passing in the actual `Transaction` (which would have to be super large) and `FeeRate` is given by the user.

The first is not true because it could be computed by passing accidentally wrong values to weight prediction, the second is not true because it can come from a fee estimation RPC call that could due to a bug return a wrong value. And even for user inputs, crashing UI because the user mistyped a value is not great.

However, maybe we should have invariants on those two as well. Limit Weight to 4M and `FeeRate` to 2100000000000000/4000000. Then the multiplication never overflows in the first place. I guess I wouldn't mind panicking on wrong `Weight` construction but for `FeeRate` I'd prefer to return `Result` precisely because it can come from UI.

Anyway, if we don't want the other invariants then I want fee computation to return `AmountOpResult` (`NumOpResult`), `FeeRate` conversion should stay as-is (returning `Option`) and the rest may panic.

> BTW, we should maybe deprecate the `from_sat_per_X` methods in favor of new `from_amount_per_X`

That is too obnoxious. If you're getting either from the user (UI), requiring another function call just to convert the type is pointless. And maybe even worse, constructing `Amount` from a number that's semantically not an amount but a fee rate is just wrong. It might have been even a mistake to add those methods. I think this at least deserves a doc note.

> I wouldn't be totally opposed to turning `FeeRate` internally into something like `AmountResult` which would be "poisoned" if you did something crazy, such that you could no longer get a fee `Amount` out of it.

Interesting idea! However I'm not too worried about people doing crazy operations on `FeeRate`. The only realistic code that I've seen (actually writing these days :)) that might possibly use it is using multiple presigned transactions with different fee rates and you might want to generate the range of possible rates with some math function. But it'll likely involve addition which `FeeRate` doesn't support anyway. Still, even my own code opts to just accept the configuration as an array, not math function.
apoelstra: >And even for user inputs, crashing UI because the user mistyped a value is not great.

I have three thoughts about this:

So, Rust's panicking arithmetic opcodes *in general* cause this sort of UX. I have heard from non-Rust people that they consider Rust code to be prone to crashes because of this sort of thing. I agree that we should try to discourage this but we're fighting against the tide here and we should consider our budget for non-idiomatic APIs.

Secondly, if we get crazy amounts because of a bug in somebody's code then it's fine to panic. If we return a `NumResult` and the author believes that overflow can never happen then they'll just do an `unwrap` and the result is the same but noisier. And if the author **does think that overflow can happen**, then...

...this person's code is already susceptible to money-losing UX bugs. The difference between fat-fingering a feerate so that it overflows `u64` and the user fat-fingering a feerate so that they spend all their coins to fees is just a matter of magnitude. So downstream software will need to set some sort of maximum *anyway* (e.g. I think Core won't let you put an absolute fee greater than 0.1BTC or something unless you pass an additional flag) and if we're also returning errors about this then downstream has more complicated error logic without reason.
Kixunil: > Rust's panicking arithmetic opcodes _in general_ cause this sort of UX.

Yes, but crash because of a programmer mistake is materially different from crash because user entered a wrong amount. User can't debug the former either way but can the latter. You wouldn't be wrong arguing that the second is actually an instance of the first where the programmer didn't read the docs properly and just blindly passed in the `u64` but if we can reasonably cheaply prevent it I'd much like that. It seems to me that the invariants I suggested are reasonably cheap but so is returning `AmountOpResult` from `Weight * FeeRate` as that is the most common operation. Anyway, I don't care too much which of the two we pick as long as the common path is panic-free.

> I have heard from non-Rust people that they consider Rust code to be prone to crashes because of this sort of thing.

I'd love to know if they have specific examples in mind.

> If we return a `NumResult` and the author believes that overflow can never happen then they'll just do an `unwrap` and the result is the same but noisier.

This is true in Rust everywhere, yet `Option` is useful despite having the `unwrap` method. The thing is people often forget about a check, they don't "just think error won't happen" and compiler error makes them remember. This is my experience at least but a strong one.

> this person's code is already susceptible to money-losing UX bugs. The difference between fat-fingering a feerate so that it overflows `u64` and the user fat-fingering a feerate so that they spend all their coins to fees is just a matter of magnitude

Not really. Most wallets work in two steps: they'll allow you to enter the fee rate among other things, then predict weight and show a transaction summary. It's up to the user to review the summary and confirm only if it checks out. (I've seen some wallet disable the confirm button for 3 seconds to force users to think. A good idea, I believe.) If the overflow causes panic, the UI just crashes. If it doesn't but the user entered huge fee rate regardless, maybe there's even not enough to pay for miner fees and an error is shown (so the real operation is `let change_amount = sum_inputs - payment_amount - weight * fee_rate`). And finally, if the user fat-fingers fee to consume the vast majority of his money there's still change to fix it when the UI shows -large_value.

But you do have a point that additional protection is helpful and perhaps our default methods should block ridiculous amounts and we should have other methods to bypass them.

> if we're also returning errors about this then downstream has more complicated error logic without reason.

If we make the errors composable then it's not really that different. But I think this even supports my idea about additional invariants. Something like `(inputs.iter().sum::<AmountOpResult<_>>() - payment_amount - FeeRate::from_sat_kwu(rate) * weight).into_result()?` Is pretty good and takes care of a good chunk of error handling.
tcharding: Now fleshed out. Please see new PR description.
Kixunil: > Note that because `AmountOpResult` does not (and can not) implement `Copy` we have a fair bit of duplicate code to handle dereferencing the various ref combinations.

Oh, crap. I totally forgot about this. But I believe we can make it `Copy`. I've just realized `panic::Location` gives us `&'static`, which is `Copy` and I don't plan to store other exotic types int the error.


tcharding: > You've defined it but didn't call.

I couldn't work out this comment.


tcharding: There are a bunch of FIXME in there that are open questions please.
tcharding: 900 line diff but most of it is boilerplate and doesn't really need reading. Just look at the tests and convince yourself that we support what we want to and don't support things we don't want to.
tcharding: Nice observation re `ok` not `unwrap`. Since its internal I shortened it to `ok_or_error`. This also allowed removing the private `error` function.
tcharding: Force pushed changes to remove pattern matching in favour of `ok_or_error` (including the `OptionExt` trait method rename).
Kixunil: > I shortened it to `ok_or_error`

I feel like that will be too confusing since `Option` already has `ok_or_err`.
tcharding: This PR is blocking #3794 so I implemented most of the suggestions and promoted things that are just "add this impl" to separate issues. No one marked the last review commit so in case you wanted to use `range-diff` and can't here are the changes to my index before I force push

<details open>
<summary>The diff</summary>
<br>

```diff
diff --git a/bitcoin/examples/taproot-psbt.rs b/bitcoin/examples/taproot-psbt.rs
index 1bfef757..a2152ab0 100644
--- a/bitcoin/examples/taproot-psbt.rs
+++ b/bitcoin/examples/taproot-psbt.rs
@@ -414,3 +414,3 @@ fn create_inheritance_funding_tx(
         );
-        let value = (input_utxo.amount - ABSOLUTE_FEES).unwrap();
+        let value = (input_utxo.amount - ABSOLUTE_FEES).expect("ABSOLUTE_FEES must be set below input amount");
 
@@ -651,3 +651,3 @@ fn spend_inheritance(
             script_pubkey: to_address.script_pubkey(),
-            value: (input_value - ABSOLUTE_FEES).unwrap(),
+            value: (input_value - ABSOLUTE_FEES).expect("ABSOLUTE_FEES must be set below input amount"),
         }];
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index f9d4f38f..2471fc41 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -2251,3 +2251,3 @@ fn fee() {
             t.fee().expect("fee calculation"),
-            (prev_output_val - (output_0_val + output_1_val).unwrap()).unwrap()
+            (prev_output_val - (output_0_val + output_1_val)).unwrap()
         );
diff --git a/units/src/amount/result.rs b/units/src/amount/result.rs
index 6e252453..c0e84f6e 100644
--- a/units/src/amount/result.rs
+++ b/units/src/amount/result.rs
@@ -2,3 +2,3 @@
 
-//! Provides a monodic result type that is used to return the result of
+//! Provides a monodic numeric result type that is used to return the result of
 //! doing mathematical operations (`core::ops`) on amount types.
@@ -23,3 +23,3 @@ pub enum NumOpResult<T> {
 
-impl<T> NumOpResult<T> {
+impl<T: fmt::Debug> NumOpResult<T> {
     /// Returns the contained valid amount, consuming `self`.
@@ -28,3 +28,17 @@ impl<T> NumOpResult<T> {
     ///
-    /// Panics if the result is an `Error`.
+    /// Panics with `msg` if the numeric result is an `Error`.
+    #[track_caller]
+    pub fn expect(self, msg: &str) -> T {
+        match self {
+            R::Valid(amount) => amount,
+            R::Error(_) => panic!("{}", msg),
+        }
+    }
+
+    /// Returns the contained valid amount, consuming `self`.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the numeric result is an `Error`.
+    #[track_caller]
     pub fn unwrap(self) -> T {
@@ -32,3 +46,3 @@ pub fn unwrap(self) -> T {
             R::Valid(amount) => amount,
-            R::Error(_) => panic!("tried to unwrap an invalid result"),
+            R::Error(e) => panic!("tried to unwrap an invalid numeric result: {:?}", e),
         }
@@ -40,3 +54,4 @@ pub fn unwrap(self) -> T {
     ///
-    /// Panics if the result is a valid amount.
+    /// Panics if the numeric result is a valid amount.
+    #[track_caller]
     pub fn unwrap_err(self) -> NumOpError {
@@ -44,3 +59,3 @@ pub fn unwrap_err(self) -> NumOpError {
             R::Error(e) => e,
-            R::Valid(_) => panic!("tried to unwrap a valid result"),
+            R::Valid(a) => panic!("tried to unwrap a valid numeric result: {:?}", a),
         }
@@ -65,3 +80,3 @@ pub fn into_result(self) -> Result<T, NumOpError> {
 
-    /// Calls `op` if the result is `Valid`, otherwise returns the `Error` value of `self`.
+    /// Calls `op` if the numeric result is `Valid`, otherwise returns the `Error` value of `self`.
     pub fn and_then<F>(self, op: F) -> NumOpResult<T>
@@ -76,3 +91,3 @@ pub fn and_then<F>(self, op: F) -> NumOpResult<T>
 
-    /// Returns `true` if this result is a valid amount.
+    /// Returns `true` if the numeric result is a valid amount.
     pub fn is_valid(&self) -> bool {
@@ -84,3 +99,3 @@ pub fn is_valid(&self) -> bool {
 
-    /// Returns `true` if this result is an invalid amount.
+    /// Returns `true` if the numeric result is an invalid amount.
     pub fn is_error(&self) -> bool { !self.is_valid() }
@@ -105,3 +120,3 @@ impl ops::Add for Amount {
 
-    fn add(self, rhs: Amount) -> Self::Output { self.checked_add(rhs).ok_or_error() }
+    fn add(self, rhs: Amount) -> Self::Output { self.checked_add(rhs).valid_or_error() }
 }
@@ -133,3 +148,3 @@ impl ops::Sub for Amount {
 
-    fn sub(self, rhs: Amount) -> Self::Output { self.checked_sub(rhs).ok_or_error() }
+    fn sub(self, rhs: Amount) -> Self::Output { self.checked_sub(rhs).valid_or_error() }
 }
@@ -181,3 +196,3 @@ impl ops::Mul<u64> for Amount {
 
-    fn mul(self, rhs: u64) -> Self::Output { self.checked_mul(rhs).ok_or_error() }
+    fn mul(self, rhs: u64) -> Self::Output { self.checked_mul(rhs).valid_or_error() }
 }
@@ -202,3 +217,3 @@ impl ops::Div<u64> for Amount {
 
-    fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).ok_or_error() }
+    fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).valid_or_error() }
 }
@@ -223,3 +238,3 @@ impl ops::Rem<u64> for Amount {
 
-    fn rem(self, modulus: u64) -> Self::Output { self.checked_rem(modulus).ok_or_error() }
+    fn rem(self, modulus: u64) -> Self::Output { self.checked_rem(modulus).valid_or_error() }
 }
@@ -244,3 +259,3 @@ impl ops::Add for SignedAmount {
 
-    fn add(self, rhs: SignedAmount) -> Self::Output { self.checked_add(rhs).ok_or_error() }
+    fn add(self, rhs: SignedAmount) -> Self::Output { self.checked_add(rhs).valid_or_error() }
 }
@@ -272,3 +287,3 @@ impl ops::Sub for SignedAmount {
 
-    fn sub(self, rhs: SignedAmount) -> Self::Output { self.checked_sub(rhs).ok_or_error() }
+    fn sub(self, rhs: SignedAmount) -> Self::Output { self.checked_sub(rhs).valid_or_error() }
 }
@@ -320,3 +335,3 @@ impl ops::Mul<i64> for SignedAmount {
 
-    fn mul(self, rhs: i64) -> Self::Output { self.checked_mul(rhs).ok_or_error() }
+    fn mul(self, rhs: i64) -> Self::Output { self.checked_mul(rhs).valid_or_error() }
 }
@@ -341,3 +356,3 @@ impl ops::Div<i64> for SignedAmount {
 
-    fn div(self, rhs: i64) -> Self::Output { self.checked_div(rhs).ok_or_error() }
+    fn div(self, rhs: i64) -> Self::Output { self.checked_div(rhs).valid_or_error() }
 }
@@ -362,3 +377,3 @@ impl ops::Rem<i64> for SignedAmount {
 
-    fn rem(self, modulus: i64) -> Self::Output { self.checked_rem(modulus).ok_or_error() }
+    fn rem(self, modulus: i64) -> Self::Output { self.checked_rem(modulus).valid_or_error() }
 }
@@ -539,3 +554,3 @@ fn sum<I>(iter: I) -> Self
 pub(in crate::amount) trait OptionExt<T> {
-    fn ok_or_error(self) -> NumOpResult<T>;
+    fn valid_or_error(self) -> NumOpResult<T>;
 }
@@ -543,3 +558,3 @@ pub(in crate::amount) trait OptionExt<T> {
 impl OptionExt<Amount> for Option<Amount> {
-    fn ok_or_error(self) -> NumOpResult<Amount> {
+    fn valid_or_error(self) -> NumOpResult<Amount> {
         match self {
@@ -552,3 +567,3 @@ fn ok_or_error(self) -> NumOpResult<Amount> {
 impl OptionExt<SignedAmount> for Option<SignedAmount> {
-    fn ok_or_error(self) -> NumOpResult<SignedAmount> {
+    fn valid_or_error(self) -> NumOpResult<SignedAmount> {
         match self {
@@ -567,3 +582,3 @@ impl fmt::Display for NumOpError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "a math operation on amounts gave an invalid result")
+        write!(f, "a math operation on amounts gave an invalid numeric result")
     }
``` 

</details>


Patch:
diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml
index 6477f498..b6ed18c5 100644
--- a/.cargo/mutants.toml
+++ b/.cargo/mutants.toml
@@ -1,5 +1,5 @@
 additional_cargo_args = ["--all-features"]
-examine_globs = ["units/src/**/*.rs", "primitives/src/**/*.rs"]
+examine_globs = ["units/src/**/*.rs"]
 exclude_globs = [
     "units/src/amount/verification.rs" # kani tests
 ]
@@ -8,25 +8,20 @@ exclude_re = [
     "impl Arbitrary",
     "impl Display",
     ".*Error",
-    "deserialize", # Skip serde mutation tests
-    "Iterator", # Mutating operations in an iterator can result in an infinite loop
-
-    # ----------------------------------Crate-specific exclusions----------------------------------
+    # --------------------------------------------Crate-specific exclusions--------------------------------------------
     # Units
     # src/amount/mod.rs
     "parse_signed_to_satoshi", # Can't kill all mutants since there is no denomination smaller than Satoshi
     "fmt_satoshi_in", # Related to formatting/display
     "dec_width", # Replacing num /= 10 with num %=10 in a loop causes a timeout due to infinite loop
+    # src/fee_rate/serde.rs
+    "as_sat_per_kwu::opt::deserialize::<impl Visitor for VisitOpt>.*", # Replaces return value with Ok(Default::default()), which is the same as Ok(None)
+    "as_sat_per_vb_floor::opt::deserialize::<impl Visitor for VisitOpt>.*", # Replaces return value with Ok(Default::default()), which is the same as Ok(None)
+    "as_sat_per_vb_ceil::opt::deserialize::<impl Visitor for VisitOpt>.*", # Replaces return value with Ok(Default::default()), which is the same as Ok(None)
+    # src/amount/serde.rs
+    "as_sat::opt::deserialize::<impl Visitor for VisitOptAmt<X>>.*", # Replaces return value with Ok(Default::default()), which is the same as Ok(None)
+    "as_btc::opt::deserialize::<impl Visitor for VisitOptAmt<X>>.*", # Replaces return value with Ok(Default::default()), which is the same as Ok(None)
+    "as_str::opt::deserialize::<impl Visitor for VisitOptAmt<X>>.*", # Replaces return value with Ok(Default::default()), which is the same as Ok(None)
     # src/locktime/relative.rs
-    "Time::to_consensus_u32", # Mutant from replacing | with ^, this returns the same value since the XOR is taken against the u16 with an all-zero bitmask
-
-    # primitives
-    "Sequence::from_512_second_intervals", # Mutant from replacing | with ^, this returns the same value since the XOR is taken against the u16 with an all-zero bitmask
-    "Opcode::classify", # Not possible to kill all mutants without individually checking every opcode classification
-    "Block<Checked>::cached_witness_root", # Skip getters
-    "Block<Checked>::transactions", # Skip getters
-    "Script::to_bytes", # Deprecated
-    "decode_cursor", # Mutating operations in decode_cursor can result in an infinite loop
-    "fmt_debug", # Mutants from formatting/display changes
-    "fmt_debug_pretty", # Mutants from formatting/display changes
+    "Time::to_consensus_u32" # It will replace | with ^, which will return the same value since the XOR is always taken against the u16 and an all-zero bitmask
 ]
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/examples/taproot-psbt.rs b/bitcoin/examples/taproot-psbt.rs
index 1d712ed1..c312cf3d 100644
--- a/bitcoin/examples/taproot-psbt.rs
+++ b/bitcoin/examples/taproot-psbt.rs
@@ -412,7 +412,8 @@ fn create_inheritance_funding_tx(
             taproot_spend_info.internal_key(),
             taproot_spend_info.merkle_root(),
         );
-        let value = input_utxo.amount - ABSOLUTE_FEES;
+        let value = (input_utxo.amount - ABSOLUTE_FEES)
+            .expect("ABSOLUTE_FEES must be set below input amount");
 
         // Spend a normal BIP86-like output as an input in our inheritance funding transaction
         let tx = generate_bip86_key_spend_tx(
@@ -476,7 +477,7 @@ fn refresh_tx(
             let mut psbt = self.next_psbt.clone().expect("should have next_psbt");
             let input = &mut psbt.inputs[0];
             let input_value = input.witness_utxo.as_ref().unwrap().value;
-            let output_value = input_value - ABSOLUTE_FEES;
+            let output_value = (input_value - ABSOLUTE_FEES).into_result()?;
 
             // We use some other derivation path in this example for our inheritance protocol. The important thing is to ensure
             // that we use an unhardened path so we can make use of xpubs.
@@ -649,7 +650,8 @@ fn spend_inheritance(
         psbt.unsigned_tx.lock_time = lock_time;
         psbt.unsigned_tx.output = vec![TxOut {
             script_pubkey: to_address.script_pubkey(),
-            value: input_value - ABSOLUTE_FEES,
+            value: (input_value - ABSOLUTE_FEES)
+                .expect("ABSOLUTE_FEES must be set below input amount"),
         }];
         psbt.outputs = vec![Output::default()];
         let unsigned_tx = psbt.unsigned_tx.clone();
diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index 0a99677e..56094aa7 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 }
@@ -790,7 +790,7 @@ pub fn effective_value(
     value: Amount,
 ) -> Option<SignedAmount> {
     let weight = satisfaction_weight.checked_add(TX_IN_BASE_WEIGHT)?;
-    let signed_input_fee = fee_rate.to_fee(weight)?.to_signed();
+    let signed_input_fee = fee_rate.checked_mul_by_weight(weight)?.to_signed();
     value.to_signed().checked_sub(signed_input_fee)
 }
 
@@ -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]
@@ -1667,7 +1673,7 @@ fn effective_value_happy_path() {
 
         // 10 sat/kwu * (204wu + BASE_WEIGHT) = 4 sats
         let expected_fee = "4 sats".parse::<SignedAmount>().unwrap();
-        let expected_effective_value = value.to_signed() - expected_fee;
+        let expected_effective_value = (value.to_signed() - expected_fee).unwrap();
         assert_eq!(effective_value, expected_effective_value);
     }
 
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index 96e36786..2471fc41 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -2249,7 +2249,7 @@ fn fee() {
         };
         assert_eq!(
             t.fee().expect("fee calculation"),
-            prev_output_val - (output_0_val + output_1_val)
+            (prev_output_val - (output_0_val + output_1_val)).unwrap()
         );
         // no previous output
         let mut t2 = t.clone();
diff --git a/bitcoin/src/taproot/mod.rs b/bitcoin/src/taproot/mod.rs
index 775357f0..50ea6c25 100644
--- a/bitcoin/src/taproot/mod.rs
+++ b/bitcoin/src/taproot/mod.rs
@@ -1564,6 +1564,7 @@ impl Sealed for super::TapNodeHash {}
 #[cfg(test)]
 mod test {
     use hashes::sha256;
+    use hashes::sha256t::Tag;
     use hex::{DisplayHex, FromHex};
     use secp256k1::VerifyOnly;
 
@@ -1589,18 +1590,18 @@ fn tag_engine(tag_name: &str) -> sha256::HashEngine {
 
     #[test]
     fn midstates() {
-        use sha256t::{Hash, Tag};
+        use sha256t::Hash;
         // test that engine creation roundtrips
-        assert_eq!(tag_engine("TapLeaf").midstate().unwrap(), TapLeafTag::MIDSTATE);
-        assert_eq!(tag_engine("TapBranch").midstate().unwrap(), TapBranchTag::MIDSTATE);
-        assert_eq!(tag_engine("TapTweak").midstate().unwrap(), TapTweakTag::MIDSTATE);
-        assert_eq!(tag_engine("TapSighash").midstate().unwrap(), TapSighashTag::MIDSTATE);
+        assert_eq!(tag_engine("TapLeaf").midstate(), TapLeafTag::engine().midstate());
+        assert_eq!(tag_engine("TapBranch").midstate(), TapBranchTag::engine().midstate());
+        assert_eq!(tag_engine("TapTweak").midstate(), TapTweakTag::engine().midstate());
+        assert_eq!(tag_engine("TapSighash").midstate(), TapSighashTag::engine().midstate());
 
         // check that hash creation is the same as building into the same engine
         fn empty_hash(tag_name: &str) -> [u8; 32] {
             let mut e = tag_engine(tag_name);
             e.input(&[]);
-            sha256::Hash::from_engine(e).to_byte_array()
+            Hash::<TapBranchTag>::from_engine(e).to_byte_array()
         }
         assert_eq!(empty_hash("TapLeaf"), Hash::<TapLeafTag>::hash(&[]).to_byte_array());
         assert_eq!(empty_hash("TapBranch"), Hash::<TapBranchTag>::hash(&[]).to_byte_array());
@@ -1617,19 +1618,19 @@ fn vectors_core() {
         //   CHashWriter writer = HasherTapLeaf;
         //   writer.GetSHA256().GetHex()
         assert_eq!(
-            Hash::<TapLeafTag>::from_engine(Hash::<TapLeafTag>::engine()).to_string(),
+            Hash::<TapLeafTag>::from_engine(TapLeafTag::engine()).to_string(),
             "5212c288a377d1f8164962a5a13429f9ba6a7b84e59776a52c6637df2106facb"
         );
         assert_eq!(
-            Hash::<TapBranchTag>::from_engine(Hash::<TapBranchTag>::engine()).to_string(),
+            Hash::<TapBranchTag>::from_engine(TapBranchTag::engine()).to_string(),
             "53c373ec4d6f3c53c1f5fb2ff506dcefe1a0ed74874f93fa93c8214cbe9ffddf"
         );
         assert_eq!(
-            Hash::<TapTweakTag>::from_engine(Hash::<TapTweakTag>::engine()).to_string(),
+            Hash::<TapTweakTag>::from_engine(TapTweakTag::engine()).to_string(),
             "8aa4229474ab0100b2d6f0687f031d1fc9d8eef92a042ad97d279bff456b15e4"
         );
         assert_eq!(
-            Hash::<TapSighashTag>::from_engine(Hash::<TapSighashTag>::engine()).to_string(),
+            Hash::<TapSighashTag>::from_engine(TapSighashTag::engine()).to_string(),
             "dabc11914abcd8072900042a2681e52f8dba99ce82e224f97b5fdb7cd4b9c803"
         );
 
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/hash160/mod.rs b/hashes/src/hash160/mod.rs
index fd2e366c..b984960c 100644
--- a/hashes/src/hash160/mod.rs
+++ b/hashes/src/hash160/mod.rs
@@ -25,7 +25,7 @@ fn from_engine(e: HashEngine) -> Hash {
 }
 
 /// Engine to compute HASH160 hash function.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct HashEngine(sha256::HashEngine);
 
 impl HashEngine {
diff --git a/hashes/src/hmac/mod.rs b/hashes/src/hmac/mod.rs
index eace7f95..a661f64e 100644
--- a/hashes/src/hmac/mod.rs
+++ b/hashes/src/hmac/mod.rs
@@ -35,7 +35,7 @@ fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(Hmac(str::FromStr::from_str
 }
 
 /// Pair of underlying hash engines, used for the inner and outer hash of HMAC.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct HmacEngine<T: GeneralHash> {
     iengine: T::Engine,
     oengine: T::Engine,
@@ -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..959ccdb4 100644
--- a/hashes/src/internal_macros.rs
+++ b/hashes/src/internal_macros.rs
@@ -27,9 +27,9 @@ macro_rules! hash_trait_impls {
         $crate::impl_debug_only!(Hash, { $bits / 8 }, $reverse $(, $gen: $gent)*);
 
         impl<$($gen: $gent),*> $crate::GeneralHash for Hash<$($gen),*> {
-            type Engine = HashEngine<$($gen),*>;
+            type Engine = HashEngine;
 
-            fn from_engine(e: Self::Engine) -> Hash<$($gen),*> { Self::from_engine(e) }
+            fn from_engine(e: HashEngine) -> Hash<$($gen),*> { Self::from_engine(e) }
         }
 
         #[cfg(feature = "serde")]
@@ -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/macros.rs b/hashes/src/macros.rs
index 12ca0152..dc134577 100644
--- a/hashes/src/macros.rs
+++ b/hashes/src/macros.rs
@@ -33,7 +33,11 @@ macro_rules! sha256t_tag {
         $crate::sha256t_tag_struct!($tag_vis, $tag, stringify!($hash_name), $(#[$($tag_attr)*])*);
 
         impl $crate::sha256t::Tag for $tag {
-            const MIDSTATE: $crate::sha256::Midstate = $crate::sha256t_tag_constructor!($constructor, $($tag_value)+);
+            #[inline]
+            fn engine() -> $crate::sha256::HashEngine {
+                const MIDSTATE: $crate::sha256::Midstate = $crate::sha256t_tag_constructor!($constructor, $($tag_value)+);
+                $crate::sha256::HashEngine::from_midstate(MIDSTATE)
+            }
         }
     }
 }
diff --git a/hashes/src/ripemd160/mod.rs b/hashes/src/ripemd160/mod.rs
index e6be1753..759900ce 100644
--- a/hashes/src/ripemd160/mod.rs
+++ b/hashes/src/ripemd160/mod.rs
@@ -48,7 +48,7 @@ fn from_engine(e: HashEngine) -> Hash {
 const BLOCK_SIZE: usize = 64;
 
 /// Engine to compute RIPEMD160 hash function.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct HashEngine {
     buffer: [u8; BLOCK_SIZE],
     h: [u32; 5],
diff --git a/hashes/src/sha1/mod.rs b/hashes/src/sha1/mod.rs
index dc32d31d..6c198d18 100644
--- a/hashes/src/sha1/mod.rs
+++ b/hashes/src/sha1/mod.rs
@@ -40,7 +40,7 @@ fn from_engine(mut e: HashEngine) -> Hash {
 const BLOCK_SIZE: usize = 64;
 
 /// Engine to compute SHA1 hash function.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct HashEngine {
     buffer: [u8; BLOCK_SIZE],
     h: [u32; 5],
diff --git a/hashes/src/sha256/mod.rs b/hashes/src/sha256/mod.rs
index 6ae83b87..70bf9121 100644
--- a/hashes/src/sha256/mod.rs
+++ b/hashes/src/sha256/mod.rs
@@ -54,7 +54,7 @@ fn from_engine(e: HashEngine) -> Hash {
 const BLOCK_SIZE: usize = 64;
 
 /// Engine to compute SHA256 hash function.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct HashEngine {
     buffer: [u8; BLOCK_SIZE],
     h: [u32; 8],
diff --git a/hashes/src/sha256d/mod.rs b/hashes/src/sha256d/mod.rs
index 003801d9..165bee52 100644
--- a/hashes/src/sha256d/mod.rs
+++ b/hashes/src/sha256d/mod.rs
@@ -20,7 +20,7 @@ fn from_engine(e: HashEngine) -> Hash {
 }
 
 /// Engine to compute SHA256d hash function.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct HashEngine(sha256::HashEngine);
 
 impl HashEngine {
diff --git a/hashes/src/sha256t/mod.rs b/hashes/src/sha256t/mod.rs
index e2461eeb..6c62846c 100644
--- a/hashes/src/sha256t/mod.rs
+++ b/hashes/src/sha256t/mod.rs
@@ -5,14 +5,14 @@
 use core::cmp;
 use core::marker::PhantomData;
 
-#[cfg(doc)]
-use crate::sha256::Midstate;
 use crate::{sha256, FromSliceError, HashEngine as _};
 
+type HashEngine = sha256::HashEngine;
+
 /// Trait representing a tag that can be used as a context for SHA256t hashes.
-pub trait Tag: Clone {
-    /// The [`Midstate`] after pre-tagging the hash engine.
-    const MIDSTATE: sha256::Midstate;
+pub trait Tag {
+    /// Returns a hash engine that is pre-tagged and is ready to be used for the data.
+    fn engine() -> sha256::HashEngine;
 }
 
 /// Output of the SHA256t hash function.
@@ -57,12 +57,10 @@ pub fn from_slice(sl: &[u8]) -> Result<Hash<T>, FromSliceError> {
     }
 
     /// Produces a hash from the current state of a given engine.
-    pub fn from_engine(e: HashEngine<T>) -> Hash<T> {
-        Hash::from_byte_array(sha256::Hash::from_engine(e.0).to_byte_array())
-    }
+    pub fn from_engine(e: HashEngine) -> Hash<T> { from_engine(e) }
 
     /// Constructs a new engine.
-    pub fn engine() -> HashEngine<T> { HashEngine::default() }
+    pub fn engine() -> HashEngine { T::engine() }
 
     /// Hashes some bytes.
     #[allow(clippy::self_named_constructors)] // Hash is a noun and a verb.
@@ -87,6 +85,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 }
 
@@ -116,32 +133,111 @@ fn hash<H: core::hash::Hasher>(&self, h: &mut H) { self.0.hash(h) }
 
 crate::internal_macros::hash_trait_impls!(256, false, T: Tag);
 
-/// Engine to compute SHA256t hash function.
-#[derive(Debug, Clone)]
-pub struct HashEngine<T>(sha256::HashEngine, PhantomData<T>);
-
-impl<T: Tag> Default for HashEngine<T> {
-    fn default() -> Self {
-        let tagged = sha256::HashEngine::from_midstate(T::MIDSTATE);
-        HashEngine(tagged, PhantomData)
-    }
+fn from_engine<T>(e: sha256::HashEngine) -> Hash<T>
+where
+    T: Tag,
+{
+    Hash::from_byte_array(sha256::Hash::from_engine(e).to_byte_array())
 }
 
-impl<T: Tag> crate::HashEngine for HashEngine<T> {
-    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() }
-}
+/// Macro used to define a newtype tagged hash.
+///
+/// This macro creates two types:
+///
+/// * a tag struct
+/// * a hash wrapper
+///
+/// The syntax is:
+///
+/// ```
+/// # #[allow(deprecated)] {
+/// # use bitcoin_hashes::sha256t_hash_newtype;
+/// sha256t_hash_newtype! {
+///     /// Optional documentation details here.
+///     /// Summary is always generated.
+///     pub struct FooTag = hash_str("foo");
+///
+///     /// A foo hash.
+///     // Direction works just like the hash_newtype! macro.
+///     #[hash_newtype(backward)]
+///     pub struct FooHash(_);
+/// }
+/// # }
+/// ```
+///
+/// The structs must be defined in this order - tag first, then hash type. `hash_str` marker
+/// says the midstate should be generated by hashing the supplied string in a way described in
+/// BIP-341. Alternatively, you can supply `hash_bytes` to hash raw bytes. If you have the midstate
+/// already pre-computed and prefer **compiler** performance to readability you may use
+/// `raw(MIDSTATE_BYTES, HASHED_BYTES_LENGTH)` instead.
+///
+/// Both visibility modifiers and attributes are optional and passed to inner structs (excluding
+/// `#[hash_newtype(...)]`). The attributes suffer same compiler performance limitations as in
+/// [`hash_newtype`] macro.
+///
+/// [`hash_newtype`]: crate::hash_newtype
+#[macro_export]
+#[deprecated(since = "0.15.0", note = "use `sha256_tag!` combined with `hash_newtype!` instead")]
+macro_rules! sha256t_hash_newtype {
+    ($(#[$($tag_attr:tt)*])* $tag_vis:vis struct $tag:ident = $constructor:tt($($tag_value:tt)+); $(#[$($hash_attr:tt)*])* $hash_vis:vis struct $hash_name:ident($(#[$($field_attr:tt)*])* _);) => {
+        $crate::sha256t_tag_struct!($tag_vis, $tag, stringify!($hash_name), $(#[$($tag_attr)*])*);
+
+        impl $crate::sha256t::Tag for $tag {
+            #[inline]
+            fn engine() -> $crate::sha256::HashEngine {
+                const MIDSTATE: $crate::sha256::Midstate = $crate::sha256t_tag_constructor!($constructor, $($tag_value)+);
+                $crate::sha256::HashEngine::from_midstate(MIDSTATE)
+            }
+        }
+
+        $crate::hash_newtype! {
+            $(#[$($hash_attr)*])*
+            $hash_vis struct $hash_name($(#[$($field_attr)*])* $crate::sha256t::Hash<$tag>);
+        }
+
+        impl $hash_name {
+            /// Constructs a new engine.
+            #[allow(unused)] // the user of macro may not need this
+            pub fn engine() -> <$hash_name as $crate::GeneralHash>::Engine {
+                <$hash_name as $crate::GeneralHash>::engine()
+            }
+
+            /// Produces a hash from the current state of a given engine.
+            #[allow(unused)] // the user of macro may not need this
+            pub fn from_engine(e: <$hash_name as $crate::GeneralHash>::Engine) -> Self {
+                <$hash_name as $crate::GeneralHash>::from_engine(e)
+            }
+
+            /// Hashes some bytes.
+            #[allow(unused)] // the user of macro may not need this
+            pub fn hash(data: &[u8]) -> Self {
+                <$hash_name as $crate::GeneralHash>::hash(data)
+            }
+
+            /// Hashes all the byte slices retrieved from the iterator together.
+            #[allow(unused)] // the user of macro may not need this
+            pub fn hash_byte_chunks<B, I>(byte_slices: I) -> Self
+            where
+                B: AsRef<[u8]>,
+                I: IntoIterator<Item = B>,
+            {
+                <$hash_name as $crate::GeneralHash>::hash_byte_chunks(byte_slices)
+            }
+        }
 
-crate::internal_macros::impl_write!(
-    HashEngine<T>,
-    |us: &mut HashEngine<T>, buf| {
-        us.input(buf);
-        Ok(buf.len())
-    },
-    |_us| { Ok(()) },
-    T: crate::sha256t::Tag
-);
+        impl $crate::GeneralHash for $hash_name {
+            type Engine = <$crate::sha256t::Hash<$tag> as $crate::GeneralHash>::Engine;
+
+            fn engine() -> Self::Engine {
+                <$crate::sha256t::Hash<$tag> as $crate::GeneralHash>::engine()
+            }
+
+            fn from_engine(e: Self::Engine) -> $hash_name {
+                Self::from(<$crate::sha256t::Hash<$tag> as $crate::GeneralHash>::from_engine(e))
+            }
+        }
+    }
+}
 
 // Workaround macros being unavailable in attributes.
 #[doc(hidden)]
@@ -193,7 +289,11 @@ mod tests {
     pub struct TestHashTag;
 
     impl sha256t::Tag for TestHashTag {
-        const MIDSTATE: sha256::Midstate = sha256::Midstate::new(TEST_MIDSTATE, 64);
+        fn engine() -> sha256::HashEngine {
+            // The TapRoot TapLeaf midstate.
+            let midstate = sha256::Midstate::new(TEST_MIDSTATE, 64);
+            sha256::HashEngine::from_midstate(midstate)
+        }
     }
 
     // We support manually implementing `Tag` and creating a tagged hash from it.
diff --git a/hashes/src/sha384/mod.rs b/hashes/src/sha384/mod.rs
index 200bf9f6..18f23b40 100644
--- a/hashes/src/sha384/mod.rs
+++ b/hashes/src/sha384/mod.rs
@@ -17,7 +17,7 @@ fn from_engine(e: HashEngine) -> Hash {
 }
 
 /// Engine to compute SHA384 hash function.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct HashEngine(sha512::HashEngine);
 
 impl HashEngine {
diff --git a/hashes/src/sha512/mod.rs b/hashes/src/sha512/mod.rs
index 65bf75c1..f72f62e5 100644
--- a/hashes/src/sha512/mod.rs
+++ b/hashes/src/sha512/mod.rs
@@ -49,7 +49,7 @@ pub(crate) fn from_engine(e: HashEngine) -> Hash {
 pub(crate) const BLOCK_SIZE: usize = 128;
 
 /// Engine to compute SHA512 hash function.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct HashEngine {
     h: [u64; 8],
     bytes_hashed: u64,
diff --git a/hashes/src/sha512_256/mod.rs b/hashes/src/sha512_256/mod.rs
index 3452508f..cb1d5d37 100644
--- a/hashes/src/sha512_256/mod.rs
+++ b/hashes/src/sha512_256/mod.rs
@@ -27,7 +27,7 @@ fn from_engine(e: HashEngine) -> Hash {
 /// the output to 256 bits. It has different initial constants than sha512 so it
 /// produces an entirely different hash compared to sha512. More information at
 /// <https://eprint.iacr.org/2010/548.pdf>.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
 pub struct HashEngine(sha512::HashEngine);
 
 impl HashEngine {
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/nightly-version b/nightly-version
index 36da8033..609e36af 100644
--- a/nightly-version
+++ b/nightly-version
@@ -1,1 +1,1 @@
-nightly-2025-02-07
+nightly-2025-01-31
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/block.rs b/primitives/src/block.rs
index b30a2c6c..cfc322c1 100644
--- a/primitives/src/block.rs
+++ b/primitives/src/block.rs
@@ -365,8 +365,6 @@ fn version_is_not_signalling_when_use_version_bit_not_set() {
     fn version_is_signalling() {
         let version = Version::from_consensus(0b00100000000000000000000000000010);
         assert!(Version::is_signalling_soft_fork(&version, 1));
-        let version = Version::from_consensus(0b00110000000000000000000000000000);
-        assert!(Version::is_signalling_soft_fork(&version, 28));
     }
 
     #[test]
@@ -374,34 +372,4 @@ fn version_is_not_signalling() {
         let version = Version::from_consensus(0b00100000000000000000000000000010);
         assert!(!Version::is_signalling_soft_fork(&version, 0));
     }
-
-    #[test]
-    fn version_to_consensus() {
-        let version = Version::from_consensus(1234567890);
-        assert_eq!(version.to_consensus(), 1234567890);
-    }
-
-    // Check that the size of the header consensus serialization matches the const SIZE value
-    #[test]
-    fn header_size() {
-        let header = Header {
-            version: Version::ONE,
-            prev_blockhash: BlockHash::from_byte_array([0x99; 32]),
-            merkle_root: TxMerkleNode::from_byte_array([0x77; 32]),
-            time: 2,
-            bits: CompactTarget::from_consensus(3),
-            nonce: 4,
-        };
-
-        // Calculate the size of the block header in bytes from the sum of the serialized lengths
-        // it's fields: version, prev_blockhash, merkle_root, time, bits, nonce.
-        let header_size = header.version.to_consensus().to_le_bytes().len()
-            + header.prev_blockhash.as_byte_array().len()
-            + header.merkle_root.as_byte_array().len()
-            + header.time.to_le_bytes().len()
-            + header.bits.to_consensus().to_le_bytes().len()
-            + header.nonce.to_le_bytes().len();
-
-        assert_eq!(header_size, Header::SIZE);
-    }
 }
diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs
index 5cd71506..a25d422e 100644
--- a/primitives/src/lib.rs
+++ b/primitives/src/lib.rs
@@ -68,7 +68,6 @@ pub mod _core {
     block::{
         Block, Checked as BlockChecked, Unchecked as BlockUnchecked, Validation as BlockValidation,
     },
-    script::{Script, ScriptBuf},
     transaction::{Transaction, TxIn, TxOut},
     witness::Witness,
 };
@@ -77,11 +76,10 @@ pub mod _core {
     block::{BlockHash, Header as BlockHeader, WitnessCommitment},
     locktime::{absolute, relative},
     merkle_tree::{TxMerkleNode, WitnessMerkleNode},
-    opcodes::Opcode,
     pow::CompactTarget,
     sequence::Sequence,
     taproot::{TapBranchTag, TapLeafHash, TapLeafTag, TapNodeHash, TapTweakHash, TapTweakTag},
-    transaction::{OutPoint, Txid, Wtxid},
+    transaction::{Txid, Wtxid},
 };
 
 #[rustfmt::skip]
diff --git a/primitives/src/locktime/absolute.rs b/primitives/src/locktime/absolute.rs
index ae924e26..976f5614 100644
--- a/primitives/src/locktime/absolute.rs
+++ b/primitives/src/locktime/absolute.rs
@@ -42,9 +42,9 @@
 /// # Examples
 ///
 /// ```
-/// # use bitcoin_primitives::absolute::{self, LockTime::*};
-/// # let n = absolute::LockTime::from_consensus(741521);          // n OP_CHECKLOCKTIMEVERIFY
-/// # let lock_time = absolute::LockTime::from_consensus(741521);  // nLockTime
+/// # use bitcoin_primitives::absolute::{LockTime, LockTime::*};
+/// # let n = LockTime::from_consensus(741521);          // n OP_CHECKLOCKTIMEVERIFY
+/// # let lock_time = LockTime::from_consensus(741521);  // nLockTime
 /// // To compare absolute lock times there are various `is_satisfied_*` methods, you may also use:
 /// let _is_satisfied = match (n, lock_time) {
 ///     (Blocks(n), Blocks(lock_time)) => n <= lock_time,
@@ -59,10 +59,10 @@ pub enum LockTime {
     /// # Examples
     ///
     /// ```rust
-    /// use bitcoin_primitives::absolute;
+    /// use bitcoin_primitives::absolute::LockTime;
     ///
     /// let block: u32 = 741521;
-    /// let n = absolute::LockTime::from_height(block).expect("valid height");
+    /// let n = LockTime::from_height(block).expect("valid height");
     /// assert!(n.is_block_height());
     /// assert_eq!(n.to_consensus_u32(), block);
     /// ```
@@ -72,10 +72,10 @@ pub enum LockTime {
     /// # Examples
     ///
     /// ```rust
-    /// use bitcoin_primitives::absolute;
+    /// use bitcoin_primitives::absolute::LockTime;
     ///
     /// let seconds: u32 = 1653195600; // May 22nd, 5am UTC.
-    /// let n = absolute::LockTime::from_time(seconds).expect("valid time");
+    /// let n = LockTime::from_time(seconds).expect("valid time");
     /// assert!(n.is_block_time());
     /// assert_eq!(n.to_consensus_u32(), seconds);
     /// ```
@@ -95,9 +95,9 @@ impl LockTime {
     /// # Examples
     ///
     /// ```
-    /// # use bitcoin_primitives::absolute;
+    /// # use bitcoin_primitives::absolute::LockTime;
     /// let hex_str = "0x61cf9980"; // Unix timestamp for January 1, 2022
-    /// let lock_time = absolute::LockTime::from_hex(hex_str)?;
+    /// let lock_time = LockTime::from_hex(hex_str)?;
     /// assert_eq!(lock_time.to_consensus_u32(), 0x61cf9980);
     ///
     /// # Ok::<_, units::parse::PrefixedHexError>(())
@@ -112,9 +112,9 @@ pub fn from_hex(s: &str) -> Result<Self, PrefixedHexError> {
     /// # Examples
     ///
     /// ```
-    /// # use bitcoin_primitives::absolute;
+    /// # use bitcoin_primitives::absolute::LockTime;
     /// let hex_str = "61cf9980"; // Unix timestamp for January 1, 2022
-    /// let lock_time = absolute::LockTime::from_unprefixed_hex(hex_str)?;
+    /// let lock_time = LockTime::from_unprefixed_hex(hex_str)?;
     /// assert_eq!(lock_time.to_consensus_u32(), 0x61cf9980);
     ///
     /// # Ok::<_, units::parse::UnprefixedHexError>(())
@@ -129,11 +129,11 @@ pub fn from_unprefixed_hex(s: &str) -> Result<Self, UnprefixedHexError> {
     /// # Examples
     ///
     /// ```rust
-    /// # use bitcoin_primitives::absolute;
+    /// # use bitcoin_primitives::absolute::LockTime;
     ///
     /// // `from_consensus` roundtrips as expected with `to_consensus_u32`.
     /// let n_lock_time: u32 = 741521;
-    /// let lock_time = absolute::LockTime::from_consensus(n_lock_time);
+    /// let lock_time = LockTime::from_consensus(n_lock_time);
     /// assert_eq!(lock_time.to_consensus_u32(), n_lock_time);
     #[inline]
     pub fn from_consensus(n: u32) -> Self {
@@ -157,9 +157,9 @@ pub fn from_consensus(n: u32) -> Self {
     /// # Examples
     ///
     /// ```rust
-    /// # use bitcoin_primitives::absolute;
-    /// assert!(absolute::LockTime::from_height(741521).is_ok());
-    /// assert!(absolute::LockTime::from_height(1653195600).is_err());
+    /// # use bitcoin_primitives::absolute::LockTime;
+    /// assert!(LockTime::from_height(741521).is_ok());
+    /// assert!(LockTime::from_height(1653195600).is_err());
     /// ```
     #[inline]
     pub fn from_height(n: u32) -> Result<Self, ConversionError> {
@@ -183,9 +183,9 @@ pub fn from_height(n: u32) -> Result<Self, ConversionError> {
     /// # Examples
     ///
     /// ```rust
-    /// # use bitcoin_primitives::absolute;
-    /// assert!(absolute::LockTime::from_time(1653195600).is_ok());
-    /// assert!(absolute::LockTime::from_time(741521).is_err());
+    /// # use bitcoin_primitives::absolute::LockTime;
+    /// assert!(LockTime::from_time(1653195600).is_ok());
+    /// assert!(LockTime::from_time(741521).is_err());
     /// ```
     #[inline]
     pub fn from_time(n: u32) -> Result<Self, ConversionError> {
@@ -223,12 +223,12 @@ pub const fn is_block_time(&self) -> bool { !self.is_block_height() }
     /// # Examples
     ///
     /// ```no_run
-    /// # use bitcoin_primitives::absolute;
+    /// # use bitcoin_primitives::absolute::{LockTime, Height, Time};
     /// // Can be implemented if block chain data is available.
-    /// fn get_height() -> absolute::Height { todo!("return the current block height") }
-    /// fn get_time() -> absolute::Time { todo!("return the current block time") }
+    /// fn get_height() -> Height { todo!("return the current block height") }
+    /// fn get_time() -> Time { todo!("return the current block time") }
     ///
-    /// let n = absolute::LockTime::from_consensus(741521); // `n OP_CHEKCLOCKTIMEVERIFY`.
+    /// let n = LockTime::from_consensus(741521); // `n OP_CHEKCLOCKTIMEVERIFY`.
     /// if n.is_satisfied_by(get_height(), get_time()) {
     ///     // Can create and mine a transaction that satisfies the OP_CLTV timelock constraint.
     /// }
@@ -257,9 +257,9 @@ pub fn is_satisfied_by(&self, height: Height, time: Time) -> bool {
     /// # Examples
     ///
     /// ```rust
-    /// # use bitcoin_primitives::absolute;
-    /// let lock_time = absolute::LockTime::from_consensus(741521);
-    /// let check = absolute::LockTime::from_consensus(741521 + 1);
+    /// # use bitcoin_primitives::absolute::LockTime;
+    /// let lock_time = LockTime::from_consensus(741521);
+    /// let check = LockTime::from_consensus(741521 + 1);
     /// assert!(lock_time.is_implied_by(check));
     /// ```
     #[inline]
@@ -285,9 +285,9 @@ pub fn is_implied_by(&self, other: LockTime) -> bool {
     /// # Examples
     ///
     /// ```rust
-    /// # use bitcoin_primitives::absolute::{self, LockTime::*};
-    /// # let n = absolute::LockTime::from_consensus(741521);  // n OP_CHECKLOCKTIMEVERIFY
-    /// # let lock_time = absolute::LockTime::from_consensus(741521 + 1);  // nLockTime
+    /// # use bitcoin_primitives::absolute::{LockTime, LockTime::*};
+    /// # let n = LockTime::from_consensus(741521);              // n OP_CHECKLOCKTIMEVERIFY
+    /// # let lock_time = LockTime::from_consensus(741521 + 1);  // nLockTime
     ///
     /// let _is_satisfied = match (n, lock_time) {
     ///     (Blocks(n), Blocks(lock_time)) => n <= lock_time,
diff --git a/primitives/src/locktime/relative.rs b/primitives/src/locktime/relative.rs
index 0874225b..85aeeaea 100644
--- a/primitives/src/locktime/relative.rs
+++ b/primitives/src/locktime/relative.rs
@@ -36,16 +36,16 @@
 /// # Examples
 ///
 /// ```
-/// use bitcoin_primitives::relative;
-/// let lock_by_height = relative::LockTime::from_height(144); // 144 blocks, approx 24h.
+/// use bitcoin_primitives::relative::{LockTime, Height, Time};
+/// let lock_by_height = LockTime::from_height(144); // 144 blocks, approx 24h.
 /// assert!(lock_by_height.is_block_height());
 ///
-/// let lock_by_time = relative::LockTime::from_512_second_intervals(168); // 168 time intervals, approx 24h.
+/// let lock_by_time = LockTime::from_512_second_intervals(168); // 168 time intervals, approx 24h.
 /// assert!(lock_by_time.is_block_time());
 ///
 /// // Check if a lock time is satisfied by a given height or time.
-/// let height = relative::Height::from(150);
-/// let time = relative::Time::from_512_second_intervals(200);
+/// let height = Height::from(150);
+/// let time = Time::from_512_second_intervals(200);
 /// assert!(lock_by_height.is_satisfied_by(height, time));
 /// assert!(lock_by_time.is_satisfied_by(height, time));
 /// ```
@@ -75,17 +75,17 @@ impl LockTime {
     /// # Examples
     ///
     /// ```rust
-    /// # use bitcoin_primitives::relative;
+    /// # use bitcoin_primitives::relative::LockTime;
     ///
     /// // Values with bit 22 set to 0 will be interpreted as height-based lock times.
     /// let height: u32 = 144; // 144 blocks, approx 24h.
-    /// let lock_time = relative::LockTime::from_consensus(height)?;
+    /// let lock_time = LockTime::from_consensus(height)?;
     /// assert!(lock_time.is_block_height());
     /// assert_eq!(lock_time.to_consensus_u32(), height);
     ///
     /// // Values with bit 22 set to 1 will be interpreted as time-based lock times.
     /// let time: u32 = 168 | (1 << 22) ; // Bit 22 is 1 with time approx 24h.
-    /// let lock_time = relative::LockTime::from_consensus(time)?;
+    /// let lock_time = LockTime::from_consensus(time)?;
     /// assert!(lock_time.is_block_time());
     /// assert_eq!(lock_time.to_consensus_u32(), time);
     ///
@@ -118,11 +118,11 @@ pub fn to_consensus_u32(self) -> u32 {
     /// # Examples
     ///
     /// ```rust
-    /// # use bitcoin_primitives::{Sequence, relative};
+    /// # use bitcoin_primitives::{Sequence, relative::LockTime};
     ///
     /// // Interpret a sequence number from a Bitcoin transaction input as a relative lock time
     /// let sequence_number = Sequence::from_consensus(144); // 144 blocks, approx 24h.
-    /// let lock_time = relative::LockTime::from_sequence(sequence_number)?;
+    /// let lock_time = LockTime::from_sequence(sequence_number)?;
     /// assert!(lock_time.is_block_height());
     ///
     /// # Ok::<_, bitcoin_primitives::relative::DisabledLockTimeError>(())
@@ -200,12 +200,12 @@ pub const fn is_block_time(&self) -> bool { !self.is_block_height() }
     ///
     /// ```rust
     /// # use bitcoin_primitives::Sequence;
-    /// # use bitcoin_primitives::relative;
+    /// # use bitcoin_primitives::locktime::relative::{Height, Time};
     ///
     /// # let required_height = 100;       // 100 blocks.
     /// # let intervals = 70;     // Approx 10 hours.
-    /// # let current_height = || relative::Height::from(required_height + 10);
-    /// # let current_time = || relative::Time::from_512_second_intervals(intervals + 10);
+    /// # let current_height = || Height::from(required_height + 10);
+    /// # let current_time = || Time::from_512_second_intervals(intervals + 10);
     /// # let lock = Sequence::from_height(required_height).to_relative_lock_time().expect("valid height");
     ///
     /// // Users that have chain data can get the current height and time to check against a lock.
@@ -269,10 +269,10 @@ pub fn is_implied_by(&self, other: LockTime) -> bool {
     /// # Examples
     ///
     /// ```
-    /// # use bitcoin_primitives::{Sequence, relative};
+    /// # use bitcoin_primitives::{Sequence, relative::LockTime};
     ///
     /// let sequence = Sequence::from_consensus(1 << 22 | 168); // Bit 22 is 1 with time approx 24h.
-    /// let lock_time = relative::LockTime::from_sequence(sequence)?;
+    /// let lock_time = LockTime::from_sequence(sequence)?;
     /// let input_sequence = Sequence::from_consensus(1 << 22 | 336); // Approx 48h.
     /// assert!(lock_time.is_block_time());
     ///
@@ -299,11 +299,11 @@ pub fn is_implied_by_sequence(&self, other: Sequence) -> bool {
     ///
     /// ```rust
     /// # use bitcoin_primitives::Sequence;
-    /// # use bitcoin_primitives::relative;
+    /// # use bitcoin_primitives::locktime::relative::Height;
     ///
     /// let required_height: u16 = 100;
     /// let lock = Sequence::from_height(required_height).to_relative_lock_time().expect("valid height");
-    /// assert!(lock.is_satisfied_by_height(relative::Height::from(required_height + 1)).expect("a height"));
+    /// assert!(lock.is_satisfied_by_height(Height::from(required_height + 1)).expect("a height"));
     /// ```
     #[inline]
     pub fn is_satisfied_by_height(&self, height: Height) -> Result<bool, IncompatibleHeightError> {
@@ -325,11 +325,11 @@ pub fn is_satisfied_by_height(&self, height: Height) -> Result<bool, Incompatibl
     ///
     /// ```rust
     /// # use bitcoin_primitives::Sequence;
-    /// # use bitcoin_primitives::relative;
+    /// # use bitcoin_primitives::locktime::relative::Time;
     ///
     /// let intervals: u16 = 70; // approx 10 hours;
     /// let lock = Sequence::from_512_second_intervals(intervals).to_relative_lock_time().expect("valid time");
-    /// assert!(lock.is_satisfied_by_time(relative::Time::from_512_second_intervals(intervals + 10)).expect("a time"));
+    /// assert!(lock.is_satisfied_by_time(Time::from_512_second_intervals(intervals + 10)).expect("a time"));
     /// ```
     #[inline]
     pub fn is_satisfied_by_time(&self, time: Time) -> Result<bool, IncompatibleTimeError> {
diff --git a/primitives/src/opcodes.rs b/primitives/src/opcodes.rs
index 5da0295d..bd9e8ce8 100644
--- a/primitives/src/opcodes.rs
+++ b/primitives/src/opcodes.rs
@@ -565,12 +565,6 @@ fn formatting_works() {
         assert_eq!(s, "    OP_NOP");
     }
 
-    #[test]
-    fn ordinary_op_code() {
-        let ordinary_op = Ordinary::from_opcode(OP_PUSHDATA1).expect("0x4C");
-        assert_eq!(ordinary_op.to_u8(), 0x4C_u8);
-    }
-
     #[test]
     fn decode_pushnum() {
         // Test all possible opcodes
diff --git a/primitives/src/pow.rs b/primitives/src/pow.rs
index 95743c7a..b4a25f34 100644
--- a/primitives/src/pow.rs
+++ b/primitives/src/pow.rs
@@ -43,26 +43,3 @@ impl fmt::UpperHex for CompactTarget {
     #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::UpperHex::fmt(&self.0, f) }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn compact_target_ordering() {
-        let lower = CompactTarget::from_consensus(0x1d00fffe);
-        let lower_copy = CompactTarget::from_consensus(0x1d00fffe);
-        let higher = CompactTarget::from_consensus(0x1d00ffff);
-
-        assert!(lower < higher);
-        assert!(lower == lower_copy);
-    }
-
-    #[test]
-    fn compact_target_formatting() {
-        let compact_target = CompactTarget::from_consensus(0x1d00ffff);
-        assert_eq!(format!("{:x}", compact_target), "1d00ffff");
-        assert_eq!(format!("{:X}", compact_target), "1D00FFFF");
-        assert_eq!(compact_target.to_consensus(), 0x1d00ffff);
-    }
-}
diff --git a/primitives/src/sequence.rs b/primitives/src/sequence.rs
index e225739f..163f70a0 100644
--- a/primitives/src/sequence.rs
+++ b/primitives/src/sequence.rs
@@ -307,42 +307,4 @@ fn from_seconds_ceil_success() {
     fn from_seconds_ceil_causes_overflow_error() {
         assert!(Sequence::from_seconds_ceil(MAXIMUM_ENCODABLE_SECONDS + 1).is_err());
     }
-
-    #[test]
-    fn sequence_properties() {
-        let seq_max = Sequence(0xFFFFFFFF);
-        let seq_no_rbf = Sequence(0xFFFFFFFE);
-        let seq_rbf = Sequence(0xFFFFFFFD);
-
-        assert!(seq_max.is_final());
-        assert!(!seq_no_rbf.is_final());
-
-        assert!(seq_no_rbf.enables_absolute_lock_time());
-        assert!(!seq_max.enables_absolute_lock_time());
-
-        assert!(seq_rbf.is_rbf());
-        assert!(!seq_no_rbf.is_rbf());
-
-        let seq_relative = Sequence(0x7FFFFFFF);
-        assert!(seq_relative.is_relative_lock_time());
-        assert!(!seq_max.is_relative_lock_time());
-
-        let seq_height_locked = Sequence(0x00399999);
-        let seq_time_locked = Sequence(0x00400000);
-        assert!(seq_height_locked.is_height_locked());
-        assert!(seq_time_locked.is_time_locked());
-        assert!(!seq_time_locked.is_height_locked());
-        assert!(!seq_height_locked.is_time_locked());
-    }
-
-    #[test]
-    fn sequence_formatting() {
-        let sequence = Sequence(0x7FFFFFFF);
-        assert_eq!(format!("{:x}", sequence), "7fffffff");
-        assert_eq!(format!("{:X}", sequence), "7FFFFFFF");
-
-        // Test From<Sequence> for u32
-        let sequence_u32: u32 = sequence.into();
-        assert_eq!(sequence_u32, 0x7FFFFFFF);
-    }
 }
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index ca7e6352..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).
@@ -598,83 +598,3 @@ fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
         Ok(Txid(t))
     }
 }
-
-#[cfg(feature = "alloc")]
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn transaction_functions() {
-        let txin = TxIn {
-            previous_output: OutPoint {
-                txid: Txid::from_byte_array([0xAA; 32]), // Arbitrary invalid dummy value.
-                vout: 0,
-            },
-            script_sig: ScriptBuf::new(),
-            sequence: Sequence::MAX,
-            witness: Witness::new(),
-        };
-
-        let txout = TxOut {
-            value: Amount::from_sat(123456789),
-            script_pubkey: ScriptBuf::new(),
-        };
-
-        let tx_orig = Transaction {
-            version: Version::ONE,
-            lock_time: absolute::LockTime::from_consensus(1738968231), // The time this was written
-            input: vec![txin.clone()],
-            output: vec![txout.clone()],
-        };
-
-        // Test changing the transaction
-        let mut tx = tx_orig.clone();
-        tx.inputs_mut()[0].previous_output.txid = Txid::from_byte_array([0xFF; 32]);
-        tx.outputs_mut()[0].value = Amount::from_sat(987654321);
-        assert_eq!(tx.inputs()[0].previous_output.txid.to_byte_array(), [0xFF; 32]);
-        assert_eq!(tx.outputs()[0].value.to_sat(), 987654321);
-
-        // Test uses_segwit_serialization
-        assert!(!tx.uses_segwit_serialization());
-        tx.input[0].witness.push(vec![0xAB, 0xCD, 0xEF]);
-        assert!(tx.uses_segwit_serialization());
-
-        // Test partial ord
-        assert!(tx > tx_orig);
-    }
-
-    #[test]
-    fn outpoint_from_str() {
-        // Check format errors
-        let mut outpoint_str = "0".repeat(64); // No ":"
-        let outpoint: Result<OutPoint, ParseOutPointError> = outpoint_str.parse();
-        assert_eq!(outpoint, Err(ParseOutPointError::Format));
-
-        outpoint_str.push(':'); // Empty vout
-        let outpoint: Result<OutPoint, ParseOutPointError> = outpoint_str.parse();
-        assert_eq!(outpoint, Err(ParseOutPointError::Format));
-
-        outpoint_str.push('0'); // Correct format
-        let outpoint: OutPoint = outpoint_str.parse().unwrap();
-        assert_eq!(outpoint.txid, Txid::from_byte_array([0; 32]));
-        assert_eq!(outpoint.vout, 0);
-
-        // Check the number of bytes OutPoint contributes to the transaction is equal to SIZE
-        let outpoint_size = outpoint.txid.as_byte_array().len() + outpoint.vout.to_le_bytes().len();
-        assert_eq!(outpoint_size, OutPoint::SIZE);
-
-        // Check TooLong error
-        outpoint_str.push_str("0000000000");
-        let outpoint: Result<OutPoint, ParseOutPointError> = outpoint_str.parse();
-        assert_eq!(outpoint, Err(ParseOutPointError::TooLong));
-    }
-
-    #[test]
-    fn canonical_vout() {
-        assert_eq!(parse_vout("0").unwrap(), 0);
-        assert_eq!(parse_vout("1").unwrap(), 1);
-        assert!(parse_vout("01").is_err()); // Leading zero not allowed
-        assert!(parse_vout("+1").is_err()); // Non digits not allowed
-    }
-}
diff --git a/primitives/src/witness.rs b/primitives/src/witness.rs
index d4875599..abec3e1f 100644
--- a/primitives/src/witness.rs
+++ b/primitives/src/witness.rs
@@ -497,7 +497,6 @@ fn witness_single_empty_element() {
     fn push() {
         // Sanity check default.
         let mut witness = Witness::default();
-        assert!(witness.is_empty());
         assert_eq!(witness.last(), None);
         assert_eq!(witness.second_to_last(), None);
 
@@ -507,11 +506,10 @@ fn push() {
         assert_eq!(witness.nth(3), None);
 
         // Push a single byte element onto the witness stack.
-        let push = [11_u8];
+        let push = [0_u8];
         witness.push(push);
-        assert!(!witness.is_empty());
 
-        let elements = [1u8, 11];
+        let elements = [1u8, 0];
         let expected = Witness {
             witness_elements: 1,
             content: append_u32_vec(&elements, &[0]), // Start at index 0.
@@ -531,10 +529,10 @@ fn push() {
         assert_eq!(witness.nth(3), None);
 
         // Now push 2 byte element onto the witness stack.
-        let push = [21u8, 22u8];
+        let push = [2u8, 3u8];
         witness.push(push);
 
-        let elements = [1u8, 11, 2, 21, 22];
+        let elements = [1u8, 0, 2, 2, 3];
         let expected = Witness {
             witness_elements: 2,
             content: append_u32_vec(&elements, &[0, 2]),
@@ -554,10 +552,10 @@ fn push() {
         assert_eq!(witness.last(), Some(element_1));
 
         // Now push another 2 byte element onto the witness stack.
-        let push = [31u8, 32u8];
+        let push = [4u8, 5u8];
         witness.push(push);
 
-        let elements = [1u8, 11, 2, 21, 22, 2, 31, 32];
+        let elements = [1u8, 0, 2, 2, 3, 2, 4, 5];
         let expected = Witness {
             witness_elements: 3,
             content: append_u32_vec(&elements, &[0, 2, 5]),
@@ -573,7 +571,6 @@ fn push() {
         assert_eq!(witness.nth(2), Some(element_2));
         assert_eq!(witness.nth(3), None);
 
-        assert_eq!(witness.third_to_last(), Some(element_0));
         assert_eq!(witness.second_to_last(), Some(element_1));
         assert_eq!(witness.last(), Some(element_2));
     }
@@ -597,45 +594,6 @@ fn exact_sized_iterator() {
         }
     }
 
-    #[test]
-    fn witness_from_parts() {
-        let elements = [1u8, 11, 2, 21, 22];
-        let witness_elements = 2;
-        let content = append_u32_vec(&elements, &[0, 2]);
-        let indices_start = elements.len();
-        let witness = Witness::from_parts__unstable(content.clone(), witness_elements, indices_start);
-        assert_eq!(witness.nth(0).unwrap(), [11_u8]);
-        assert_eq!(witness.nth(1).unwrap(), [21_u8, 22]);
-        assert_eq!(witness.size(), 6);
-    }
-
-    #[test]
-    fn witness_from_impl() {
-        // Test From implementations with the same 2 elements
-        let vec = vec![vec![11], vec![21, 22]];
-        let slice_vec: &[Vec<u8>] = &vec;
-        let slice_slice: &[&[u8]] = &[&[11u8], &[21, 22]];
-        let vec_slice: Vec<&[u8]> = vec![&[11u8], &[21, 22]];
-
-        let witness_vec_vec = Witness::from(vec.clone());
-        let witness_slice_vec = Witness::from(slice_vec);
-        let witness_slice_slice = Witness::from(slice_slice);
-        let witness_vec_slice = Witness::from(vec_slice);
-
-        let mut expected = Witness::from_slice(&vec);
-        assert_eq!(expected.len(), 2);
-        assert_eq!(expected.to_vec(), vec);
-
-        assert_eq!(witness_vec_vec, expected);
-        assert_eq!(witness_slice_vec, expected);
-        assert_eq!(witness_slice_slice, expected);
-        assert_eq!(witness_vec_slice, expected);
-
-        // Test clear method
-        expected.clear();
-        assert!(expected.is_empty());
-    }
-
     #[test]
     #[cfg(feature = "serde")]
     fn serde_bincode_backward_compatibility() {
diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs
index b290c625..e5e23417 100644
--- a/units/src/amount/mod.rs
+++ b/units/src/amount/mod.rs
@@ -6,6 +6,7 @@
 //! We refer to the documentation on the types for more information.
 
 mod error;
+mod result;
 #[cfg(feature = "serde")]
 pub mod serde;
 
@@ -34,6 +35,7 @@
         OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError,
         PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError,
     },
+    result::{NumOpError, NumOpResult},
     signed::SignedAmount,
     unsigned::Amount,
 };
diff --git a/units/src/amount/result.rs b/units/src/amount/result.rs
new file mode 100644
index 00000000..c0e84f6e
--- /dev/null
+++ b/units/src/amount/result.rs
@@ -0,0 +1,588 @@
+// SPDX-License-Identifier: CC0-1.0
+
+//! Provides a monodic numeric result type that is used to return the result of
+//! doing mathematical operations (`core::ops`) on amount types.
+
+use core::{fmt, ops};
+
+use NumOpResult as R;
+
+use super::{Amount, SignedAmount};
+
+/// Result of an operation on [`Amount`] or [`SignedAmount`].
+///
+/// The type parameter `T` should be normally `Amout` or `SignedAmount`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[must_use]
+pub enum NumOpResult<T> {
+    /// Result of a successful mathematical operation.
+    Valid(T),
+    /// Result of an unsuccessful mathematical operation.
+    Error(NumOpError),
+}
+
+impl<T: fmt::Debug> NumOpResult<T> {
+    /// Returns the contained valid amount, consuming `self`.
+    ///
+    /// # Panics
+    ///
+    /// Panics with `msg` if the numeric result is an `Error`.
+    #[track_caller]
+    pub fn expect(self, msg: &str) -> T {
+        match self {
+            R::Valid(amount) => amount,
+            R::Error(_) => panic!("{}", msg),
+        }
+    }
+
+    /// Returns the contained valid amount, consuming `self`.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the numeric result is an `Error`.
+    #[track_caller]
+    pub fn unwrap(self) -> T {
+        match self {
+            R::Valid(amount) => amount,
+            R::Error(e) => panic!("tried to unwrap an invalid numeric result: {:?}", e),
+        }
+    }
+
+    /// Returns the contained error, consuming `self`.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the numeric result is a valid amount.
+    #[track_caller]
+    pub fn unwrap_err(self) -> NumOpError {
+        match self {
+            R::Error(e) => e,
+            R::Valid(a) => panic!("tried to unwrap a valid numeric result: {:?}", a),
+        }
+    }
+
+    /// Converts this `NumOpResult` to an `Option<T>`.
+    pub fn ok(self) -> Option<T> {
+        match self {
+            R::Valid(amount) => Some(amount),
+            R::Error(_) => None,
+        }
+    }
+
+    /// Converts this `NumOpResult` to a `Result<T, NumOpError>`.
+    #[allow(clippy::missing_errors_doc)]
+    pub fn into_result(self) -> Result<T, NumOpError> {
+        match self {
+            R::Valid(amount) => Ok(amount),
+            R::Error(e) => Err(e),
+        }
+    }
+
+    /// Calls `op` if the numeric result is `Valid`, otherwise returns the `Error` value of `self`.
+    pub fn and_then<F>(self, op: F) -> NumOpResult<T>
+    where
+        F: FnOnce(T) -> NumOpResult<T>,
+    {
+        match self {
+            R::Valid(amount) => op(amount),
+            R::Error(e) => R::Error(e),
+        }
+    }
+
+    /// Returns `true` if the numeric result is a valid amount.
+    pub fn is_valid(&self) -> bool {
+        match self {
+            R::Valid(_) => true,
+            R::Error(_) => false,
+        }
+    }
+
+    /// Returns `true` if the numeric result is an invalid amount.
+    pub fn is_error(&self) -> bool { !self.is_valid() }
+}
+
+impl From<Amount> for NumOpResult<Amount> {
+    fn from(a: Amount) -> Self { Self::Valid(a) }
+}
+impl From<&Amount> for NumOpResult<Amount> {
+    fn from(a: &Amount) -> Self { Self::Valid(*a) }
+}
+
+impl From<SignedAmount> for NumOpResult<SignedAmount> {
+    fn from(a: SignedAmount) -> Self { Self::Valid(a) }
+}
+impl From<&SignedAmount> for NumOpResult<SignedAmount> {
+    fn from(a: &SignedAmount) -> Self { Self::Valid(*a) }
+}
+
+impl ops::Add for Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn add(self, rhs: Amount) -> Self::Output { self.checked_add(rhs).valid_or_error() }
+}
+crate::internal_macros::impl_add_for_amount_references!(Amount);
+
+impl ops::Add<NumOpResult<Amount>> for Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn add(self, rhs: NumOpResult<Amount>) -> Self::Output { rhs.and_then(|a| a + self) }
+}
+impl ops::Add<NumOpResult<Amount>> for &Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn add(self, rhs: NumOpResult<Amount>) -> Self::Output { rhs.and_then(|a| a + self) }
+}
+impl ops::Add<Amount> for NumOpResult<Amount> {
+    type Output = NumOpResult<Amount>;
+
+    fn add(self, rhs: Amount) -> Self::Output { rhs + self }
+}
+impl ops::Add<&Amount> for NumOpResult<Amount> {
+    type Output = NumOpResult<Amount>;
+
+    fn add(self, rhs: &Amount) -> Self::Output { rhs + self }
+}
+
+impl ops::Sub for Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn sub(self, rhs: Amount) -> Self::Output { self.checked_sub(rhs).valid_or_error() }
+}
+crate::internal_macros::impl_sub_for_amount_references!(Amount);
+
+impl ops::Sub<NumOpResult<Amount>> for Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn sub(self, rhs: NumOpResult<Amount>) -> Self::Output {
+        match rhs {
+            R::Valid(amount) => self - amount,
+            R::Error(_) => rhs,
+        }
+    }
+}
+impl ops::Sub<NumOpResult<Amount>> for &Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn sub(self, rhs: NumOpResult<Amount>) -> Self::Output {
+        match rhs {
+            R::Valid(amount) => self - amount,
+            R::Error(_) => rhs,
+        }
+    }
+}
+impl ops::Sub<Amount> for NumOpResult<Amount> {
+    type Output = NumOpResult<Amount>;
+
+    fn sub(self, rhs: Amount) -> Self::Output {
+        match self {
+            R::Valid(amount) => amount - rhs,
+            R::Error(_) => self,
+        }
+    }
+}
+impl ops::Sub<&Amount> for NumOpResult<Amount> {
+    type Output = NumOpResult<Amount>;
+
+    fn sub(self, rhs: &Amount) -> Self::Output {
+        match self {
+            R::Valid(amount) => amount - (*rhs),
+            R::Error(_) => self,
+        }
+    }
+}
+
+impl ops::Mul<u64> for Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn mul(self, rhs: u64) -> Self::Output { self.checked_mul(rhs).valid_or_error() }
+}
+impl ops::Mul<&u64> for Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn mul(self, rhs: &u64) -> Self::Output { self.mul(*rhs) }
+}
+impl ops::Mul<u64> for &Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn mul(self, rhs: u64) -> Self::Output { (*self).mul(rhs) }
+}
+impl ops::Mul<&u64> for &Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn mul(self, rhs: &u64) -> Self::Output { self.mul(*rhs) }
+}
+
+impl ops::Div<u64> for Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).valid_or_error() }
+}
+impl ops::Div<&u64> for Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn div(self, rhs: &u64) -> Self::Output { self.div(*rhs) }
+}
+impl ops::Div<u64> for &Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn div(self, rhs: u64) -> Self::Output { (*self).div(rhs) }
+}
+impl ops::Div<&u64> for &Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn div(self, rhs: &u64) -> Self::Output { (*self).div(*rhs) }
+}
+
+impl ops::Rem<u64> for Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn rem(self, modulus: u64) -> Self::Output { self.checked_rem(modulus).valid_or_error() }
+}
+impl ops::Rem<&u64> for Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn rem(self, modulus: &u64) -> Self::Output { self.rem(*modulus) }
+}
+impl ops::Rem<u64> for &Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn rem(self, modulus: u64) -> Self::Output { (*self).rem(modulus) }
+}
+impl ops::Rem<&u64> for &Amount {
+    type Output = NumOpResult<Amount>;
+
+    fn rem(self, modulus: &u64) -> Self::Output { (*self).rem(*modulus) }
+}
+
+impl ops::Add for SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn add(self, rhs: SignedAmount) -> Self::Output { self.checked_add(rhs).valid_or_error() }
+}
+crate::internal_macros::impl_add_for_amount_references!(SignedAmount);
+
+impl ops::Add<NumOpResult<SignedAmount>> for SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn add(self, rhs: NumOpResult<SignedAmount>) -> Self::Output { rhs.and_then(|a| a + self) }
+}
+impl ops::Add<NumOpResult<SignedAmount>> for &SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn add(self, rhs: NumOpResult<SignedAmount>) -> Self::Output { rhs.and_then(|a| a + self) }
+}
+impl ops::Add<SignedAmount> for NumOpResult<SignedAmount> {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn add(self, rhs: SignedAmount) -> Self::Output { rhs + self }
+}
+impl ops::Add<&SignedAmount> for NumOpResult<SignedAmount> {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn add(self, rhs: &SignedAmount) -> Self::Output { rhs + self }
+}
+
+impl ops::Sub for SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn sub(self, rhs: SignedAmount) -> Self::Output { self.checked_sub(rhs).valid_or_error() }
+}
+crate::internal_macros::impl_sub_for_amount_references!(SignedAmount);
+
+impl ops::Sub<NumOpResult<SignedAmount>> for SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn sub(self, rhs: NumOpResult<SignedAmount>) -> Self::Output {
+        match rhs {
+            R::Valid(amount) => amount - rhs,
+            R::Error(_) => rhs,
+        }
+    }
+}
+impl ops::Sub<NumOpResult<SignedAmount>> for &SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn sub(self, rhs: NumOpResult<SignedAmount>) -> Self::Output {
+        match rhs {
+            R::Valid(amount) => amount - rhs,
+            R::Error(_) => rhs,
+        }
+    }
+}
+impl ops::Sub<SignedAmount> for NumOpResult<SignedAmount> {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn sub(self, rhs: SignedAmount) -> Self::Output {
+        match self {
+            R::Valid(amount) => amount - rhs,
+            R::Error(_) => self,
+        }
+    }
+}
+impl ops::Sub<&SignedAmount> for NumOpResult<SignedAmount> {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn sub(self, rhs: &SignedAmount) -> Self::Output {
+        match self {
+            R::Valid(amount) => amount - *rhs,
+            R::Error(_) => self,
+        }
+    }
+}
+
+impl ops::Mul<i64> for SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn mul(self, rhs: i64) -> Self::Output { self.checked_mul(rhs).valid_or_error() }
+}
+impl ops::Mul<&i64> for SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn mul(self, rhs: &i64) -> Self::Output { self.mul(*rhs) }
+}
+impl ops::Mul<i64> for &SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn mul(self, rhs: i64) -> Self::Output { (*self).mul(rhs) }
+}
+impl ops::Mul<&i64> for &SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn mul(self, rhs: &i64) -> Self::Output { self.mul(*rhs) }
+}
+
+impl ops::Div<i64> for SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn div(self, rhs: i64) -> Self::Output { self.checked_div(rhs).valid_or_error() }
+}
+impl ops::Div<&i64> for SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn div(self, rhs: &i64) -> Self::Output { self.div(*rhs) }
+}
+impl ops::Div<i64> for &SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn div(self, rhs: i64) -> Self::Output { (*self).div(rhs) }
+}
+impl ops::Div<&i64> for &SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn div(self, rhs: &i64) -> Self::Output { (*self).div(*rhs) }
+}
+
+impl ops::Rem<i64> for SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn rem(self, modulus: i64) -> Self::Output { self.checked_rem(modulus).valid_or_error() }
+}
+impl ops::Rem<&i64> for SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn rem(self, modulus: &i64) -> Self::Output { self.rem(*modulus) }
+}
+impl ops::Rem<i64> for &SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn rem(self, modulus: i64) -> Self::Output { (*self).rem(modulus) }
+}
+impl ops::Rem<&i64> for &SignedAmount {
+    type Output = NumOpResult<SignedAmount>;
+
+    fn rem(self, modulus: &i64) -> Self::Output { (*self).rem(*modulus) }
+}
+
+impl ops::Neg for SignedAmount {
+    type Output = Self;
+
+    fn neg(self) -> Self::Output { Self::from_sat(self.to_sat().neg()) }
+}
+
+impl<T> ops::Add for NumOpResult<T>
+where
+    T: ops::Add<Output = NumOpResult<T>>,
+{
+    type Output = NumOpResult<T>;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        match (self, rhs) {
+            (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
+            (_, _) => R::Error(NumOpError {}),
+        }
+    }
+}
+impl<T> ops::Add<NumOpResult<T>> for &NumOpResult<T>
+where
+    T: ops::Add<Output = NumOpResult<T>> + Copy,
+{
+    type Output = NumOpResult<T>;
+
+    fn add(self, rhs: NumOpResult<T>) -> Self::Output {
+        match (self, rhs) {
+            (R::Valid(lhs), R::Valid(rhs)) => *lhs + rhs,
+            (_, _) => R::Error(NumOpError {}),
+        }
+    }
+}
+impl<T> ops::Add<&NumOpResult<T>> for NumOpResult<T>
+where
+    T: ops::Add<Output = NumOpResult<T>> + Copy,
+{
+    type Output = NumOpResult<T>;
+
+    fn add(self, rhs: &NumOpResult<T>) -> Self::Output {
+        match (self, rhs) {
+            (R::Valid(lhs), R::Valid(rhs)) => lhs + *rhs,
+            (_, _) => R::Error(NumOpError {}),
+        }
+    }
+}
+impl<T> ops::Add for &NumOpResult<T>
+where
+    T: ops::Add<Output = NumOpResult<T>> + Copy,
+{
+    type Output = NumOpResult<T>;
+
+    fn add(self, rhs: &NumOpResult<T>) -> Self::Output {
+        match (self, rhs) {
+            (R::Valid(lhs), R::Valid(rhs)) => *lhs + *rhs,
+            (_, _) => R::Error(NumOpError {}),
+        }
+    }
+}
+
+impl<T> ops::Sub for NumOpResult<T>
+where
+    T: ops::Sub<Output = NumOpResult<T>>,
+{
+    type Output = NumOpResult<T>;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        match (self, rhs) {
+            (R::Valid(lhs), R::Valid(rhs)) => lhs - rhs,
+            (_, _) => R::Error(NumOpError {}),
+        }
+    }
+}
+impl<T> ops::Sub<NumOpResult<T>> for &NumOpResult<T>
+where
+    T: ops::Sub<Output = NumOpResult<T>> + Copy,
+{
+    type Output = NumOpResult<T>;
+
+    fn sub(self, rhs: NumOpResult<T>) -> Self::Output {
+        match (self, rhs) {
+            (R::Valid(lhs), R::Valid(rhs)) => *lhs - rhs,
+            (_, _) => R::Error(NumOpError {}),
+        }
+    }
+}
+impl<T> ops::Sub<&NumOpResult<T>> for NumOpResult<T>
+where
+    T: ops::Sub<Output = NumOpResult<T>> + Copy,
+{
+    type Output = NumOpResult<T>;
+
+    fn sub(self, rhs: &NumOpResult<T>) -> Self::Output {
+        match (self, rhs) {
+            (R::Valid(lhs), R::Valid(rhs)) => lhs - *rhs,
+            (_, _) => R::Error(NumOpError {}),
+        }
+    }
+}
+impl<T> ops::Sub for &NumOpResult<T>
+where
+    T: ops::Sub<Output = NumOpResult<T>> + Copy,
+{
+    type Output = NumOpResult<T>;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        match (self, rhs) {
+            (R::Valid(lhs), R::Valid(rhs)) => *lhs - *rhs,
+            (_, _) => R::Error(NumOpError {}),
+        }
+    }
+}
+
+impl core::iter::Sum<NumOpResult<Amount>> for NumOpResult<Amount> {
+    fn sum<I>(iter: I) -> Self
+    where
+        I: Iterator<Item = NumOpResult<Amount>>,
+    {
+        iter.fold(R::Valid(Amount::ZERO), |acc, amount| match (acc, amount) {
+            (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
+            (_, _) => R::Error(NumOpError {}),
+        })
+    }
+}
+impl<'a> core::iter::Sum<&'a NumOpResult<Amount>> for NumOpResult<Amount> {
+    fn sum<I>(iter: I) -> Self
+    where
+        I: Iterator<Item = &'a NumOpResult<Amount>>,
+    {
+        iter.fold(R::Valid(Amount::ZERO), |acc, amount| match (acc, amount) {
+            (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
+            (_, _) => R::Error(NumOpError {}),
+        })
+    }
+}
+
+impl core::iter::Sum<NumOpResult<SignedAmount>> for NumOpResult<SignedAmount> {
+    fn sum<I>(iter: I) -> Self
+    where
+        I: Iterator<Item = NumOpResult<SignedAmount>>,
+    {
+        iter.fold(R::Valid(SignedAmount::ZERO), |acc, amount| match (acc, amount) {
+            (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
+            (_, _) => R::Error(NumOpError {}),
+        })
+    }
+}
+impl<'a> core::iter::Sum<&'a NumOpResult<SignedAmount>> for NumOpResult<SignedAmount> {
+    fn sum<I>(iter: I) -> Self
+    where
+        I: Iterator<Item = &'a NumOpResult<SignedAmount>>,
+    {
+        iter.fold(R::Valid(SignedAmount::ZERO), |acc, amount| match (acc, amount) {
+            (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
+            (_, _) => R::Error(NumOpError {}),
+        })
+    }
+}
+
+pub(in crate::amount) trait OptionExt<T> {
+    fn valid_or_error(self) -> NumOpResult<T>;
+}
+
+impl OptionExt<Amount> for Option<Amount> {
+    fn valid_or_error(self) -> NumOpResult<Amount> {
+        match self {
+            Some(amount) => R::Valid(amount),
+            None => R::Error(NumOpError {}),
+        }
+    }
+}
+
+impl OptionExt<SignedAmount> for Option<SignedAmount> {
+    fn valid_or_error(self) -> NumOpResult<SignedAmount> {
+        match self {
+            Some(amount) => R::Valid(amount),
+            None => R::Error(NumOpError {}),
+        }
+    }
+}
+
+/// An error occurred while doing a mathematical operation.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct NumOpError;
+
+impl fmt::Display for NumOpError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "a math operation on amounts gave an invalid numeric result")
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for NumOpError {}
diff --git a/units/src/amount/signed.rs b/units/src/amount/signed.rs
index 99602eca..f51be557 100644
--- a/units/src/amount/signed.rs
+++ b/units/src/amount/signed.rs
@@ -5,7 +5,7 @@
 #[cfg(feature = "alloc")]
 use alloc::string::{String, ToString};
 use core::str::FromStr;
-use core::{default, fmt, ops};
+use core::{default, fmt};
 
 #[cfg(feature = "arbitrary")]
 use arbitrary::{Arbitrary, Unstructured};
@@ -479,68 +479,6 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     }
 }
 
-impl ops::Add for SignedAmount {
-    type Output = SignedAmount;
-
-    fn add(self, rhs: SignedAmount) -> Self::Output {
-        self.checked_add(rhs).expect("SignedAmount addition error")
-    }
-}
-crate::internal_macros::impl_add_for_references!(SignedAmount);
-crate::internal_macros::impl_add_assign!(SignedAmount);
-
-impl ops::Sub for SignedAmount {
-    type Output = SignedAmount;
-
-    fn sub(self, rhs: SignedAmount) -> Self::Output {
-        self.checked_sub(rhs).expect("SignedAmount subtraction error")
-    }
-}
-crate::internal_macros::impl_sub_for_references!(SignedAmount);
-crate::internal_macros::impl_sub_assign!(SignedAmount);
-
-impl ops::Rem<i64> for SignedAmount {
-    type Output = SignedAmount;
-
-    fn rem(self, modulus: i64) -> Self::Output {
-        self.checked_rem(modulus).expect("SignedAmount remainder error")
-    }
-}
-
-impl ops::RemAssign<i64> for SignedAmount {
-    fn rem_assign(&mut self, modulus: i64) { *self = *self % modulus }
-}
-
-impl ops::Mul<i64> for SignedAmount {
-    type Output = SignedAmount;
-
-    fn mul(self, rhs: i64) -> Self::Output {
-        self.checked_mul(rhs).expect("SignedAmount multiplication error")
-    }
-}
-
-impl ops::MulAssign<i64> for SignedAmount {
-    fn mul_assign(&mut self, rhs: i64) { *self = *self * rhs }
-}
-
-impl ops::Div<i64> for SignedAmount {
-    type Output = SignedAmount;
-
-    fn div(self, rhs: i64) -> Self::Output {
-        self.checked_div(rhs).expect("SignedAmount division error")
-    }
-}
-
-impl ops::DivAssign<i64> for SignedAmount {
-    fn div_assign(&mut self, rhs: i64) { *self = *self / rhs }
-}
-
-impl ops::Neg for SignedAmount {
-    type Output = Self;
-
-    fn neg(self) -> Self::Output { Self(self.0.neg()) }
-}
-
 impl FromStr for SignedAmount {
     type Err = ParseError;
 
@@ -572,23 +510,6 @@ fn from(value: Amount) -> Self {
     }
 }
 
-impl core::iter::Sum for SignedAmount {
-    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
-        let sats: i64 = iter.map(|amt| amt.0).sum();
-        Self::from_sat(sats)
-    }
-}
-
-impl<'a> core::iter::Sum<&'a SignedAmount> for SignedAmount {
-    fn sum<I>(iter: I) -> Self
-    where
-        I: Iterator<Item = &'a SignedAmount>,
-    {
-        let sats: i64 = iter.map(|amt| amt.0).sum();
-        Self::from_sat(sats)
-    }
-}
-
 #[cfg(feature = "arbitrary")]
 impl<'a> Arbitrary<'a> for SignedAmount {
     fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
diff --git a/units/src/amount/unsigned.rs b/units/src/amount/unsigned.rs
index 645dbe9d..c52a8f2c 100644
--- a/units/src/amount/unsigned.rs
+++ b/units/src/amount/unsigned.rs
@@ -5,7 +5,7 @@
 #[cfg(feature = "alloc")]
 use alloc::string::{String, ToString};
 use core::str::FromStr;
-use core::{default, fmt, ops};
+use core::{default, fmt};
 
 #[cfg(feature = "arbitrary")]
 use arbitrary::{Arbitrary, Unstructured};
@@ -422,60 +422,6 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     }
 }
 
-impl ops::Add for Amount {
-    type Output = Amount;
-
-    fn add(self, rhs: Amount) -> Self::Output {
-        self.checked_add(rhs).expect("Amount addition error")
-    }
-}
-crate::internal_macros::impl_add_for_references!(Amount);
-crate::internal_macros::impl_add_assign!(Amount);
-
-impl ops::Sub for Amount {
-    type Output = Amount;
-
-    fn sub(self, rhs: Amount) -> Self::Output {
-        self.checked_sub(rhs).expect("Amount subtraction error")
-    }
-}
-crate::internal_macros::impl_sub_for_references!(Amount);
-crate::internal_macros::impl_sub_assign!(Amount);
-
-impl ops::Rem<u64> for Amount {
-    type Output = Amount;
-
-    fn rem(self, modulus: u64) -> Self::Output {
-        self.checked_rem(modulus).expect("Amount remainder error")
-    }
-}
-
-impl ops::RemAssign<u64> for Amount {
-    fn rem_assign(&mut self, modulus: u64) { *self = *self % modulus }
-}
-
-impl ops::Mul<u64> for Amount {
-    type Output = Amount;
-
-    fn mul(self, rhs: u64) -> Self::Output {
-        self.checked_mul(rhs).expect("Amount multiplication error")
-    }
-}
-
-impl ops::MulAssign<u64> for Amount {
-    fn mul_assign(&mut self, rhs: u64) { *self = *self * rhs }
-}
-
-impl ops::Div<u64> for Amount {
-    type Output = Amount;
-
-    fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).expect("Amount division error") }
-}
-
-impl ops::DivAssign<u64> for Amount {
-    fn div_assign(&mut self, rhs: u64) { *self = *self / rhs }
-}
-
 impl FromStr for Amount {
     type Err = ParseError;
 
@@ -506,23 +452,6 @@ impl TryFrom<SignedAmount> for Amount {
     fn try_from(value: SignedAmount) -> Result<Self, Self::Error> { value.to_unsigned() }
 }
 
-impl core::iter::Sum for Amount {
-    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
-        let sats: u64 = iter.map(|amt| amt.0).sum();
-        Self::from_sat(sats)
-    }
-}
-
-impl<'a> core::iter::Sum<&'a Amount> for Amount {
-    fn sum<I>(iter: I) -> Self
-    where
-        I: Iterator<Item = &'a Amount>,
-    {
-        let sats: u64 = iter.map(|amt| amt.0).sum();
-        Self::from_sat(sats)
-    }
-}
-
 #[cfg(feature = "arbitrary")]
 impl<'a> Arbitrary<'a> for Amount {
     fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
diff --git a/units/src/amount/verification.rs b/units/src/amount/verification.rs
index f0d7b47e..ecf35d21 100644
--- a/units/src/amount/verification.rs
+++ b/units/src/amount/verification.rs
@@ -29,19 +29,11 @@ fn u_amount_homomorphic() {
     let a2 = Amount::from_sat(n2);
     kani::assume(a1.checked_add(a2).is_some()); // Adding amounts doesn't overflow.
 
-    assert_eq!(Amount::from_sat(n1) + Amount::from_sat(n2), Amount::from_sat(n1 + n2));
-
-    let mut amt = Amount::from_sat(n1);
-    amt += Amount::from_sat(n2);
-    assert_eq!(amt, Amount::from_sat(n1 + n2));
+    assert_eq!(Amount::from_sat(n1) + Amount::from_sat(n2), Amount::from_sat(n1 + n2).into());
 
     let max = cmp::max(n1, n2);
     let min = cmp::min(n1, n2);
-    assert_eq!(Amount::from_sat(max) - Amount::from_sat(min), Amount::from_sat(max - min));
-
-    let mut amt = Amount::from_sat(max);
-    amt -= Amount::from_sat(min);
-    assert_eq!(amt, Amount::from_sat(max - min));
+    assert_eq!(Amount::from_sat(max) - Amount::from_sat(min), Amount::from_sat(max - min).into());
 }
 
 #[kani::unwind(4)]
@@ -59,17 +51,10 @@ fn s_amount_homomorphic() {
 
     assert_eq!(
         SignedAmount::from_sat(n1) + SignedAmount::from_sat(n2),
-        SignedAmount::from_sat(n1 + n2)
+        SignedAmount::from_sat(n1 + n2).into()
     );
     assert_eq!(
         SignedAmount::from_sat(n1) - SignedAmount::from_sat(n2),
-        SignedAmount::from_sat(n1 - n2)
+        SignedAmount::from_sat(n1 - n2).into()
     );
-
-    let mut amt = SignedAmount::from_sat(n1);
-    amt += SignedAmount::from_sat(n2);
-    assert_eq!(amt, SignedAmount::from_sat(n1 + n2));
-    let mut amt = SignedAmount::from_sat(n1);
-    amt -= SignedAmount::from_sat(n2);
-    assert_eq!(amt, SignedAmount::from_sat(n1 - n2));
 }
diff --git a/units/src/internal_macros.rs b/units/src/internal_macros.rs
index 475f9374..1bae6898 100644
--- a/units/src/internal_macros.rs
+++ b/units/src/internal_macros.rs
@@ -34,6 +34,36 @@ fn add(self, rhs: &'a $ty) -> Self::Output { *self + *rhs }
 }
 pub(crate) use impl_add_for_references;
 
+/// Implements `ops::Add` for various amount references.
+///
+/// Requires `$ty` it implement `Add` e.g. 'impl Add<T> for T'. Adds impls of:
+///
+/// - Add<T> for &T
+/// - Add<&T> for T
+/// - Add<&T> for &T
+macro_rules! impl_add_for_amount_references {
+    ($ty:ident) => {
+        impl core::ops::Add<$ty> for &$ty {
+            type Output = NumOpResult<$ty>;
+
+            fn add(self, rhs: $ty) -> Self::Output { *self + rhs }
+        }
+
+        impl core::ops::Add<&$ty> for $ty {
+            type Output = NumOpResult<$ty>;
+
+            fn add(self, rhs: &$ty) -> Self::Output { self + *rhs }
+        }
+
+        impl<'a> core::ops::Add<&'a $ty> for &$ty {
+            type Output = NumOpResult<$ty>;
+
+            fn add(self, rhs: &'a $ty) -> Self::Output { *self + *rhs }
+        }
+    };
+}
+pub(crate) use impl_add_for_amount_references;
+
 /// Implement `ops::AddAssign` for `$ty` and `&$ty`.
 macro_rules! impl_add_assign {
     ($ty:ident) => {
@@ -78,6 +108,36 @@ fn sub(self, rhs: &'a $ty) -> Self::Output { *self - *rhs }
 }
 pub(crate) use impl_sub_for_references;
 
+/// Implement `ops::Sub` for various amount references.
+///
+/// Requires `$ty` it implement `Sub` e.g. 'impl Sub<T> for T'. Adds impls of:
+///
+/// - Sub<T> for &T
+/// - Sub<&T> for T
+/// - Sub<&T> for &T
+macro_rules! impl_sub_for_amount_references {
+    ($ty:ident) => {
+        impl core::ops::Sub<$ty> for &$ty {
+            type Output = NumOpResult<$ty>;
+
+            fn sub(self, rhs: $ty) -> Self::Output { *self - rhs }
+        }
+
+        impl core::ops::Sub<&$ty> for $ty {
+            type Output = NumOpResult<$ty>;
+
+            fn sub(self, rhs: &$ty) -> Self::Output { self - *rhs }
+        }
+
+        impl<'a> core::ops::Sub<&'a $ty> for &$ty {
+            type Output = NumOpResult<$ty>;
+
+            fn sub(self, rhs: &'a $ty) -> Self::Output { *self - *rhs }
+        }
+    };
+}
+pub(crate) use impl_sub_for_amount_references;
+
 /// Implement `ops::SubAssign` for `$ty` and `&$ty`.
 macro_rules! impl_sub_assign {
     ($ty:ident) => {


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..0b685679 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")]
 
@@ -42,7 +40,10 @@ fn $test_name() {
 pub struct RegHashTag;
 
 impl sha256t::Tag for RegHashTag {
-    const MIDSTATE: sha256::Midstate = sha256::Midstate::new([0xab; 32], 64);
+    fn engine() -> sha256::HashEngine {
+        let midstate = sha256::Midstate::new([0xab; 32], 64);
+        sha256::HashEngine::from_midstate(midstate)
+    }
 }
 
 type RegHash = sha256t::Hash<RegHashTag>;
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=""
diff --git a/units/src/amount/tests.rs b/units/src/amount/tests.rs
index a733e304..f5b450fd 100644
--- a/units/src/amount/tests.rs
+++ b/units/src/amount/tests.rs
@@ -116,28 +116,15 @@ fn mul_div() {
     let sat = Amount::from_sat;
     let ssat = SignedAmount::from_sat;
 
-    assert_eq!(sat(14) * 3, sat(42));
-    assert_eq!(sat(14) / 2, sat(7));
-    assert_eq!(sat(14) % 3, sat(2));
-    assert_eq!(ssat(-14) * 3, ssat(-42));
-    assert_eq!(ssat(-14) / 2, ssat(-7));
-    assert_eq!(ssat(-14) % 3, ssat(-2));
-
-    let mut a = sat(30);
-    a /= 3;
-    assert_eq!(a, sat(10));
-    a %= 3;
-    assert_eq!(a, sat(1));
-    a *= 3;
-    assert_eq!(a, sat(3));
-
-    let mut b = ssat(30);
-    b /= 3;
-    assert_eq!(b, ssat(10));
-    b %= 3;
-    assert_eq!(b, ssat(1));
-    b *= 3;
-    assert_eq!(b, ssat(3));
+    let op_result_sat = |sat| NumOpResult::Valid(Amount::from_sat(sat));
+    let op_result_ssat = |sat| NumOpResult::Valid(SignedAmount::from_sat(sat));
+
+    assert_eq!(sat(14) * 3, op_result_sat(42));
+    assert_eq!(sat(14) / 2, op_result_sat(7));
+    assert_eq!(sat(14) % 3, op_result_sat(2));
+    assert_eq!(ssat(-14) * 3, op_result_ssat(-42));
+    assert_eq!(ssat(-14) / 2, op_result_ssat(-7));
+    assert_eq!(ssat(-14) % 3, op_result_ssat(-2));
 }
 
 #[test]
@@ -149,11 +136,41 @@ fn neg() {
 #[cfg(feature = "std")]
 #[test]
 fn overflows() {
-    // panic on overflow
-    let result = panic::catch_unwind(|| Amount::MAX + Amount::from_sat_unchecked(1));
-    assert!(result.is_err());
-    let result = panic::catch_unwind(|| Amount::from_sat_unchecked(8_446_744_073_709_551_615) * 3);
-    assert!(result.is_err());
+    let result = Amount::MAX + Amount::from_sat_unchecked(1);
+    assert!(result.is_error());
+    let result = Amount::from_sat_unchecked(8_446_744_073_709_551_615) * 3;
+    assert!(result.is_error());
+}
+
+#[test]
+fn add() {
+    let sat = Amount::from_sat;
+    let ssat = SignedAmount::from_sat;
+
+    assert!(sat(0) + sat(0) == sat(0).into());
+    assert!(sat(127) + sat(179) == sat(306).into());
+
+    assert!(ssat(0) + ssat(0) == ssat(0).into());
+    assert!(ssat(127) + ssat(179) == ssat(306).into());
+    assert!(ssat(-127) + ssat(179) == ssat(52).into());
+    assert!(ssat(127) + ssat(-179) == ssat(-52).into());
+    assert!(ssat(-127) + ssat(-179) == ssat(-306).into());
+}
+
+#[test]
+fn sub() {
+    let sat = Amount::from_sat;
+    let ssat = SignedAmount::from_sat;
+
+    assert!(sat(0) - sat(0) == sat(0).into());
+    assert!(sat(179) - sat(127) == sat(52).into());
+    assert!((sat(127) - sat(179)).is_error());
+
+    assert!(ssat(0) - ssat(0) == ssat(0).into());
+    assert!(ssat(127) - ssat(179) == ssat(-52).into());
+    assert!(ssat(-127) - ssat(179) == ssat(-306).into());
+    assert!(ssat(127) - ssat(-179) == ssat(306).into());
+    assert!(ssat(-127) - ssat(-179) == ssat(52).into());
 }
 
 #[test]
@@ -990,18 +1007,28 @@ fn sum_amounts() {
     let sat = Amount::from_sat;
     let ssat = SignedAmount::from_sat;
 
-    assert_eq!([].iter().sum::<Amount>(), Amount::ZERO);
-    assert_eq!([].iter().sum::<SignedAmount>(), SignedAmount::ZERO);
+    assert_eq!([].iter().sum::<NumOpResult<Amount>>(), Amount::ZERO.into());
+    assert_eq!([].iter().sum::<NumOpResult<SignedAmount>>(), SignedAmount::ZERO.into());
 
     let amounts = [sat(42), sat(1337), sat(21)];
-    assert_eq!(amounts.iter().sum::<Amount>(), sat(1400));
-    let sum = amounts.into_iter().sum::<Amount>();
-    assert_eq!(sum, sat(1400));
+    assert_eq!(
+        amounts.iter().map(|a| NumOpResult::Valid(*a)).sum::<NumOpResult<Amount>>(),
+        sat(1400).into(),
+    );
+    assert_eq!(
+        amounts.into_iter().map(NumOpResult::Valid).sum::<NumOpResult<Amount>>(),
+        sat(1400).into(),
+    );
 
     let amounts = [ssat(-42), ssat(1337), ssat(21)];
-    assert_eq!(amounts.iter().sum::<SignedAmount>(), ssat(1316));
-    let sum = amounts.into_iter().sum::<SignedAmount>();
-    assert_eq!(sum, ssat(1316));
+    assert_eq!(
+        amounts.iter().map(NumOpResult::from).sum::<NumOpResult<SignedAmount>>(),
+        ssat(1316).into(),
+    );
+    assert_eq!(
+        amounts.into_iter().map(NumOpResult::from).sum::<NumOpResult<SignedAmount>>(),
+        ssat(1316).into()
+    );
 }
 
 #[test]
@@ -1108,10 +1135,11 @@ fn unsigned_addition() {
     let two = sat(2);
     let three = sat(3);
 
-    assert!(one + two == three);
-    assert!(&one + two == three);
-    assert!(one + &two == three);
-    assert!(&one + &two == three);
+    assert!((one + two) == three.into());
+    assert!((one + two) == three.into());
+    assert!((&one + two) == three.into());
+    assert!((one + &two) == three.into());
+    assert!((&one + &two) == three.into());
 }
 
 #[test]
@@ -1123,36 +1151,10 @@ fn unsigned_subtract() {
     let two = sat(2);
     let three = sat(3);
 
-    assert!(three - two == one);
-    assert!(&three - two == one);
-    assert!(three - &two == one);
-    assert!(&three - &two == one);
-}
-
-#[test]
-fn unsigned_add_assign() {
-    let sat = Amount::from_sat;
-
-    let mut f = sat(1);
-    f += sat(2);
-    assert_eq!(f, sat(3));
-
-    let mut f = sat(1);
-    f += &sat(2);
-    assert_eq!(f, sat(3));
-}
-
-#[test]
-fn unsigned_sub_assign() {
-    let sat = Amount::from_sat;
-
-    let mut f = sat(3);
-    f -= sat(2);
-    assert_eq!(f, sat(1));
-
-    let mut f = sat(3);
-    f -= &sat(2);
-    assert_eq!(f, sat(1));
+    assert!(three - two == one.into());
+    assert!(&three - two == one.into());
+    assert!(three - &two == one.into());
+    assert!(&three - &two == one.into());
 }
 
 #[test]
@@ -1164,10 +1166,10 @@ fn signed_addition() {
     let two = ssat(2);
     let three = ssat(3);
 
-    assert!(one + two == three);
-    assert!(&one + two == three);
-    assert!(one + &two == three);
-    assert!(&one + &two == three);
+    assert!(one + two == three.into());
+    assert!(&one + two == three.into());
+    assert!(one + &two == three.into());
+    assert!(&one + &two == three.into());
 }
 
 #[test]
@@ -1179,43 +1181,199 @@ fn signed_subtract() {
     let two = ssat(2);
     let three = ssat(3);
 
-    assert!(three - two == one);
-    assert!(&three - two == one);
-    assert!(three - &two == one);
-    assert!(&three - &two == one);
+    assert!(three - two == one.into());
+    assert!(&three - two == one.into());
+    assert!(three - &two == one.into());
+    assert!(&three - &two == one.into());
 }
 
 #[test]
-fn signed_add_assign() {
-    let ssat = SignedAmount::from_sat;
+fn check_const() {
+    assert_eq!(SignedAmount::ONE_BTC.to_sat(), 100_000_000);
+    assert_eq!(Amount::ONE_BTC.to_sat(), 100_000_000);
+    assert_eq!(SignedAmount::FIFTY_BTC.to_sat(), SignedAmount::ONE_BTC.to_sat() * 50);
+    assert_eq!(Amount::FIFTY_BTC.to_sat(), Amount::ONE_BTC.to_sat() * 50);
+    assert_eq!(Amount::MAX_MONEY.to_sat() as i64, SignedAmount::MAX_MONEY.to_sat());
+}
 
-    let mut f = ssat(1);
-    f += ssat(2);
-    assert_eq!(f, ssat(3));
+// Verify we have implemented all combinations of ops for `Amount` and `SignedAmount`.
+// It's easier to read this test that check the code.
+#[test]
+#[allow(clippy::op_ref)] // We are explicitly testing the references work with ops.
+fn amount_tyes_all_ops() {
+    // Sanity check than stdlib supports the set of reference combinations for the ops we want.
+    {
+        let x = 127;
 
-    let mut f = ssat(1);
-    f += &ssat(2);
-    assert_eq!(f, ssat(3));
+        let _ = x + x;
+        let _ = &x + x;
+        let _ = x + &x;
+        let _ = &x + &x;
+
+        let _ = x - x;
+        let _ = &x - x;
+        let _ = x - &x;
+        let _ = &x - &x;
+
+        let _ = -x;
+    }
+
+    let sat = Amount::from_sat(1);
+    let ssat = SignedAmount::from_sat(1);
+
+    // Add
+    let _ = sat + sat;
+    let _ = &sat + sat;
+    let _ = sat + &sat;
+    let _ = &sat + &sat;
+
+    // let _ = ssat + sat;
+    // let _ = &ssat + sat;
+    // let _ = ssat + &sat;
+    // let _ = &ssat + &sat;
+
+    // let _ = sat + ssat;
+    // let _ = &sat + ssat;
+    // let _ = sat + &ssat;
+    // let _ = &sat + &ssat;
+
+    let _ = ssat + ssat;
+    let _ = &ssat + ssat;
+    let _ = ssat + &ssat;
+    let _ = &ssat + &ssat;
+
+    // Sub
+    let _ = sat - sat;
+    let _ = &sat - sat;
+    let _ = sat - &sat;
+    let _ = &sat - &sat;
+
+    // let _ = ssat - sat;
+    // let _ = &ssat - sat;
+    // let _ = ssat - &sat;
+    // let _ = &ssat - &sat;
+
+    // let _ = sat - ssat;
+    // let _ = &sat - ssat;
+    // let _ = sat - &ssat;
+    // let _ = &sat - &ssat;
+
+    let _ = ssat - ssat;
+    let _ = &ssat - ssat;
+    let _ = ssat - &ssat;
+    let _ = &ssat - &ssat;
+
+    // let _ = sat * sat;  // Intentionally not supported.
+
+    // Mul
+    let _ = sat * 3;
+    let _ = sat * &3;
+    let _ = &sat * 3;
+    let _ = &sat * &3;
+
+    let _ = ssat * 3_i64; // Explicit type for the benefit of the reader.
+    let _ = ssat * &3;
+    let _ = &ssat * 3;
+    let _ = &ssat * &3;
+
+    // Div
+    let _ = sat / 3;
+    let _ = &sat / 3;
+    let _ = sat / &3;
+    let _ = &sat / &3;
+
+    let _ = ssat / 3_i64; // Explicit type for the benefit of the reader.
+    let _ = &ssat / 3;
+    let _ = ssat / &3;
+    let _ = &ssat / &3;
+
+    // Rem
+    let _ = sat % 3;
+    let _ = &sat % 3;
+    let _ = sat % &3;
+    let _ = &sat % &3;
+
+    let _ = ssat % 3;
+    let _ = &ssat % 3;
+    let _ = ssat % &3;
+    let _ = &ssat % &3;
+
+    // FIXME: Do we want to support this?
+    // let _ = sat / sat;
+    //
+    // "How many times does this amount go into that amount?" - seems
+    // like a reasonable question to ask.
+
+    // FIXME: Do we want to support these?
+    // let _ = -sat;
+    // let _ = -ssat;
 }
 
+// FIXME: Should we support this sort of thing?
+// It will be a lot more code for possibly not that much benefit.
 #[test]
-fn signed_sub_assign() {
-    let ssat = SignedAmount::from_sat;
-
-    let mut f = ssat(3);
-    f -= ssat(2);
-    assert_eq!(f, ssat(1));
+fn can_ops_on_amount_and_signed_amount() {
+    // let res: NumOpResult<SignedAmount> = sat + ssat;
+}
 
-    let mut f = ssat(3);
-    f -= &ssat(2);
-    assert_eq!(f, ssat(1));
+// Verify we have implemented all combinations of ops for the `NumOpResult` type.
+// It's easier to read this test that check the code.
+#[test]
+#[allow(clippy::op_ref)] // We are explicitly testing the references work with ops.
+fn amount_op_result_all_ops() {
+    let sat = Amount::from_sat(1);
+    // let ssat = SignedAmount::from_sat(1);
+
+    // Explicit type as sanity check.
+    let res: NumOpResult<Amount> = sat + sat;
+    // let sres: NumOpResult<SignedAmount> = ssat + ssat;
+
+    // Operations that where RHS is the result of another operation.
+    let _ = sat + res.clone();
+    let _ = &sat + res.clone();
+    // let _ = sat + &res.clone();
+    // let _ = &sat + &res.clone();
+
+    let _ = sat - res.clone();
+    let _ = &sat - res.clone();
+    // let _ = sat - &res.clone();
+    // let _ = &sat - &res.clone();
+
+    // Operations that where LHS is the result of another operation.
+    let _ = res.clone() + sat;
+    // let _ = &res.clone() + sat;
+    let _ = res.clone() + &sat;
+    // let _ = &res.clone() + &sat;
+
+    let _ = res.clone() - sat;
+    // let _ = &res.clone() - sat;
+    let _ = res.clone() - &sat;
+    // let _ = &res.clone() - &sat;
+
+    // Operations that where both sides are the result of another operation.
+    let _ = res.clone() + res.clone();
+    // let _ = &res.clone() + res.clone();
+    // let _ = res.clone() + &res.clone();
+    // let _ = &res.clone() + &res.clone();
+
+    let _ = res.clone() - res.clone();
+    // let _ = &res.clone() - res.clone();
+    // let _ = res.clone() - &res.clone();
+    // let _ = &res.clone() - &res.clone();
 }
 
+// Verify we have implemented all `Sum` for the `NumOpResult` type.
 #[test]
-fn check_const() {
-    assert_eq!(SignedAmount::ONE_BTC.to_sat(), 100_000_000);
-    assert_eq!(Amount::ONE_BTC.to_sat(), 100_000_000);
-    assert_eq!(SignedAmount::FIFTY_BTC.to_sat(), SignedAmount::ONE_BTC.to_sat() * 50);
-    assert_eq!(Amount::FIFTY_BTC.to_sat(), Amount::ONE_BTC.to_sat() * 50);
-    assert_eq!(Amount::MAX_MONEY.to_sat() as i64, SignedAmount::MAX_MONEY.to_sat());
+fn amount_op_result_sum() {
+    let res = Amount::from_sat(1) + Amount::from_sat(1);
+    let amounts = [res.clone(), res.clone()];
+    let amount_refs = [&res, &res];
+
+    // Sum iterators.
+    let _ = amounts.iter().sum::<NumOpResult<Amount>>();
+    let _ = amount_refs.iter().copied().sum::<NumOpResult<Amount>>();
+    let _ = amount_refs.into_iter().sum::<NumOpResult<Amount>>();
+
+    // FIXME: Should we support this? I don't think so (Tobin).
+    // let _ = amount_refs.iter().sum::<NumOpResult<&Amount>>();
 }