Repository: rust-bitcoin/rust-bitcoin
Pull request #4322 (2025-04-09 13:04:57)

fix: enforce standard minimal push for push_slice
Currently `push_slice` doesn't check the standard minimal push rule, which could result in possible money loss(e.g. if non minimal push is used in output script of p2wsh or p2tr).
Introduce `push_slice_non_minimal` to provide the way to push as now, and change `push_slice` logic to follow standard minimal push rule.

https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/script.cpp#L366

coveralls: ## Pull Request Test Coverage Report for [Build 14357894544](https://coveralls.io/builds/73146199)


### Details

* **19** of **30**   **(63.33%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-0.02%**) to **83.61%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [bitcoin/src/blockdata/script/owned.rs](https://coveralls.io/builds/73146199/source?filename=bitcoin%2Fsrc%2Fblockdata%2Fscript%2Fowned.rs#L43) | 7 | 12 | 58.33%
| [bitcoin/src/blockdata/script/builder.rs](https://coveralls.io/builds/73146199/source?filename=bitcoin%2Fsrc%2Fblockdata%2Fscript%2Fbuilder.rs#L85) | 12 | 18 | 66.67%
<!-- | **Total:** | **19** | **30** | **63.33%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/73146199/badge)](https://coveralls.io/builds/73146199) |
| :-- | --: |
| Change from base [Build 14323978720](https://coveralls.io/builds/73116314): |  -0.02% |
| Covered Lines: | 22251 |
| Relevant Lines: | 26613 |

---
##### 💛  - [Coveralls](https://coveralls.io)

apoelstra: I'm struggling to understand the relevance of BIP62. BIP62 was superceded by BIP66 (for the immediate malleability problems affecting Mt Gox) and segwit (for the general class of malleability vectors). At least, that's how I remember this going down.

If there is a Bitcoin Core policy rule that we're failing to respect, we should cite Core's policy code rather than BIP62.

But concept ACK adding this function -- I'm surprised that we don't have any method right now that does this. I didn't know though that it was *required* to push slices in this form -- using the numeric opcodes rather than the push opcodes. I think it's probably correct that `push_slice` does this and `push_slice_non_minimal` is the second-class method, though it does feel a bit surprising that you can call a method called "push slice" and actually it adds a numeric op.

I'd just like the citation for this behavior to be correct.
ChrisCho-H: There's a `CheckMinimalPush` function in [core](https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/script.cpp#L366), which is used in the [interpreter](https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/interpreter.cpp#L478).  It throws error if we push slice like `push_bytes_1 01` as so far...
I guess some of BIP62 rules(e.g. [minimal push for data and number](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#push-operators)) remains valid because the code above exactly matches what BIP62 specified.
I agree that it's bit awkward to push numeric op instead of slice to conform to minimal push rule, but couldn't come up with other way to do so. 
apoelstra: Your linked code only takes effect if `SCRIPT_VERIFY_MINIMALDATA` is set, which is a standardness flag, not a consensus one.

I think this PR would be good to go if you just replaced BIP62 everywhere with a link to `CheckMinimalPush` in any specific Core commit, and said that standardness rules require push minimality, and that if you want to do nonstandard stuff you need to use the other method.
apoelstra: Can you also remove the reference to bip62 in your commit message?

Patch:
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index eabf863d..aedf19df 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -111,7 +111,6 @@ name = "bitcoin-primitives"
 version = "0.101.0"
 dependencies = [
  "arbitrary",
- "arrayvec",
  "bincode",
  "bitcoin-internals",
  "bitcoin-units",
@@ -214,7 +213,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4afe881d0527571892c4034822e59bb10c6c991cce6abe8199b6f5cf10766f55"
 dependencies = [
  "arrayvec",
- "serde",
 ]
 
 [[package]]
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index 002fdd04..4d452dcd 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -110,7 +110,6 @@ name = "bitcoin-primitives"
 version = "0.101.0"
 dependencies = [
  "arbitrary",
- "arrayvec",
  "bincode",
  "bitcoin-internals",
  "bitcoin-units",
@@ -216,7 +215,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4afe881d0527571892c4034822e59bb10c6c991cce6abe8199b6f5cf10766f55"
 dependencies = [
  "arrayvec",
- "serde",
 ]
 
 [[package]]
diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml
index 60f93fcd..85b7291f 100644
--- a/bitcoin/Cargo.toml
+++ b/bitcoin/Cargo.toml
@@ -31,7 +31,7 @@ hashes = { package = "bitcoin_hashes", path = "../hashes", default-features = fa
 hex = { package = "hex-conservative", version = "0.3.0", default-features = false, features = ["alloc"] }
 internals = { package = "bitcoin-internals", path = "../internals", features = ["alloc", "hex"] }
 io = { package = "bitcoin-io", path = "../io", default-features = false, features = ["alloc", "hashes"] }
-primitives = { package = "bitcoin-primitives", path = "../primitives", default-features = false, features = ["alloc", "hex"] }
+primitives = { package = "bitcoin-primitives", path = "../primitives", default-features = false, features = ["alloc"] }
 secp256k1 = { version = "0.30.0", default-features = false, features = ["hashes", "alloc", "rand"] }
 units = { package = "bitcoin-units", path = "../units", default-features = false, features = ["alloc"] }
 
diff --git a/bitcoin/embedded/Cargo.toml b/bitcoin/embedded/Cargo.toml
index 8a6f7ecb..01fcd890 100644
--- a/bitcoin/embedded/Cargo.toml
+++ b/bitcoin/embedded/Cargo.toml
@@ -10,8 +10,10 @@ version = "0.1.0"
 members = ["."]
 
 [dependencies]
+cortex-m = "0.6.0"
 cortex-m-rt = "0.6.10"
 cortex-m-semihosting = "0.3.3"
+panic-halt = "0.2.0"
 alloc-cortex-m = "0.4.1"
 bitcoin = { path="../", default-features = false, features = ["secp-lowmemory"] }
 
diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs
index 1dde641f..94418f68 100644
--- a/bitcoin/src/address/mod.rs
+++ b/bitcoin/src/address/mod.rs
@@ -135,9 +135,7 @@ impl NetworkValidationUnchecked for super::NetworkUnchecked {}
 
 /// Marker of status of address's network validation. See section [*Parsing addresses*](Address#parsing-addresses)
 /// on [`Address`] for details.
-pub trait NetworkValidation:
-    sealed::NetworkValidation + Sync + Send + Sized + Unpin + Copy
-{
+pub trait NetworkValidation: sealed::NetworkValidation + Sync + Send + Sized + Unpin {
     /// Indicates whether this `NetworkValidation` is `NetworkChecked` or not.
     const IS_CHECKED: bool;
 }
@@ -153,13 +151,13 @@ pub trait NetworkValidationUnchecked:
 
 /// Marker that address's network has been successfully validated. See section [*Parsing addresses*](Address#parsing-addresses)
 /// on [`Address`] for details.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 pub enum NetworkChecked {}
 
 /// Marker that address's network has not yet been validated. See section [*Parsing addresses*](Address#parsing-addresses)
 /// on [`Address`] for details.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 pub enum NetworkUnchecked {}
 
@@ -176,7 +174,7 @@ impl NetworkValidationUnchecked for NetworkUnchecked {}
 ///
 /// This struct represents the inner representation of an address without the network validation
 /// tag, which is used to ensure that addresses are used only on the appropriate network.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 enum AddressInner {
     P2pkh { hash: PubkeyHash, network: NetworkKind },
     P2sh { hash: ScriptHash, network: NetworkKind },
@@ -394,7 +392,7 @@ pub enum AddressData {
     /// * [BIP142 - Address Format for Segregated Witness](https://github.com/bitcoin/bips/blob/master/bip-0142.mediawiki)
     /// * [BIP341 - Taproot: SegWit version 1 spending rules](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)
     /// * [BIP350 - Bech32m format for v1+ witness addresses](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki)
-    #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+    #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
     // The `#[repr(transparent)]` attribute is used to guarantee the layout of the `Address` struct. It
     // is an implementation detail and users should not rely on it in their code.
     pub struct Address<V = NetworkChecked>(PhantomData<V>, AddressInner)
@@ -623,7 +621,7 @@ pub fn address_type(&self) -> Option<AddressType> {
     }
 
     /// Gets the address data from this address.
-    pub fn to_address_data(self) -> AddressData {
+    pub fn to_address_data(&self) -> AddressData {
         use AddressData::*;
 
         match *self.inner() {
@@ -744,7 +742,7 @@ pub fn script_pubkey(&self) -> ScriptBuf {
     /// # })().unwrap();
     /// # assert_eq!(writer, ADDRESS);
     /// ```
-    pub fn to_qr_uri(self) -> String { format!("bitcoin:{:#}", self) }
+    pub fn to_qr_uri(&self) -> String { format!("bitcoin:{:#}", self) }
 
     /// Returns true if the given pubkey is directly related to the address payload.
     ///
@@ -1178,7 +1176,7 @@ struct Test<V: NetworkValidation> {
         let unchecked = addr_str.parse::<Address<_>>().unwrap();
 
         assert_eq!(
-            format!("{:?}", Test { address: unchecked }),
+            format!("{:?}", Test { address: unchecked.clone() }),
             format!("Test {{ address: Address<NetworkUnchecked>({}) }}", addr_str)
         );
 
@@ -1225,7 +1223,7 @@ fn json_serialize() {
 
         let addr =
             "132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM".parse::<Address<_>>().unwrap().assume_checked();
-        let json = serde_json::to_value(addr).unwrap();
+        let json = serde_json::to_value(&addr).unwrap();
         assert_eq!(
             json,
             serde_json::Value::String("132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM".to_owned())
@@ -1239,7 +1237,7 @@ fn json_serialize() {
 
         let addr =
             "33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k".parse::<Address<_>>().unwrap().assume_checked();
-        let json = serde_json::to_value(addr).unwrap();
+        let json = serde_json::to_value(&addr).unwrap();
         assert_eq!(
             json,
             serde_json::Value::String("33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k".to_owned())
@@ -1267,7 +1265,7 @@ fn json_serialize() {
             .parse::<Address<_>>()
             .unwrap()
             .assume_checked();
-        let json = serde_json::to_value(addr).unwrap();
+        let json = serde_json::to_value(&addr).unwrap();
         assert_eq!(
             json,
             serde_json::Value::String(
@@ -1288,7 +1286,7 @@ fn json_serialize() {
             .parse::<Address<_>>()
             .unwrap()
             .assume_checked();
-        let json = serde_json::to_value(addr).unwrap();
+        let json = serde_json::to_value(&addr).unwrap();
         assert_eq!(
             json,
             serde_json::Value::String("bcrt1q2nfxmhd4n3c8834pj72xagvyr9gl57n5r94fsl".to_owned())
@@ -1550,7 +1548,7 @@ struct Foo<V>
         let unchecked = addr_str.parse::<Address<_>>().unwrap();
 
         // Serialize with an unchecked address.
-        let foo_unchecked = Foo { address: unchecked };
+        let foo_unchecked = Foo { address: unchecked.clone() };
         let ser = serde_json::to_string(&foo_unchecked).expect("failed to serialize");
         let rinsed: Foo<NetworkUnchecked> =
             serde_json::from_str(&ser).expect("failed to deserialize");
diff --git a/bitcoin/src/bip152.rs b/bitcoin/src/bip152.rs
index 5aed10c9..b31f900a 100644
--- a/bitcoin/src/bip152.rs
+++ b/bitcoin/src/bip152.rs
@@ -484,7 +484,7 @@ fn compact_block_vector() {
     #[test]
     fn getblocktx_differential_encoding_de_and_serialization() {
         let testcases = vec![
-            // differentially encoded VarInts, indices
+            // differentially encoded VarInts, indicies
             (vec![4, 0, 5, 1, 10], vec![0, 6, 8, 19]),
             (vec![1, 0], vec![0]),
             (vec![5, 0, 0, 0, 0, 0], vec![0, 1, 2, 3, 4]),
diff --git a/bitcoin/src/blockdata/mod.rs b/bitcoin/src/blockdata/mod.rs
index ab3b6746..a6f39ba9 100644
--- a/bitcoin/src/blockdata/mod.rs
+++ b/bitcoin/src/blockdata/mod.rs
@@ -19,7 +19,7 @@
     weight::Weight
 };
 
-/// Implements `FeeRate` and associated features.
+/// Implements `FeeRate` and assoctiated features.
 pub mod fee_rate {
     #[cfg(feature = "serde")]
     pub use units::fee_rate::serde;
diff --git a/bitcoin/src/blockdata/script/borrowed.rs b/bitcoin/src/blockdata/script/borrowed.rs
index 481145e8..c048ba36 100644
--- a/bitcoin/src/blockdata/script/borrowed.rs
+++ b/bitcoin/src/blockdata/script/borrowed.rs
@@ -261,7 +261,7 @@ fn redeem_script(&self) -> Option<&Script> {
         /// Returns the minimum value an output with this script should have in order to be
         /// broadcastable on today’s Bitcoin network.
         #[deprecated(since = "0.32.0", note = "use `minimal_non_dust` etc. instead")]
-        fn dust_value(&self) -> Amount { self.minimal_non_dust() }
+        fn dust_value(&self) -> Option<Amount> { self.minimal_non_dust() }
 
         /// Returns the minimum value an output with this script should have in order to be
         /// broadcastable on today's Bitcoin network.
@@ -272,9 +272,8 @@ fn dust_value(&self) -> Amount { self.minimal_non_dust() }
         /// To use a custom value, use [`minimal_non_dust_custom`].
         ///
         /// [`minimal_non_dust_custom`]: Script::minimal_non_dust_custom
-        fn minimal_non_dust(&self) -> Amount {
+        fn minimal_non_dust(&self) -> Option<Amount> {
             self.minimal_non_dust_internal(DUST_RELAY_TX_FEE.into())
-                .expect("dust_relay_fee or script length should not be absurdly large")
         }
 
         /// Returns the minimum value an output with this script should have in order to be
diff --git a/bitcoin/src/blockdata/script/builder.rs b/bitcoin/src/blockdata/script/builder.rs
index bc22f8e1..aa857e0e 100644
--- a/bitcoin/src/blockdata/script/builder.rs
+++ b/bitcoin/src/blockdata/script/builder.rs
@@ -74,12 +74,36 @@ pub fn push_int_unchecked(self, n: i64) -> Builder {
     pub(in crate::blockdata) fn push_int_non_minimal(self, data: i64) -> Builder {
         let mut buf = [0u8; 8];
         let len = write_scriptint(&mut buf, data);
-        self.push_slice(&<&PushBytes>::from(&buf)[..len])
+        self.push_slice_non_minimal(&<&PushBytes>::from(&buf)[..len])
     }
 
     /// Adds instructions to push some arbitrary data onto the stack.
-    pub fn push_slice<T: AsRef<PushBytes>>(mut self, data: T) -> Builder {
-        self.0.push_slice(data);
+    pub fn push_slice<T: AsRef<PushBytes>>(self, data: T) -> Builder {
+        let bytes = data.as_ref().as_bytes();
+        if bytes.len() == 1 && (bytes[0] == 0x81 || bytes[0] <= 16) {
+            match bytes[0] {
+                0x81 => self.push_opcode(OP_PUSHNUM_NEG1),
+                0 => self.push_opcode(OP_PUSHBYTES_0),
+                1..=16 => {
+                    self.push_opcode(Opcode::from(bytes[0] + (OP_PUSHNUM_1.to_u8() - 1)))
+                }
+                _ => self, // unreachable arm
+            }
+        } else {
+            self.push_slice_non_minimal(data.as_ref())
+        }
+    }
+
+    /// Adds instructions to push some arbitrary data onto the stack without minimality.
+    ///
+    /// Standardness rules require push minimality according to [CheckMinimalPush] of core.
+    ///
+    /// [CheckMinimalPush]: <https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/script.cpp#L366>
+    pub fn push_slice_non_minimal<T: AsRef<PushBytes>>(
+        mut self,
+        data: T,
+    ) -> Builder {
+        self.0.push_slice_non_minimal(data);
         self.1 = None;
         self
     }
@@ -129,7 +153,7 @@ pub fn push_relative_lock_time(self, lock_time: relative::LockTime) -> Builder {
     ///
     /// # Deprecated
     /// This method is deprecated in favor of `push_relative_lock_time`.
-    ///
+    /// 
     /// In Bitcoin script semantics, when using CHECKSEQUENCEVERIFY, you typically
     /// want to push a relative locktime value to be compared against the input's
     /// sequence number, not the sequence number itself.
diff --git a/bitcoin/src/blockdata/script/owned.rs b/bitcoin/src/blockdata/script/owned.rs
index 8b8b73e3..7d4c9b97 100644
--- a/bitcoin/src/blockdata/script/owned.rs
+++ b/bitcoin/src/blockdata/script/owned.rs
@@ -37,6 +37,25 @@ fn from_hex(s: &str) -> Result<ScriptBuf, hex::HexToBytesError> {
 
         /// Adds instructions to push some arbitrary data onto the stack.
         fn push_slice<T: AsRef<PushBytes>>(&mut self, data: T) {
+            let bytes = data.as_ref().as_bytes();
+            if bytes.len() == 1 && (bytes[0] == 0x81 || bytes[0] <= 16) {
+                match bytes[0] {
+                    0x81 => { self.push_opcode(OP_PUSHNUM_NEG1); },
+                    0 => { self.push_opcode(OP_PUSHBYTES_0); },
+                    1..=16 => { self.push_opcode(Opcode::from(bytes[0] + (OP_PUSHNUM_1.to_u8() - 1))); },
+                    _ => {}, // unreachable arm
+                }
+            } else {
+                self.push_slice_non_minimal(data);
+            }
+        }
+
+        /// Adds instructions to push some arbitrary data onto the stack without minimality.
+        ///
+        /// Standardness rules require push minimality according to [CheckMinimalPush] of core.
+        ///
+        /// [CheckMinimalPush]: <https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/script.cpp#L366>
+        fn push_slice_non_minimal<T: AsRef<PushBytes>>(&mut self, data: T) {
             let data = data.as_ref();
             self.reserve(ScriptBuf::reserved_len_for_slice(data.len()));
             self.push_slice_no_opt(data);
diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index bdb7a383..341fba10 100644
--- a/bitcoin/src/blockdata/transaction.rs
+++ b/bitcoin/src/blockdata/transaction.rs
@@ -183,8 +183,8 @@ fn size(&self) -> usize { size_from_script_pubkey(&self.script_pubkey) }
         /// To use a custom value, use [`minimal_non_dust_custom`].
         ///
         /// [`minimal_non_dust_custom`]: TxOut::minimal_non_dust_custom
-        fn minimal_non_dust(script_pubkey: ScriptBuf) -> TxOut {
-            TxOut { value: script_pubkey.minimal_non_dust(), script_pubkey }
+        fn minimal_non_dust(script_pubkey: ScriptBuf) -> Option<TxOut> {
+            Some(TxOut { value: script_pubkey.minimal_non_dust()?, script_pubkey })
         }
 
         /// Constructs a new `TxOut` with given script and the smallest possible `value` that is **not** dust
diff --git a/bitcoin/src/p2p/message.rs b/bitcoin/src/p2p/message.rs
index a78bf9c7..100142bc 100644
--- a/bitcoin/src/p2p/message.rs
+++ b/bitcoin/src/p2p/message.rs
@@ -27,7 +27,7 @@
 pub const MAX_INV_SIZE: usize = 50_000;
 
 /// Maximum size, in bytes, of an encoded message
-/// This by necessity should be larger than `MAX_VEC_SIZE`
+/// This by necessity should be larger tham `MAX_VEC_SIZE`
 pub const MAX_MSG_SIZE: usize = 5_000_000;
 
 /// Serializer for command string
diff --git a/hashes/Cargo.toml b/hashes/Cargo.toml
index 99caab0b..f1d67003 100644
--- a/hashes/Cargo.toml
+++ b/hashes/Cargo.toml
@@ -24,7 +24,7 @@ small-hash = []
 [dependencies]
 internals = { package = "bitcoin-internals", path = "../internals" }
 hex = { package = "hex-conservative", version = "0.3.0", default-features = false, optional = true }
-serde = { version = "1.0.103", default-features = false, optional = true }
+serde = { version = "1.0", default-features = false, optional = true }
 
 [dev-dependencies]
 serde_test = "1.0"
diff --git a/hashes/embedded/Cargo.toml b/hashes/embedded/Cargo.toml
index 531c98b6..f6779e42 100644
--- a/hashes/embedded/Cargo.toml
+++ b/hashes/embedded/Cargo.toml
@@ -14,6 +14,7 @@ alloc = ["alloc-cortex-m", "bitcoin_hashes/alloc"]
 hex = ["bitcoin_hashes/hex"]
 
 [dependencies]
+cortex-m = "0.6.0"
 cortex-m-rt = "0.6.10"
 cortex-m-semihosting = "0.3.3"
 panic-halt = "0.2.0"
diff --git a/internals/src/array.rs b/internals/src/array.rs
index 4142212b..d5c63962 100644
--- a/internals/src/array.rs
+++ b/internals/src/array.rs
@@ -30,7 +30,7 @@ fn get_static<const INDEX: usize>(&self) -> &Self::Item { &self.sub_array::<INDE
     /// the places where useless checks are made.
     fn first(&self) -> &Self::Item { self.get_static::<0>() }
 
-    /// Splits the array into two, non-overlapping smaller arrays covering the entire range.
+    /// Splits the array into two, non-overlaping smaller arrays covering the entire range.
     ///
     /// This is almost equivalent to just calling [`sub_array`](Self::sub_array) twice, except it also
     /// checks that the arrays don't overlap and that they cover the full range. This is very useful
diff --git a/nightly-version b/nightly-version
index 0fd7fce9..4312103f 100644
--- a/nightly-version
+++ b/nightly-version
@@ -1,1 +1,1 @@
-nightly-2025-04-11
+nightly-2025-04-04
diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml
index 7af43f83..6a565954 100644
--- a/primitives/Cargo.toml
+++ b/primitives/Cargo.toml
@@ -15,21 +15,19 @@ rust-version = "1.63.0"
 exclude = ["tests", "contrib"]
 
 [features]
-default = ["std", "hex"]
-std = ["alloc", "hashes/std", "hex?/std", "internals/std", "units/std", "arrayvec/std"]
-alloc = ["hashes/alloc", "hex?/alloc", "internals/alloc", "units/alloc"]
-serde = ["dep:serde", "hashes/serde", "hex?/serde", "internals/serde", "units/serde", "alloc", "hex"]
+default = ["std"]
+std = ["alloc", "hashes/std", "hex/std", "internals/std", "units/std"]
+alloc = ["hashes/alloc", "hex/alloc", "internals/alloc", "units/alloc"]
+serde = ["dep:serde", "hashes/serde", "internals/serde", "units/serde", "alloc"]
 arbitrary = ["dep:arbitrary", "units/arbitrary"]
-hex = ["dep:hex", "hashes/hex", "internals/hex"]
 
 [dependencies]
-hashes = { package = "bitcoin_hashes", path = "../hashes", default-features = false }
-internals = { package = "bitcoin-internals", path = "../internals" }
+hashes = { package = "bitcoin_hashes", path = "../hashes", default-features = false, features = ["hex"] }
+hex = { package = "hex-conservative", version = "0.3.0", default-features = false }
+internals = { package = "bitcoin-internals", path = "../internals", features = ["hex"] }
 units = { package = "bitcoin-units", path = "../units", default-features = false }
-arrayvec = { version = "0.7", default-features = false }
 
 arbitrary = { version = "1.4", optional = true }
-hex = { package = "hex-conservative", version = "0.3.0", default-features = false, optional = true }
 serde = { version = "1.0.103", default-features = false, features = ["derive", "alloc"], optional = true }
 
 [dev-dependencies]
diff --git a/primitives/src/block.rs b/primitives/src/block.rs
index f746f4db..a4d49b0a 100644
--- a/primitives/src/block.rs
+++ b/primitives/src/block.rs
@@ -207,27 +207,6 @@ pub fn block_hash(&self) -> BlockHash {
     }
 }
 
-#[cfg(feature = "hex")]
-impl fmt::Display for Header {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use fmt::Write as _;
-        use hex::DisplayHex as _;
-
-        let mut buf = arrayvec::ArrayString::<160>::new();
-        write!(
-            &mut buf,
-            "{}{}{}{}{}{}",
-            self.version.to_consensus().to_le_bytes().as_hex(),
-            self.prev_blockhash.as_byte_array().as_hex(),
-            self.merkle_root.as_byte_array().as_hex(),
-            self.time.to_u32().to_le_bytes().as_hex(),
-            self.bits.to_consensus().to_le_bytes().as_hex(),
-            self.nonce.to_le_bytes().as_hex(),
-        )?;
-        fmt::Display::fmt(&buf, f)
-    }
-}
-
 impl fmt::Debug for Header {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         f.debug_struct("Header")
@@ -331,10 +310,7 @@ fn default() -> Version { Self::NO_SOFT_FORK_SIGNALLING }
     pub struct WitnessCommitment(sha256d::Hash);
 }
 
-#[cfg(feature = "hex")]
 hashes::impl_hex_for_newtype!(BlockHash, WitnessCommitment);
-#[cfg(not(feature = "hex"))]
-hashes::impl_debug_only_for_newtype!(BlockHash, WitnessCommitment);
 #[cfg(feature = "serde")]
 hashes::impl_serde_for_newtype!(BlockHash, WitnessCommitment);
 
@@ -556,35 +532,4 @@ fn header_debug() {
         );
         assert_eq!(format!("{:?}", header), expected);
     }
-
-    #[test]
-    #[cfg(feature = "hex")]
-    fn header_display() {
-        let seconds: u32 = 1_653_195_600; // Arbitrary timestamp: May 22nd, 5am UTC.
-
-        let header = Header {
-            version: Version::TWO,
-            prev_blockhash: BlockHash::from_byte_array([0xab; 32]),
-            merkle_root: TxMerkleNode::from_byte_array([0xcd; 32]),
-            time: BlockTime::from(seconds),
-            bits: CompactTarget::from_consensus(0xbeef),
-            nonce: 0xcafe,
-        };
-
-        let want = concat!(
-            "02000000",                                                         // version
-            "abababababababababababababababababababababababababababababababab", // prev_blockhash
-            "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", // merkle_root
-            "50c38962",                                                         // time
-            "efbe0000",                                                         // bits
-            "feca0000",                                                         // nonce
-        );
-        assert_eq!(want.len(), 160);
-        assert_eq!(format!("{}", header), want);
-
-        // Check how formatting options are handled.
-        let want = format!("{:.20}", want);
-        let got = format!("{:.20}", header);
-        assert_eq!(got, want);
-    }
 }
diff --git a/primitives/src/merkle_tree.rs b/primitives/src/merkle_tree.rs
index e5cc6010..18a16a5a 100644
--- a/primitives/src/merkle_tree.rs
+++ b/primitives/src/merkle_tree.rs
@@ -11,9 +11,6 @@
     pub struct WitnessMerkleNode(sha256d::Hash);
 }
 
-#[cfg(feature = "hex")]
 hashes::impl_hex_for_newtype!(TxMerkleNode, WitnessMerkleNode);
-#[cfg(not(feature = "hex"))]
-hashes::impl_debug_only_for_newtype!(TxMerkleNode, WitnessMerkleNode);
 #[cfg(feature = "serde")]
 hashes::impl_serde_for_newtype!(TxMerkleNode, WitnessMerkleNode);
diff --git a/primitives/src/script/borrowed.rs b/primitives/src/script/borrowed.rs
index c7269e33..4f3bca28 100644
--- a/primitives/src/script/borrowed.rs
+++ b/primitives/src/script/borrowed.rs
@@ -90,20 +90,14 @@ impl Script {
     pub const fn new() -> &'static Self { Self::from_bytes(&[]) }
 
     /// Returns the script data as a byte slice.
-    ///
-    /// This is just the script bytes **not** consensus encoding (which includes a length prefix).
-   #[inline]
+    #[inline]
     pub const fn as_bytes(&self) -> &[u8] { &self.0 }
 
     /// Returns the script data as a mutable byte slice.
-    ///
-    /// This is just the script bytes **not** consensus encoding (which includes a length prefix).
     #[inline]
     pub fn as_mut_bytes(&mut self) -> &mut [u8] { &mut self.0 }
 
     /// Returns a copy of the script data.
-    ///
-    /// This is just the script bytes **not** consensus encoding (which includes a length prefix).
     #[inline]
     pub fn to_vec(&self) -> Vec<u8> { self.as_bytes().to_owned() }
 
@@ -201,7 +195,7 @@ fn script_len() {
 
     #[test]
     fn script_is_empty() {
-        let script: &Script = Default::default();
+        let script = Script::new();
         assert!(script.is_empty());
 
         let script = Script::from_bytes(&[1, 2, 3]);
diff --git a/primitives/src/script/mod.rs b/primitives/src/script/mod.rs
index 3a0244c5..af9c4b42 100644
--- a/primitives/src/script/mod.rs
+++ b/primitives/src/script/mod.rs
@@ -10,7 +10,6 @@
 use core::fmt;
 
 use hashes::{hash160, sha256};
-#[cfg(feature = "hex")]
 use hex::DisplayHex;
 use internals::script::{self, PushDataLenLen};
 
@@ -50,10 +49,7 @@
     pub struct WScriptHash(sha256::Hash);
 }
 
-#[cfg(feature = "hex")]
 hashes::impl_hex_for_newtype!(ScriptHash, WScriptHash);
-#[cfg(not(feature = "hex"))]
-hashes::impl_debug_only_for_newtype!(ScriptHash, WScriptHash);
 #[cfg(feature = "serde")]
 hashes::impl_serde_for_newtype!(ScriptHash, WScriptHash);
 
@@ -429,7 +425,6 @@ impl fmt::Display for ScriptBuf {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(self.as_script(), f) }
 }
 
-#[cfg(feature = "hex")]
 impl fmt::LowerHex for Script {
     #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -437,19 +432,15 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     }
 }
 #[cfg(feature = "alloc")]
-#[cfg(feature = "hex")]
 internals::impl_to_hex_from_lower_hex!(Script, |script: &Self| script.len() * 2);
 
-#[cfg(feature = "hex")]
 impl fmt::LowerHex for ScriptBuf {
     #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(self.as_script(), f) }
 }
 #[cfg(feature = "alloc")]
-#[cfg(feature = "hex")]
 internals::impl_to_hex_from_lower_hex!(ScriptBuf, |script_buf: &Self| script_buf.len() * 2);
 
-#[cfg(feature = "hex")]
 impl fmt::UpperHex for Script {
     #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -457,7 +448,6 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     }
 }
 
-#[cfg(feature = "hex")]
 impl fmt::UpperHex for ScriptBuf {
     #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::UpperHex::fmt(self.as_script(), f) }
@@ -717,23 +707,6 @@ fn try_from_scriptbuf_for_scripthash() {
         assert!(ScriptHash::try_from(script).is_err());
     }
 
-    #[test]
-    fn try_from_scriptbuf_ref_for_scripthash() {
-        let script = ScriptBuf::from(vec![0x51; 520]);
-        assert!(ScriptHash::try_from(&script).is_ok());
-
-        let script = ScriptBuf::from(vec![0x51; 521]);
-        assert!(ScriptHash::try_from(&script).is_err());
-    }
-    #[test]
-    fn try_from_script_for_scripthash() {
-        let script = Script::from_bytes(&[0x51; 520]);
-        assert!(ScriptHash::try_from(script).is_ok());
-
-        let script = Script::from_bytes(&[0x51; 521]);
-        assert!(ScriptHash::try_from(script).is_err());
-    }
-
     #[test]
     fn try_from_scriptbuf_for_wscript_hash() {
         let script = ScriptBuf::from(vec![0x51; 10_000]);
@@ -743,47 +716,20 @@ fn try_from_scriptbuf_for_wscript_hash() {
         assert!(WScriptHash::try_from(script).is_err());
     }
 
-    #[test]
-    fn try_from_scriptbuf_ref_for_wscript_hash() {
-        let script = ScriptBuf::from(vec![0x51; 10_000]);
-        assert!(WScriptHash::try_from(&script).is_ok());
-
-        let script = ScriptBuf::from(vec![0x51; 10_001]);
-        assert!(WScriptHash::try_from(&script).is_err());
-    }
-
-
-    #[test]
-    fn try_from_script_for_wscript_hash() {
-        let script = Script::from_bytes(&[0x51; 10_000]);
-        assert!(WScriptHash::try_from(script).is_ok());
-
-        let script = Script::from_bytes(&[0x51; 10_001]);
-        assert!(WScriptHash::try_from(script).is_err());
-    }
-
     #[test]
     fn script_display() {
         let script = Script::from_bytes(&[0xa1, 0xb2, 0xc3]);
         assert_eq!(format!("{}", script), "OP_LESSTHANOREQUAL OP_CSV OP_RETURN_195");
-
-        #[cfg(feature = "hex")]
-        {
-            assert_eq!(format!("{:x}", script), "a1b2c3");
-            assert_eq!(format!("{:X}", script), "A1B2C3");
-        }
+        assert_eq!(format!("{:x}", script), "a1b2c3");
+        assert_eq!(format!("{:X}", script), "A1B2C3");
     }
 
     #[test]
     fn scriptbuf_display() {
         let script_buf = ScriptBuf::from(vec![0xa1, 0xb2, 0xc3]);
         assert_eq!(format!("{}", script_buf), "OP_LESSTHANOREQUAL OP_CSV OP_RETURN_195");
-
-        #[cfg(feature = "hex")]
-        {
-            assert_eq!(format!("{:x}", script_buf), "a1b2c3");
-            assert_eq!(format!("{:X}", script_buf), "A1B2C3");
-        }
+        assert_eq!(format!("{:x}", script_buf), "a1b2c3");
+        assert_eq!(format!("{:X}", script_buf), "A1B2C3");
     }
 
     #[test]
diff --git a/primitives/src/script/owned.rs b/primitives/src/script/owned.rs
index 1732845e..27596365 100644
--- a/primitives/src/script/owned.rs
+++ b/primitives/src/script/owned.rs
@@ -27,8 +27,7 @@ pub const fn new() -> Self { Self::from_bytes(Vec::new()) }
 
     /// Converts byte vector into script.
     ///
-    /// This method doesn't (re)allocate. `bytes` is just the script bytes **not** consensus
-    /// encoding (i.e no length prefix).
+    /// This method doesn't (re)allocate.
     #[inline]
     pub const fn from_bytes(bytes: Vec<u8>) -> Self { Self(bytes) }
 
@@ -43,10 +42,6 @@ pub fn as_mut_script(&mut self) -> &mut Script { Script::from_bytes_mut(&mut sel
     /// Converts the script into a byte vector.
     ///
     /// This method doesn't (re)allocate.
-    ///
-    /// # Returns
-    ///
-    /// Just the script bytes **not** consensus encoding (which includes a length prefix).
     #[inline]
     pub fn into_bytes(self) -> Vec<u8> { self.0 }
 
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index 53cd51a4..1c79a187 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -13,7 +13,6 @@
 #[cfg(feature = "alloc")]
 use core::cmp;
 #[cfg(feature = "alloc")]
-#[cfg(feature = "hex")]
 use core::convert::Infallible;
 use core::fmt;
 
@@ -21,13 +20,9 @@
 use arbitrary::{Arbitrary, Unstructured};
 use hashes::sha256d;
 #[cfg(feature = "alloc")]
-use internals::compact_size;
-#[cfg(feature = "hex")]
-use internals::write_err;
-#[cfg(feature = "hex")]
-use units::parse;
+use internals::{compact_size, write_err};
 #[cfg(feature = "alloc")]
-use units::{Amount, Weight};
+use units::{parse, Amount, Weight};
 
 #[cfg(feature = "alloc")]
 use crate::locktime::absolute;
@@ -394,7 +389,6 @@ impl OutPoint {
     pub const COINBASE_PREVOUT: Self = Self { txid: Txid::COINBASE_PREVOUT, vout: u32::MAX };
 }
 
-#[cfg(feature = "hex")]
 impl fmt::Display for OutPoint {
     #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -403,7 +397,6 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 }
 
 #[cfg(feature = "alloc")]
-#[cfg(feature = "hex")]
 impl core::str::FromStr for OutPoint {
     type Err = ParseOutPointError;
 
@@ -431,7 +424,6 @@ fn from_str(s: &str) -> Result<Self, Self::Err> {
 ///
 /// Does not permit leading zeroes or non-digit characters.
 #[cfg(feature = "alloc")]
-#[cfg(feature = "hex")]
 fn parse_vout(s: &str) -> Result<u32, ParseOutPointError> {
     if s.len() > 1 {
         let first = s.chars().next().unwrap();
@@ -446,7 +438,6 @@ fn parse_vout(s: &str) -> Result<u32, ParseOutPointError> {
 #[derive(Debug, Clone, PartialEq, Eq)]
 #[non_exhaustive]
 #[cfg(feature = "alloc")]
-#[cfg(feature = "hex")]
 pub enum ParseOutPointError {
     /// Error in TXID part.
     Txid(hex::HexToArrayError),
@@ -461,14 +452,12 @@ pub enum ParseOutPointError {
 }
 
 #[cfg(feature = "alloc")]
-#[cfg(feature = "hex")]
 impl From<Infallible> for ParseOutPointError {
     #[inline]
     fn from(never: Infallible) -> Self { match never {} }
 }
 
 #[cfg(feature = "alloc")]
-#[cfg(feature = "hex")]
 impl fmt::Display for ParseOutPointError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use ParseOutPointError as E;
@@ -484,7 +473,6 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 }
 
 #[cfg(feature = "std")]
-#[cfg(feature = "hex")]
 impl std::error::Error for ParseOutPointError {
     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         use ParseOutPointError as E;
@@ -512,10 +500,7 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
     pub struct Wtxid(sha256d::Hash);
 }
 
-#[cfg(feature = "hex")]
 hashes::impl_hex_for_newtype!(Txid, Wtxid);
-#[cfg(not(feature = "hex"))]
-hashes::impl_debug_only_for_newtype!(Txid, Wtxid);
 #[cfg(feature = "serde")]
 hashes::impl_serde_for_newtype!(Txid, Wtxid);
 
@@ -714,7 +699,6 @@ fn transaction_functions() {
     }
 
     #[test]
-    #[cfg(feature = "hex")]
     fn outpoint_from_str() {
         // Check format errors
         let mut outpoint_str = "0".repeat(64); // No ":"
@@ -741,7 +725,6 @@ fn outpoint_from_str() {
     }
 
     #[test]
-    #[cfg(feature = "hex")]
     fn canonical_vout() {
         assert_eq!(parse_vout("0").unwrap(), 0);
         assert_eq!(parse_vout("1").unwrap(), 1);
diff --git a/primitives/src/witness.rs b/primitives/src/witness.rs
index 3eb62a02..159a80c1 100644
--- a/primitives/src/witness.rs
+++ b/primitives/src/witness.rs
@@ -9,6 +9,7 @@
 
 #[cfg(feature = "arbitrary")]
 use arbitrary::{Arbitrary, Unstructured};
+use hex::DisplayHex;
 use internals::compact_size;
 use internals::slice::SliceExt;
 use internals::wrap_debug::WrapDebug;
@@ -313,7 +314,7 @@ fn eq(&self, rhs: &Witness) -> bool { *rhs == *self }
 /// Debug implementation that displays the witness as a structured output containing:
 /// - Number of witness elements
 /// - Total bytes across all elements
-/// - List of hex-encoded witness elements if `hex` features is enabled.
+/// - List of hex-encoded witness elements
 #[allow(clippy::missing_fields_in_debug)] // We don't want to show `indices_start`.
 impl fmt::Debug for Witness {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -325,14 +326,7 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
             .field(
                 "elements",
                 &WrapDebug(|f| {
-                    #[cfg(feature = "hex")]
-                    {
-                        f.debug_list().entries(self.iter().map(hex::DisplayHex::as_hex)).finish()
-                    }
-                    #[cfg(not(feature = "hex"))]
-                    {
-                        f.debug_list().entries(self.iter()).finish()
-                    }
+                    f.debug_list().entries(self.iter().map(DisplayHex::as_hex)).finish()
                 }),
             )
             .finish()
@@ -698,98 +692,23 @@ fn witness_from_impl() {
         assert!(expected.is_empty());
     }
 
-    #[test]
-    fn witness_from_array_impl() {
-        const DATA_1: [u8; 3] = [1, 2, 3];
-        const DATA_2: [u8; 3] = [4, 5, 6];
-        let witness = Witness::from_slice(&[DATA_1, DATA_2]);
-
-        let witness_from_array_ref = Witness::from(&[DATA_1, DATA_2]);
-        let witness_from_array_of_refs = Witness::from([&DATA_1, &DATA_2]);
-        let witness_from_ref_to_array_of_refs = Witness::from(&[&DATA_1, &DATA_2]);
-        let witness_from_fixed_array = Witness::from([DATA_1, DATA_2]);
-        let witness_from_slice_of_refs = Witness::from(&[&DATA_1, &DATA_2][..]);
-        let witness_from_nested_array = Witness::from(&[DATA_1, DATA_2][..]);
-
-        assert_eq!(witness_from_array_ref, witness);
-        assert_eq!(witness_from_array_of_refs, witness);
-        assert_eq!(witness_from_ref_to_array_of_refs, witness);
-        assert_eq!(witness_from_fixed_array, witness);
-        assert_eq!(witness_from_slice_of_refs, witness);
-        assert_eq!(witness_from_nested_array, witness);
-    }
-
     #[test]
     fn partial_eq() {
         const EMPTY_BYTES: &[u8] = &[];
-        const DATA_1: &[u8] = &[42];
-        const DATA_2: &[u8] = &[42, 21];
-
+        assert_eq!(Vec::<&[u8]>::new(), Witness::new());
         macro_rules! ck {
-            ($witness:expr, $container:expr, $different:expr) => {{
-                let witness = $witness;
+            ($container:expr) => {{
                 let container = $container;
-                let different = $different;
-
+                let witness = Witness::from(Clone::clone(&container));
                 assert_eq!(witness, container, stringify!($container));
-                assert_eq!(container, witness, stringify!($container));
-
-                assert_ne!(witness, different, stringify!($container));
-                assert_ne!(different, witness, stringify!($container));
             }};
         }
-
-        let witness = Witness::from_slice(&[DATA_1, DATA_2]);
-
-        // &[T]
-        let container: &[&[u8]] = &[EMPTY_BYTES];
-        let different: &[&[u8]] = &[DATA_1];
-        ck!(Witness::from(container), container, different);
-
-        let container: &[&[u8]] = &[DATA_1];
-        let different: &[&[u8]] = &[DATA_2];
-        ck!(Witness::from(container), container, different);
-
-        // &[T; N]
-        let container: &[&[u8]; 2] = &[DATA_1, DATA_2];
-        let different: &[&[u8]; 2] = &[DATA_2, DATA_1];
-        ck!(Witness::from(container), container, different);
-
-        // [&[T]; N]
-        let container: [&[u8]; 2] = [DATA_1, DATA_2];
-        let different: [&[u8]; 2] = [DATA_2, DATA_1];
-        ck!(Witness::from(container), container, different);
-
-        // Vec<T>
-        let container: Vec<&[u8]> = vec![DATA_1, DATA_2];
-        let different: Vec<&[u8]> = vec![DATA_2, DATA_1];
-        ck!(witness.clone(), container, different);
-
-        // Box<[T]>
-        let container: Box<[&[u8]]> = vec![DATA_1, DATA_2].into_boxed_slice();
-        let different: Box<[&[u8]]> = vec![DATA_2, DATA_1].into_boxed_slice();
-        ck!(witness.clone(), container, different);
-
-        // Rc<[T]>
-        let container: alloc::rc::Rc<[&[u8]]> = vec![DATA_1, DATA_2].into();
-        let different: alloc::rc::Rc<[&[u8]]> = vec![DATA_2, DATA_1].into();
-        ck!(witness.clone(), container, different);
-
-        // Arc<[T]>
-        let container: alloc::sync::Arc<[&[u8]]> = vec![DATA_1, DATA_2].into();
-        let different: alloc::sync::Arc<[&[u8]]> = vec![DATA_2, DATA_1].into();
-        ck!(witness, container, different);
-    }
-
-    #[test]
-    fn partial_eq_for_slice() {
-        let witness = Witness::from_slice(&[vec![1, 2, 3], vec![4, 5, 6]]);
-        let container: &[Vec<u8>] = &[vec![1, 2, 3], vec![4, 5, 6]];
-        let different: &[Vec<u8>] = &[vec![1, 2], vec![4, 5]];
-
-        // Explicitly dereference the slice to invoke the `[T]` implementation.
-        assert_eq!(*container, witness);
-        assert_ne!(*different, witness);
+        ck!([EMPTY_BYTES]);
+        ck!([EMPTY_BYTES, EMPTY_BYTES]);
+        ck!([[42]]);
+        ck!([[42, 21]]);
+        ck!([&[42], EMPTY_BYTES]);
+        ck!([[42u8], [21]]);
     }
 
     #[test]
diff --git a/units/src/amount/error.rs b/units/src/amount/error.rs
index dd06db04..2c91392b 100644
--- a/units/src/amount/error.rs
+++ b/units/src/amount/error.rs
@@ -10,7 +10,7 @@
 
 use super::INPUT_STRING_LEN_LIMIT;
 
-/// Error returned when parsing an amount with denomination fails.
+/// An error during amount parsing amount with denomination.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct ParseError(pub(crate) ParseErrorInner);
 
@@ -84,7 +84,7 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
     }
 }
 
-/// Error returned when parsing an amount fails.
+/// An error during amount parsing.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct ParseAmountError(pub(crate) ParseAmountErrorInner);
 
diff --git a/units/src/amount/mod.rs b/units/src/amount/mod.rs
index 9a4f715b..8928cea9 100644
--- a/units/src/amount/mod.rs
+++ b/units/src/amount/mod.rs
@@ -35,9 +35,11 @@
         OutOfRangeError, ParseAmountError, ParseDenominationError, ParseError,
         PossiblyConfusingDenominationError, TooPreciseError, UnknownDenominationError,
     },
+    result::{NumOpError, NumOpResult},
     signed::SignedAmount,
     unsigned::Amount,
 };
+pub(crate) use self::result::OptionExt;
 
 /// A set of denominations in which amounts can be expressed.
 ///
@@ -605,7 +607,9 @@ impl<T> CheckedSum<Amount> for T
     T: Iterator<Item = Amount>,
 {
     fn checked_sum(mut self) -> Option<Amount> {
-        self.try_fold(Amount::ZERO, Amount::checked_add)
+        let first = Some(self.next().unwrap_or_default());
+
+        self.fold(first, |acc, item| acc.and_then(|acc| acc.checked_add(item)))
     }
 }
 
@@ -614,7 +618,9 @@ impl<T> CheckedSum<SignedAmount> for T
     T: Iterator<Item = SignedAmount>,
 {
     fn checked_sum(mut self) -> Option<SignedAmount> {
-        self.try_fold(SignedAmount::ZERO, SignedAmount::checked_add)
+        let first = Some(self.next().unwrap_or_default());
+
+        self.fold(first, |acc, item| acc.and_then(|acc| acc.checked_add(item)))
     }
 }
 
diff --git a/units/src/amount/result.rs b/units/src/amount/result.rs
index 614e55bd..ab06f207 100644
--- a/units/src/amount/result.rs
+++ b/units/src/amount/result.rs
@@ -1,13 +1,112 @@
 // SPDX-License-Identifier: CC0-1.0
 
-//! Provides a monodic type returned by mathematical operations (`core::ops`).
+//! Provides a monodic type used when mathematical operations (`core::ops`) return an amount type.
 
-use core::ops;
+use core::{fmt, ops};
 
 use NumOpResult as R;
 
 use super::{Amount, SignedAmount};
-use crate::{MathOp, NumOpError, NumOpResult, OptionExt};
+
+/// Result of an operation on [`Amount`] or [`SignedAmount`].
+///
+/// The type parameter `T` should be normally `Amount` or `SignedAmount`.
+#[derive(Debug, Copy, 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`.
+    #[inline]
+    #[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`.
+    #[inline]
+    #[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.
+    #[inline]
+    #[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>`.
+    #[inline]
+    pub fn ok(self) -> Option<T> {
+        match self {
+            R::Valid(amount) => Some(amount),
+            R::Error(_) => None,
+        }
+    }
+
+    /// Converts this `NumOpResult` to a `Result<T, NumOpError>`.
+    #[inline]
+    #[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`.
+    #[inline]
+    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.
+    #[inline]
+    pub fn is_valid(&self) -> bool {
+        match self {
+            R::Valid(_) => true,
+            R::Error(_) => false,
+        }
+    }
+
+    /// Returns `true` if the numeric result is an invalid amount.
+    #[inline]
+    pub fn is_error(&self) -> bool { !self.is_valid() }
+}
 
 impl From<Amount> for NumOpResult<Amount> {
     fn from(a: Amount) -> Self { Self::Valid(a) }
@@ -27,7 +126,7 @@ fn from(a: &SignedAmount) -> Self { Self::Valid(*a) }
     impl ops::Add<Amount> for Amount {
         type Output = NumOpResult<Amount>;
 
-        fn add(self, rhs: Amount) -> Self::Output { self.checked_add(rhs).valid_or_error(MathOp::Add) }
+        fn add(self, rhs: Amount) -> Self::Output { self.checked_add(rhs).valid_or_error() }
     }
     impl ops::Add<NumOpResult<Amount>> for Amount {
         type Output = NumOpResult<Amount>;
@@ -38,7 +137,7 @@ fn add(self, rhs: NumOpResult<Amount>) -> Self::Output { rhs.and_then(|a| a + se
     impl ops::Sub<Amount> for Amount {
         type Output = NumOpResult<Amount>;
 
-        fn sub(self, rhs: Amount) -> Self::Output { self.checked_sub(rhs).valid_or_error(MathOp::Sub) }
+        fn sub(self, rhs: Amount) -> Self::Output { self.checked_sub(rhs).valid_or_error() }
     }
     impl ops::Sub<NumOpResult<Amount>> for Amount {
         type Output = NumOpResult<Amount>;
@@ -54,7 +153,7 @@ fn sub(self, rhs: NumOpResult<Amount>) -> Self::Output {
     impl ops::Mul<u64> for Amount {
         type Output = NumOpResult<Amount>;
 
-        fn mul(self, rhs: u64) -> Self::Output { self.checked_mul(rhs).valid_or_error(MathOp::Mul) }
+        fn mul(self, rhs: u64) -> Self::Output { self.checked_mul(rhs).valid_or_error() }
     }
     impl ops::Mul<u64> for NumOpResult<Amount> {
         type Output = NumOpResult<Amount>;
@@ -64,7 +163,7 @@ fn mul(self, rhs: u64) -> Self::Output { self.and_then(|lhs| lhs * rhs) }
     impl ops::Mul<Amount> for u64 {
         type Output = NumOpResult<Amount>;
 
-        fn mul(self, rhs: Amount) -> Self::Output { rhs.checked_mul(self).valid_or_error(MathOp::Mul) }
+        fn mul(self, rhs: Amount) -> Self::Output { rhs.checked_mul(self).valid_or_error() }
     }
     impl ops::Mul<NumOpResult<Amount>> for u64 {
         type Output = NumOpResult<Amount>;
@@ -75,7 +174,7 @@ fn mul(self, rhs: NumOpResult<Amount>) -> Self::Output { rhs.and_then(|rhs| self
     impl ops::Div<u64> for Amount {
         type Output = NumOpResult<Amount>;
 
-        fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).valid_or_error(MathOp::Div) }
+        fn div(self, rhs: u64) -> Self::Output { self.checked_div(rhs).valid_or_error() }
     }
     impl ops::Div<u64> for NumOpResult<Amount> {
         type Output = NumOpResult<Amount>;
@@ -83,17 +182,15 @@ impl ops::Div<u64> for NumOpResult<Amount> {
         fn div(self, rhs: u64) -> Self::Output { self.and_then(|lhs| lhs / rhs) }
     }
     impl ops::Div<Amount> for Amount {
-        type Output = NumOpResult<u64>;
+        type Output = u64;
 
-        fn div(self, rhs: Amount) -> Self::Output {
-            self.to_sat().checked_div(rhs.to_sat()).valid_or_error(MathOp::Div)
-        }
+        fn div(self, rhs: Amount) -> Self::Output { self.to_sat() / rhs.to_sat() }
     }
 
     impl ops::Rem<u64> for Amount {
         type Output = NumOpResult<Amount>;
 
-        fn rem(self, modulus: u64) -> Self::Output { self.checked_rem(modulus).valid_or_error(MathOp::Rem) }
+        fn rem(self, modulus: u64) -> Self::Output { self.checked_rem(modulus).valid_or_error() }
     }
     impl ops::Rem<u64> for NumOpResult<Amount> {
         type Output = NumOpResult<Amount>;
@@ -104,7 +201,7 @@ fn rem(self, modulus: u64) -> Self::Output { self.and_then(|lhs| lhs % modulus)
     impl ops::Add<SignedAmount> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
 
-        fn add(self, rhs: SignedAmount) -> Self::Output { self.checked_add(rhs).valid_or_error(MathOp::Add) }
+        fn add(self, rhs: SignedAmount) -> Self::Output { self.checked_add(rhs).valid_or_error() }
     }
     impl ops::Add<NumOpResult<SignedAmount>> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
@@ -115,7 +212,7 @@ fn add(self, rhs: NumOpResult<SignedAmount>) -> Self::Output { rhs.and_then(|a|
     impl ops::Sub<SignedAmount> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
 
-        fn sub(self, rhs: SignedAmount) -> Self::Output { self.checked_sub(rhs).valid_or_error(MathOp::Sub) }
+        fn sub(self, rhs: SignedAmount) -> Self::Output { self.checked_sub(rhs).valid_or_error() }
     }
     impl ops::Sub<NumOpResult<SignedAmount>> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
@@ -131,7 +228,7 @@ fn sub(self, rhs: NumOpResult<SignedAmount>) -> Self::Output {
     impl ops::Mul<i64> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
 
-        fn mul(self, rhs: i64) -> Self::Output { self.checked_mul(rhs).valid_or_error(MathOp::Mul) }
+        fn mul(self, rhs: i64) -> Self::Output { self.checked_mul(rhs).valid_or_error() }
     }
     impl ops::Mul<i64> for NumOpResult<SignedAmount> {
         type Output = NumOpResult<SignedAmount>;
@@ -141,7 +238,7 @@ fn mul(self, rhs: i64) -> Self::Output { self.and_then(|lhs| lhs * rhs) }
     impl ops::Mul<SignedAmount> for i64 {
         type Output = NumOpResult<SignedAmount>;
 
-        fn mul(self, rhs: SignedAmount) -> Self::Output { rhs.checked_mul(self).valid_or_error(MathOp::Mul) }
+        fn mul(self, rhs: SignedAmount) -> Self::Output { rhs.checked_mul(self).valid_or_error() }
     }
     impl ops::Mul<NumOpResult<SignedAmount>> for i64 {
         type Output = NumOpResult<SignedAmount>;
@@ -152,7 +249,7 @@ fn mul(self, rhs: NumOpResult<SignedAmount>) -> Self::Output { rhs.and_then(|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(MathOp::Div) }
+        fn div(self, rhs: i64) -> Self::Output { self.checked_div(rhs).valid_or_error() }
     }
     impl ops::Div<i64> for NumOpResult<SignedAmount> {
         type Output = NumOpResult<SignedAmount>;
@@ -160,17 +257,15 @@ impl ops::Div<i64> for NumOpResult<SignedAmount> {
         fn div(self, rhs: i64) -> Self::Output { self.and_then(|lhs| lhs / rhs) }
     }
     impl ops::Div<SignedAmount> for SignedAmount {
-        type Output = NumOpResult<i64>;
+        type Output = i64;
 
-        fn div(self, rhs: SignedAmount) -> Self::Output {
-            self.to_sat().checked_div(rhs.to_sat()).valid_or_error(MathOp::Div)
-        }
+        fn div(self, rhs: SignedAmount) -> Self::Output { self.to_sat() / rhs.to_sat() }
     }
 
     impl ops::Rem<i64> for SignedAmount {
         type Output = NumOpResult<SignedAmount>;
 
-        fn rem(self, modulus: i64) -> Self::Output { self.checked_rem(modulus).valid_or_error(MathOp::Rem) }
+        fn rem(self, modulus: i64) -> Self::Output { self.checked_rem(modulus).valid_or_error() }
     }
     impl ops::Rem<i64> for NumOpResult<SignedAmount> {
         type Output = NumOpResult<SignedAmount>;
@@ -187,7 +282,7 @@ impl<T> ops::Add<NumOpResult<T>> for NumOpResult<T>
         fn add(self, rhs: Self) -> Self::Output {
             match (self, rhs) {
                 (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
-                (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
+                (_, _) => R::Error(NumOpError {}),
             }
         }
     }
@@ -210,7 +305,7 @@ impl<T> ops::Sub<NumOpResult<T>> for NumOpResult<T>
         fn sub(self, rhs: Self) -> Self::Output {
             match (self, rhs) {
                 (R::Valid(lhs), R::Valid(rhs)) => lhs - rhs,
-                (_, _) => R::Error(NumOpError::while_doing(MathOp::Sub)),
+                (_, _) => R::Error(NumOpError {}),
             }
         }
     }
@@ -245,7 +340,7 @@ fn sum<I>(iter: I) -> Self
     {
         iter.fold(R::Valid(Amount::ZERO), |acc, amount| match (acc, amount) {
             (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
-            (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
+            (_, _) => R::Error(NumOpError {}),
         })
     }
 }
@@ -256,7 +351,7 @@ fn sum<I>(iter: I) -> Self
     {
         iter.fold(R::Valid(Amount::ZERO), |acc, amount| match (acc, amount) {
             (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
-            (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
+            (_, _) => R::Error(NumOpError {}),
         })
     }
 }
@@ -268,7 +363,7 @@ fn sum<I>(iter: I) -> Self
     {
         iter.fold(R::Valid(SignedAmount::ZERO), |acc, amount| match (acc, amount) {
             (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
-            (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
+            (_, _) => R::Error(NumOpError {}),
         })
     }
 }
@@ -279,7 +374,45 @@ fn sum<I>(iter: I) -> Self
     {
         iter.fold(R::Valid(SignedAmount::ZERO), |acc, amount| match (acc, amount) {
             (R::Valid(lhs), R::Valid(rhs)) => lhs + rhs,
-            (_, _) => R::Error(NumOpError::while_doing(MathOp::Add)),
+            (_, _) => R::Error(NumOpError {}),
         })
     }
 }
+
+pub(crate) trait OptionExt<T> {
+    fn valid_or_error(self) -> NumOpResult<T>;
+}
+
+impl OptionExt<Amount> for Option<Amount> {
+    #[inline]
+    fn valid_or_error(self) -> NumOpResult<Amount> {
+        match self {
+            Some(amount) => R::Valid(amount),
+            None => R::Error(NumOpError {}),
+        }
+    }
+}
+
+impl OptionExt<SignedAmount> for Option<SignedAmount> {
+    #[inline]
+    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, Copy, 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 c033eb31..7070adf7 100644
--- a/units/src/amount/signed.rs
+++ b/units/src/amount/signed.rs
@@ -25,7 +25,7 @@ mod encapsulate {
     /// conversion to various denominations. The [`SignedAmount`] type does not implement [`serde`]
     /// traits but we do provide modules for serializing as satoshis or bitcoin.
     ///
-    /// **Warning!**
+    /// Warning!
     ///
     /// This type implements several arithmetic operations from [`core::ops`].
     /// To prevent errors due to an overflow when using these operations,
@@ -57,6 +57,14 @@ impl SignedAmount {
         /// The minimum value of an amount.
         pub const MIN: Self = Self(-21_000_000 * 100_000_000);
 
+        /// Constructs a new [`SignedAmount`] with satoshi precision and the given number of satoshis.
+        ///
+        /// Accepts an `i32` which is guaranteed to be in range for the type, but which can only
+        /// represent roughly -21.47 to 21.47 BTC.
+        pub const fn from_sat_i32(satoshi: i32) -> Self {
+            Self(satoshi as i64) // cannot use i64::from in a constfn
+        }
+
         /// Gets the number of satoshis in this [`SignedAmount`].
         ///
         /// # Examples
@@ -108,18 +116,6 @@ impl SignedAmount {
     /// The maximum value allowed as an amount. Useful for sanity checking.
     pub const MAX_MONEY: Self = Self::MAX;
 
-    /// Constructs a new [`SignedAmount`] with satoshi precision and the given number of satoshis.
-    ///
-    /// Accepts an `i32` which is guaranteed to be in range for the type, but which can only
-    /// represent roughly -21.47 to 21.47 BTC.
-    pub const fn from_sat_i32(satoshi: i32) -> Self {
-        let sats = satoshi as i64; // cannot use i64::from in a constfn
-        match Self::from_sat(sats) {
-            Ok(amount) => amount,
-            Err(_) => panic!("unreachable - 32,767 BTC is within range"),
-        }
-    }
-
     /// Converts from a value expressing a decimal number of bitcoin to a [`SignedAmount`].
     ///
     /// # Errors
@@ -156,7 +152,7 @@ pub const fn from_btc_i16(whole_bitcoin: i16) -> Self {
 
         match Self::from_sat(sats) {
             Ok(amount) => amount,
-            Err(_) => panic!("unreachable - 32,767 BTC is within range"),
+            Err(_) => panic!("unreachable - 65536 BTC is within range"),
         }
     }
 
diff --git a/units/src/amount/unsigned.rs b/units/src/amount/unsigned.rs
index 33c97b4f..20bb6f82 100644
--- a/units/src/amount/unsigned.rs
+++ b/units/src/amount/unsigned.rs
@@ -25,7 +25,7 @@ mod encapsulate {
     /// conversion to various denominations. The [`Amount`] type does not implement [`serde`] traits
     /// but we do provide modules for serializing as satoshis or bitcoin.
     ///
-    /// **Warning!**
+    /// Warning!
     ///
     /// This type implements several arithmetic operations from [`core::ops`].
     /// To prevent errors due to an overflow when using these operations,
@@ -57,6 +57,14 @@ impl Amount {
         /// The minimum value of an amount.
         pub const MIN: Self = Self(0);
 
+        /// Constructs a new [`Amount`] with satoshi precision and the given number of satoshis.
+        ///
+        /// Accepts an `u32` which is guaranteed to be in range for the type, but which can only
+        /// represent roughly 0 to 42.95 BTC.
+        pub const fn from_sat_u32(satoshi: u32) -> Self {
+            Self(satoshi as u64) // cannot use u64::from in a constfn
+        }
+
         /// Gets the number of satoshis in this [`Amount`].
         ///
         /// # Examples
@@ -108,18 +116,6 @@ impl Amount {
     /// The number of bytes that an amount contributes to the size of a transaction.
     pub const SIZE: usize = 8; // Serialized length of a u64.
 
-    /// Constructs a new [`Amount`] with satoshi precision and the given number of satoshis.
-    ///
-    /// Accepts an `u32` which is guaranteed to be in range for the type, but which can only
-    /// represent roughly 0 to 42.95 BTC.
-    pub const fn from_sat_u32(satoshi: u32) -> Self {
-        let sats = satoshi as u64; // cannot use i64::from in a constfn
-        match Self::from_sat(sats) {
-            Ok(amount) => amount,
-            Err(_) => panic!("unreachable - 65,536 BTC is within range"),
-        }
-    }
-
     /// Converts from a value expressing a decimal number of bitcoin to an [`Amount`].
     ///
     /// # Errors
@@ -156,7 +152,7 @@ pub const fn from_btc_u16(whole_bitcoin: u16) -> Self {
 
         match Self::from_sat(sats) {
             Ok(amount) => amount,
-            Err(_) => panic!("unreachable - 65,535 BTC is within range"),
+            Err(_) => panic!("unreachable - 65536 BTC is within range"),
         }
     }
 
diff --git a/units/src/fee.rs b/units/src/fee.rs
index 009dd34d..21d223c9 100644
--- a/units/src/fee.rs
+++ b/units/src/fee.rs
@@ -13,15 +13,14 @@
 
 use core::ops;
 
-use crate::{Amount, FeeRate, MathOp, NumOpResult, OptionExt, Weight};
-
-use NumOpResult as R;
+use crate::amount::{NumOpResult, OptionExt};
+use crate::{Amount, FeeRate, Weight};
 
 impl Amount {
     /// Checked weight ceiling division.
     ///
     /// Be aware that integer division loses the remainder if no exact division
-    /// can be made. This method rounds up ensuring the transaction fee rate is
+    /// can be made. This method rounds up ensuring the transaction fee-rate is
     /// sufficient. See also [`Self::checked_div_by_weight_floor`].
     ///
     /// Returns [`None`] if overflow occurred.
@@ -32,8 +31,8 @@ impl Amount {
     /// # use bitcoin_units::{amount, Amount, FeeRate, Weight};
     /// let amount = Amount::from_sat(10)?;
     /// let weight = Weight::from_wu(300);
-    /// let fee_rate = amount.checked_div_by_weight_ceil(weight);
-    /// assert_eq!(fee_rate, Some(FeeRate::from_sat_per_kwu(34)));
+    /// let fee_rate = amount.checked_div_by_weight_ceil(weight).expect("Division by weight failed");
+    /// assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(34));
     /// # Ok::<_, amount::OutOfRangeError>(())
     /// ```
     #[must_use]
@@ -142,9 +141,10 @@ pub fn fee_vb(self, vb: u64) -> Option<Amount> {
 
     /// Checked weight multiplication.
     ///
-    /// Computes the absolute fee amount for a given [`Weight`] at this fee rate. When the resulting
-    /// fee is a non-integer amount, the amount is rounded up, ensuring that the transaction fee is
-    /// enough instead of falling short if rounded down.
+    /// Computes the absolute fee amount for a given [`Weight`] at this fee rate.
+    /// When the resulting fee is a non-integer amount, the amount is rounded up,
+    /// ensuring that the transaction fee is enough instead of falling short if
+    /// rounded down.
     ///
     /// Returns [`None`] if overflow occurred.
     #[must_use]
@@ -167,156 +167,30 @@ pub const fn checked_mul_by_weight(self, weight: Weight) -> Option<Amount> {
     impl ops::Mul<FeeRate> for Weight {
         type Output = NumOpResult<Amount>;
         fn mul(self, rhs: FeeRate) -> Self::Output {
-            rhs.checked_mul_by_weight(self).valid_or_error(MathOp::Mul)
-        }
-    }
-    impl ops::Mul<FeeRate> for NumOpResult<Weight> {
-        type Output = NumOpResult<Amount>;
-        fn mul(self, rhs: FeeRate) -> Self::Output {
-            match self {
-                R::Valid(lhs) => lhs * rhs,
-                R::Error(e) => NumOpResult::Error(e),
-            }
-        }
-    }
-    impl ops::Mul<NumOpResult<FeeRate>> for Weight {
-        type Output = NumOpResult<Amount>;
-        fn mul(self, rhs: NumOpResult<FeeRate>) -> Self::Output {
-            match rhs {
-                R::Valid(fee_rate) => self * fee_rate,
-                R::Error(e) => NumOpResult::Error(e),
-            }
-        }
-    }
-    impl ops::Mul<NumOpResult<FeeRate>> for NumOpResult<Weight> {
-        type Output = NumOpResult<Amount>;
-        fn mul(self, rhs: NumOpResult<FeeRate>) -> Self::Output {
-            match self {
-                R::Valid(lhs) => { match rhs {
-                    R::Valid(fee_rate) => lhs * fee_rate,
-                    R::Error(e) => NumOpResult::Error(e),
-                }}
-                R::Error(e) => NumOpResult::Error(e),
-            }
+            rhs.checked_mul_by_weight(self).valid_or_error()
         }
     }
 
     impl ops::Mul<Weight> for FeeRate {
         type Output = NumOpResult<Amount>;
         fn mul(self, rhs: Weight) -> Self::Output {
-            self.checked_mul_by_weight(rhs).valid_or_error(MathOp::Mul)
-        }
-    }
-    impl ops::Mul<Weight> for NumOpResult<FeeRate> {
-        type Output = NumOpResult<Amount>;
-        fn mul(self, rhs: Weight) -> Self::Output {
-            match self {
-                R::Valid(lhs) => lhs * rhs,
-                R::Error(e) => NumOpResult::Error(e),
-            }
-        }
-    }
-    impl ops::Mul<NumOpResult<Weight>> for FeeRate {
-        type Output = NumOpResult<Amount>;
-        fn mul(self, rhs: NumOpResult<Weight>) -> Self::Output {
-            match rhs {
-                R::Valid(weight) => self * weight,
-                R::Error(e) => NumOpResult::Error(e),
-            }
-        }
-    }
-    impl ops::Mul<NumOpResult<Weight>> for NumOpResult<FeeRate> {
-        type Output = NumOpResult<Amount>;
-        fn mul(self, rhs: NumOpResult<Weight>) -> Self::Output {
-            match self {
-                R::Valid(lhs) => { match rhs {
-                    R::Valid(weight) => lhs * weight,
-                    R::Error(e) => NumOpResult::Error(e),
-                }}
-                R::Error(e) => NumOpResult::Error(e),
-            }
+            self.checked_mul_by_weight(rhs).valid_or_error()
         }
     }
 
     impl ops::Div<Weight> for Amount {
-        type Output = NumOpResult<FeeRate>;
+        type Output = FeeRate;
 
         fn div(self, rhs: Weight) -> Self::Output {
-            self.checked_div_by_weight_floor(rhs).valid_or_error(MathOp::Div)
-        }
-    }
-    impl ops::Div<Weight> for NumOpResult<Amount> {
-        type Output = NumOpResult<FeeRate>;
-
-        fn div(self, rhs: Weight) -> Self::Output {
-            match self {
-                R::Valid(lhs) => lhs / rhs,
-                R::Error(e) => NumOpResult::Error(e),
-            }
-        }
-    }
-    impl ops::Div<NumOpResult<Weight>> for Amount {
-        type Output = NumOpResult<FeeRate>;
-
-        fn div(self, rhs: NumOpResult<Weight>) -> Self::Output {
-            match rhs {
-                R::Valid(weight) => self / weight,
-                R::Error(e) => NumOpResult::Error(e),
-            }
-        }
-    }
-    impl ops::Div<NumOpResult<Weight>> for NumOpResult<Amount> {
-        type Output = NumOpResult<FeeRate>;
-
-        fn div(self, rhs: NumOpResult<Weight>) -> Self::Output {
-            match self {
-                R::Valid(lhs) => { match rhs {
-                    R::Valid(weight) => lhs / weight,
-                    R::Error(e) => NumOpResult::Error(e),
-                }}
-                R::Error(e) => NumOpResult::Error(e),
-            }
+            FeeRate::from_sat_per_kwu(self.to_sat() * 1000 / rhs.to_wu())
         }
     }
 
     impl ops::Div<FeeRate> for Amount {
-        type Output = NumOpResult<Weight>;
+        type Output = Weight;
 
         fn div(self, rhs: FeeRate) -> Self::Output {
-            self.checked_div_by_fee_rate_floor(rhs).valid_or_error(MathOp::Div)
-        }
-    }
-    impl ops::Div<FeeRate> for NumOpResult<Amount> {
-        type Output = NumOpResult<Weight>;
-
-        fn div(self, rhs: FeeRate) -> Self::Output {
-            match self {
-                R::Valid(lhs) => lhs / rhs,
-                R::Error(e) => NumOpResult::Error(e),
-            }
-        }
-    }
-    impl ops::Div<NumOpResult<FeeRate>> for Amount {
-        type Output = NumOpResult<Weight>;
-
-        fn div(self, rhs: NumOpResult<FeeRate>) -> Self::Output {
-            match rhs {
-                R::Valid(fee_rate) => self / fee_rate,
-                R::Error(e) => NumOpResult::Error(e),
-            }
-        }
-    }
-    impl ops::Div<NumOpResult<FeeRate>> for NumOpResult<Amount> {
-        type Output = NumOpResult<Weight>;
-
-        fn div(self, rhs: NumOpResult<FeeRate>) -> Self::Output {
-            match self {
-                R::Valid(lhs) => { match rhs {
-                    R::Valid(fee_rate) => lhs / fee_rate,
-                    R::Error(e) => NumOpResult::Error(e),
-                }}
-                R::Error(e) => NumOpResult::Error(e),
-            }
+            self.checked_div_by_fee_rate_floor(rhs).unwrap()
         }
     }
 }
@@ -324,9 +198,10 @@ fn div(self, rhs: NumOpResult<FeeRate>) -> Self::Output {
 impl Weight {
     /// Checked fee rate multiplication.
     ///
-    /// Computes the absolute fee amount for a given [`FeeRate`] at this weight. When the resulting
-    /// fee is a non-integer amount, the amount is rounded up, ensuring that the transaction fee is
-    /// enough instead of falling short if rounded down.
+    /// Computes the absolute fee amount for a given [`FeeRate`] at this weight.
+    /// When the resulting fee is a non-integer amount, the amount is rounded up,
+    /// ensuring that the transaction fee is enough instead of falling short if
+    /// rounded down.
     ///
     /// Returns [`None`] if overflow occurred.
     #[must_use]
@@ -341,7 +216,7 @@ mod tests {
 
     #[test]
     fn fee_rate_div_by_weight() {
-        let fee_rate = (Amount::from_sat_u32(329) / Weight::from_wu(381)).unwrap();
+        let fee_rate = Amount::from_sat_u32(329) / Weight::from_wu(381);
         assert_eq!(fee_rate, FeeRate::from_sat_per_kwu(863));
     }
 
@@ -401,21 +276,21 @@ fn amount_div_by_fee_rate() {
         // Test exact division
         let amount = Amount::from_sat_u32(1000);
         let fee_rate = FeeRate::from_sat_per_kwu(2);
-        let weight = (amount / fee_rate).unwrap();
+        let weight = amount / fee_rate;
         assert_eq!(weight, Weight::from_wu(500_000));
 
         // Test reference division
-        let weight_ref = (&amount / fee_rate).unwrap();
+        let weight_ref = &amount / fee_rate;
         assert_eq!(weight_ref, Weight::from_wu(500_000));
-        let weight_ref2 = (amount / &fee_rate).unwrap();
+        let weight_ref2 = amount / &fee_rate;
         assert_eq!(weight_ref2, Weight::from_wu(500_000));
-        let weight_ref3 = (&amount / &fee_rate).unwrap();
+        let weight_ref3 = &amount / &fee_rate;
         assert_eq!(weight_ref3, Weight::from_wu(500_000));
 
         // Test truncation behavior
         let amount = Amount::from_sat_u32(1000);
         let fee_rate = FeeRate::from_sat_per_kwu(3);
-        let weight = (amount / fee_rate).unwrap();
+        let weight = amount / fee_rate;
         // 1000 * 1000 = 1,000,000 msats
         // 1,000,000 / 3 = 333,333.33... wu
         // Should truncate down to 333,333 wu
diff --git a/units/src/fee_rate/mod.rs b/units/src/fee_rate/mod.rs
index 709e3f98..238d21a7 100644
--- a/units/src/fee_rate/mod.rs
+++ b/units/src/fee_rate/mod.rs
@@ -10,10 +10,10 @@
 #[cfg(feature = "arbitrary")]
 use arbitrary::{Arbitrary, Unstructured};
 
-/// Fee rate.
+/// Represents fee rate.
 ///
-/// This is an integer newtype representing fee rate in `sat/kwu`. It provides protection against
-/// mixing up the types as well as basic formatting features.
+/// This is an integer newtype representing fee rate in `sat/kwu`. It provides protection against mixing
+/// up the types as well as basic formatting features.
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
 pub struct FeeRate(u64);
 
diff --git a/units/src/internal_macros.rs b/units/src/internal_macros.rs
index ee6fc33d..c5f0ac6f 100644
--- a/units/src/internal_macros.rs
+++ b/units/src/internal_macros.rs
@@ -4,7 +4,7 @@
 //!
 //! Macros meant to be used inside the `bitcoin-units` library.
 
-/// Implements a mathematical operation for various reference combinations.
+/// Implements an opcode for various reference combinations.
 ///
 /// Given `$ty`, assumes the `$op_trait<$other_ty>` trait is implemented on it,
 /// and implements the same trait with the full matrix of `&$ty` and `&$other_ty`:
diff --git a/units/src/lib.rs b/units/src/lib.rs
index b00a254c..4a166ebb 100644
--- a/units/src/lib.rs
+++ b/units/src/lib.rs
@@ -3,20 +3,6 @@
 //! # Rust Bitcoin - unit types
 //!
 //! This library provides basic types used by the Rust Bitcoin ecosystem.
-//!
-//! If you are using `rust-bitcoin` then you do not need to access this crate directly. Everything
-//! here is re-exported in `rust-bitcoin` at the same path. Also the same re-exports exist in
-//! `primitives` if you are using that crate instead of `bitcoin`.
-//!
-//! # Examples
-//!
-//! ```
-//! // Exactly the same as `use bitcoin::{amount, Amount}`.
-//! use bitcoin_units::{amount, Amount};
-//!
-//! let amount = Amount::from_sat(1_000)?;
-//! # Ok::<_, amount::OutOfRangeError>(())
-//! ```
 
 #![no_std]
 // Experimental features we need.
@@ -34,7 +20,6 @@
 
 mod fee;
 mod internal_macros;
-mod result;
 
 #[doc(hidden)]
 pub mod _export {
@@ -58,8 +43,6 @@ pub mod _core {
     amount::{Amount, SignedAmount},
     block::{BlockHeight, BlockInterval},
     fee_rate::FeeRate,
-    result::{NumOpError, NumOpResult, MathOp},
     time::BlockTime,
     weight::Weight
 };
-pub(crate) use self::result::OptionExt;
diff --git a/units/src/locktime/absolute.rs b/units/src/locktime/absolute.rs
index 153aa0e3..69851607 100644
--- a/units/src/locktime/absolute.rs
+++ b/units/src/locktime/absolute.rs
@@ -256,7 +256,7 @@ pub const fn is_block_height(n: u32) -> bool { n < LOCK_TIME_THRESHOLD }
 /// Returns true if `n` is a UNIX timestamp i.e., greater than or equal to 500,000,000.
 pub const fn is_block_time(n: u32) -> bool { n >= LOCK_TIME_THRESHOLD }
 
-/// Error returned when converting a `u32` to a lock time variant fails.
+/// An error that occurs when converting a `u32` to a lock time variant.
 #[derive(Debug, Clone, PartialEq, Eq)]
 #[non_exhaustive]
 pub struct ConversionError {
diff --git a/units/src/locktime/relative.rs b/units/src/locktime/relative.rs
index 9b1abadf..7f8b4413 100644
--- a/units/src/locktime/relative.rs
+++ b/units/src/locktime/relative.rs
@@ -128,7 +128,7 @@ impl fmt::Display for Time {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
 }
 
-/// Error returned when the input time in seconds was too large to be encoded to a 16 bit 512 second interval.
+/// Input time in seconds was too large to be encoded to a 16 bit 512 second interval.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct TimeOverflowError {
     /// Time value in seconds that overflowed.
diff --git a/units/src/result.rs b/units/src/result.rs
deleted file mode 100644
index 97d6fefa..00000000
--- a/units/src/result.rs
+++ /dev/null
@@ -1,191 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Provides a monodic type returned by mathematical operations (`core::ops`).
-
-use core::fmt;
-
-use NumOpResult as R;
-
-use crate::{Amount, FeeRate, SignedAmount, Weight};
-
-/// Result of a mathematical operation on two numeric types.
-#[derive(Debug, Copy, 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 numeric type, consuming `self`.
-    ///
-    /// # Panics
-    ///
-    /// Panics with `msg` if the numeric result is an `Error`.
-    #[inline]
-    #[track_caller]
-    pub fn expect(self, msg: &str) -> T {
-        match self {
-            R::Valid(x) => x,
-            R::Error(_) => panic!("{}", msg),
-        }
-    }
-
-    /// Returns the contained valid numeric type, consuming `self`.
-    ///
-    /// # Panics
-    ///
-    /// Panics if the numeric result is an `Error`.
-    #[inline]
-    #[track_caller]
-    pub fn unwrap(self) -> T {
-        match self {
-            R::Valid(x) => x,
-            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 valid.
-    #[inline]
-    #[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>`.
-    #[inline]
-    pub fn ok(self) -> Option<T> {
-        match self {
-            R::Valid(x) => Some(x),
-            R::Error(_) => None,
-        }
-    }
-
-    /// Converts this `NumOpResult` to a `Result<T, NumOpError>`.
-    #[inline]
-    #[allow(clippy::missing_errors_doc)]
-    pub fn into_result(self) -> Result<T, NumOpError> {
-        match self {
-            R::Valid(x) => Ok(x),
-            R::Error(e) => Err(e),
-        }
-    }
-
-    /// Calls `op` if the numeric result is `Valid`, otherwise returns the `Error` value of `self`.
-    #[inline]
-    pub fn and_then<F>(self, op: F) -> NumOpResult<T>
-    where
-        F: FnOnce(T) -> NumOpResult<T>,
-    {
-        match self {
-            R::Valid(x) => op(x),
-            R::Error(e) => R::Error(e),
-        }
-    }
-
-    /// Returns `true` if the numeric result is valid.
-    #[inline]
-    pub fn is_valid(&self) -> bool {
-        match self {
-            R::Valid(_) => true,
-            R::Error(_) => false,
-        }
-    }
-
-    /// Returns `true` if the numeric result is invalid.
-    #[inline]
-    pub fn is_error(&self) -> bool { !self.is_valid() }
-}
-
-pub(crate) trait OptionExt<T> {
-    fn valid_or_error(self, op: MathOp) -> NumOpResult<T>;
-}
-
-macro_rules! impl_opt_ext {
-    ($($ty:ident),* $(,)?) => {
-        $(
-            impl OptionExt<$ty> for Option<$ty> {
-                #[inline]
-                fn valid_or_error(self, op: MathOp) -> NumOpResult<$ty> {
-                    match self {
-                        Some(amount) => R::Valid(amount),
-                        None => R::Error(NumOpError(op)),
-                    }
-                }
-            }
-        )*
-    }
-}
-impl_opt_ext!(Amount, SignedAmount, u64, i64, FeeRate, Weight);
-
-/// Error returned when a mathematical operation fails.
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub struct NumOpError(MathOp);
-
-impl NumOpError {
-    /// Creates a [`NumOpError`] caused by `op`.
-    pub fn while_doing(op: MathOp) -> Self { NumOpError(op) }
-
-    /// Returns the [`MathOp`] that caused this error.
-    pub fn operation(self) -> MathOp { self.0 }
-}
-
-impl fmt::Display for NumOpError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "math operation '{}' gave an invalid numeric result", self.operation())
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for NumOpError {}
-
-/// The math operation that caused the error.
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub enum MathOp {
-    /// Addition failed ([`core::ops::Add`] resulted in an invalid value).
-    Add,
-    /// Subtraction failed ([`core::ops::Sub`] resulted in an invalid value).
-    Sub,
-    /// Multiplication failed ([`core::ops::Mul`] resulted in an invalid value).
-    Mul,
-    /// Division failed ([`core::ops::Div`] attempted div-by-zero).
-    Div,
-    /// Calculating the remainder failed ([`core::ops::Rem`] attempted div-by-zero).
-    Rem,
-    /// Negation failed ([`core::ops::Neg`] resulted in an invalid value).
-    Neg,
-}
-
-impl MathOp {
-    /// Returns `true` if this operation error'ed due to overflow.
-    pub fn is_overflow(self) -> bool {
-        matches!(self, MathOp::Add | MathOp::Sub | MathOp::Mul | MathOp::Neg)
-    }
-
-    /// Returns `true` if this operation error'ed due to division by zero.
-    pub fn is_div_by_zero(self) -> bool { !self.is_overflow() }
-}
-
-impl fmt::Display for MathOp {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            MathOp::Add => write!(f, "add"),
-            MathOp::Sub => write!(f, "sub"),
-            MathOp::Mul => write!(f, "mul"),
-            MathOp::Div => write!(f, "div"),
-            MathOp::Rem => write!(f, "rem"),
-            MathOp::Neg => write!(f, "neg"),
-        }
-    }
-}
diff --git a/units/src/weight.rs b/units/src/weight.rs
index 22aea383..50c08df9 100644
--- a/units/src/weight.rs
+++ b/units/src/weight.rs
@@ -12,7 +12,7 @@
 /// The factor that non-witness serialization data is multiplied by during weight calculation.
 pub const WITNESS_SCALE_FACTOR: usize = 4;
 
-/// The weight of a transaction or block.
+/// Represents weight - the weight of a transaction or block.
 ///
 /// This is an integer newtype representing [`Weight`] in `wu`. It provides protection against mixing
 /// up types as well as basic formatting features.


Test patch:
diff --git a/bitcoin/src/blockdata/script/tests.rs b/bitcoin/src/blockdata/script/tests.rs
index a7878b3b..27dcd32e 100644
--- a/bitcoin/src/blockdata/script/tests.rs
+++ b/bitcoin/src/blockdata/script/tests.rs
@@ -65,7 +65,7 @@ fn p2pk_pubkey_bytes_no_checksig_returns_none() {
 }
 
 #[test]
-fn p2pk_pubkey_bytes_empty_script_returns_none() {
+fn p2pk_pubkey_bytes_emptry_script_returns_none() {
     let empty_script = Script::builder().into_script();
     assert!(empty_script.p2pk_pubkey_bytes().is_none());
 }
@@ -556,11 +556,11 @@ fn p2sh_p2wsh_conversion() {
     // Test vectors taken from Core tests/data/script_tests.json
     // bare p2wsh
     let witness_script = ScriptBuf::from_hex("410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac").unwrap();
-    let expected_without =
+    let expected_witout =
         ScriptBuf::from_hex("0020b95237b48faaa69eb078e1170be3b5cbb3fddf16d0a991e14ad274f7b33a4f64")
             .unwrap();
     assert!(witness_script.to_p2wsh().unwrap().is_p2wsh());
-    assert_eq!(witness_script.to_p2wsh().unwrap(), expected_without);
+    assert_eq!(witness_script.to_p2wsh().unwrap(), expected_witout);
 
     // p2sh
     let redeem_script = ScriptBuf::from_hex("0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8").unwrap();
@@ -571,13 +571,13 @@ fn p2sh_p2wsh_conversion() {
 
     // p2sh-p2wsh
     let witness_script = ScriptBuf::from_hex("410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac").unwrap();
-    let expected_without =
+    let expected_witout =
         ScriptBuf::from_hex("0020b95237b48faaa69eb078e1170be3b5cbb3fddf16d0a991e14ad274f7b33a4f64")
             .unwrap();
     let expected_out =
         ScriptBuf::from_hex("a914f386c2ba255cc56d20cfa6ea8b062f8b5994551887").unwrap();
     assert!(witness_script.to_p2sh().unwrap().is_p2sh());
-    assert_eq!(witness_script.to_p2wsh().unwrap(), expected_without);
+    assert_eq!(witness_script.to_p2wsh().unwrap(), expected_witout);
     assert_eq!(witness_script.to_p2wsh().unwrap().to_p2sh().unwrap(), expected_out);
 }
 
@@ -684,7 +684,7 @@ fn default_dust_value() {
     // well-known scriptPubKey types.
     let script_p2wpkh = Builder::new().push_int_unchecked(0).push_slice([42; 20]).into_script();
     assert!(script_p2wpkh.is_p2wpkh());
-    assert_eq!(script_p2wpkh.minimal_non_dust(), Amount::from_sat_u32(294));
+    assert_eq!(script_p2wpkh.minimal_non_dust(), Some(Amount::from_sat_u32(294)));
     assert_eq!(
         script_p2wpkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)),
         Some(Amount::from_sat_u32(588))
@@ -698,7 +698,7 @@ fn default_dust_value() {
         .push_opcode(OP_CHECKSIG)
         .into_script();
     assert!(script_p2pkh.is_p2pkh());
-    assert_eq!(script_p2pkh.minimal_non_dust(), Amount::from_sat_u32(546));
+    assert_eq!(script_p2pkh.minimal_non_dust(), Some(Amount::from_sat_u32(546)));
     assert_eq!(
         script_p2pkh.minimal_non_dust_custom(FeeRate::from_sat_per_vb_unchecked(6)),
         Some(Amount::from_sat_u32(1092))
diff --git a/bitcoin/tests/bip_174.rs b/bitcoin/tests/bip_174.rs
index 49565865..396fc6f3 100644
--- a/bitcoin/tests/bip_174.rs
+++ b/bitcoin/tests/bip_174.rs
@@ -263,15 +263,15 @@ fn update_psbt(mut psbt: Psbt, fingerprint: Fingerprint) -> Psbt {
     psbt
 }
 
-/// `pk_path` holds tuples of `(public_key, derivation_path)`. `indices` is used to access the
+/// `pk_path` holds tuples of `(public_key, derivation_path)`. `indecies` is used to access the
 /// `pk_path` vector. `fingerprint` is from the parent extended public key.
 fn bip32_derivation(
     fingerprint: Fingerprint,
     pk_path: &[(&str, &str)],
-    indices: Vec<usize>,
+    indecies: Vec<usize>,
 ) -> BTreeMap<secp256k1::PublicKey, KeySource> {
     let mut tree = BTreeMap::new();
-    for i in indices {
+    for i in indecies {
         let pk = pk_path[i].0;
         let path = pk_path[i].1;
 
diff --git a/bitcoin/tests/psbt-sign-taproot.rs b/bitcoin/tests/psbt-sign-taproot.rs
index 78f3a77b..0f8cf593 100644
--- a/bitcoin/tests/psbt-sign-taproot.rs
+++ b/bitcoin/tests/psbt-sign-taproot.rs
@@ -83,8 +83,11 @@ fn get_key<C: Signing>(
         //
         // Step 1: create psbt for key path spend.
         //
-        let mut psbt_key_path_spend =
-            create_psbt_for_taproot_key_path_spend(address, to_address, tree.clone());
+        let mut psbt_key_path_spend = create_psbt_for_taproot_key_path_spend(
+            address.clone(),
+            to_address.clone(),
+            tree.clone(),
+        );
 
         //
         // Step 2: sign psbt.
@@ -128,8 +131,8 @@ fn get_key<C: Signing>(
         // Step 1: create psbt for script path spend.
         //
         let mut psbt_script_path_spend = create_psbt_for_taproot_script_path_spend(
-            address,
-            to_address,
+            address.clone(),
+            to_address.clone(),
             tree.clone(),
             x_only_pubkey,
             signing_key_path,
diff --git a/hashes/tests/api.rs b/hashes/tests/api.rs
deleted file mode 100644
index 5de01d55..00000000
--- a/hashes/tests/api.rs
+++ /dev/null
@@ -1,225 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Test the API surface of `units`.
-//!
-//! The point of these tests are to check the API surface as opposed to test the API functionality.
-//!
-//! ref: <https://rust-lang.github.io/api-guidelines/about.html>
-
-#![allow(dead_code)]
-#![allow(unused_imports)]
-
-// Import using module style e.g., `sha256::Hash`.
-use bitcoin_hashes::{
-    hash160, hash_newtype, hkdf, hmac, ripemd160, sha1, sha256, sha256d, sha256t, sha256t_tag,
-    sha384, sha512, sha512_256, siphash24, FromSliceError, Hash, HashEngine,
-};
-// Import using type alias style e.g., `Sha256`.
-use bitcoin_hashes::{
-    Hash160, Hkdf, Hmac, HmacEngine, Ripemd160, Sha1, Sha256, Sha256d, Sha256t, Sha384, Sha512,
-    Sha512_256, Siphash24,
-};
-
-// Arbitrary midstate value; taken from as sha256t unit tests.
-const TEST_MIDSTATE: [u8; 32] = [
-    156, 224, 228, 230, 124, 17, 108, 57, 56, 179, 202, 242, 195, 15, 80, 137, 211, 243, 147, 108,
-    71, 99, 110, 96, 125, 179, 62, 234, 221, 198, 240, 201,
-];
-
-sha256t_tag! {
-    /// Test tag so we don't have to use generics.
-    #[derive(Debug)]
-    struct Tag = raw(TEST_MIDSTATE, 64);
-}
-hash_newtype! {
-    /// A concrete sha256t hash type so we don't have to use generics.
-    #[derive(Debug)]
-    struct TaggedHash(sha256t::Hash<Tag>);
-}
-
-/// All the hash types excluding `Hkdf`.
-// `Hkdf` only implements `Copy` and `Clone` ATM - by design.
-#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] // C-COMMON-TRAITS
-// We check `Hkdf` implements `Debug` in the non-empty debug test below.
-#[derive(Debug)] // All public types implement Debug (C-DEBUG).
-struct Hashes<T: Hash> {
-    a: hash160::Hash,
-    c: Hmac<T>,
-    d: ripemd160::Hash,
-    e: sha1::Hash,
-    f: sha256::Hash,
-    g: sha256d::Hash,
-    h: TaggedHash,
-    i: sha384::Hash,
-    j: sha512::Hash,
-    k: sha512_256::Hash,
-    l: siphash24::Hash,
-}
-
-impl Hashes<Sha256> {
-    fn new_sha256() -> Self {
-        let hmac = HmacEngine::<sha256::HashEngine>::new(&[]).finalize();
-        // `TaggedHash` is not a general hash but `Sha256<Tag>` is.
-        let tagged = TaggedHash::from_byte_array(Sha256t::<Tag>::hash(&[]).to_byte_array());
-        let siphash = Siphash24::from_engine(siphash24::HashEngine::with_keys(0, 0));
-
-        Hashes {
-            a: Hash160::hash(&[]),
-            // b: hkdf,
-            c: hmac,
-            d: Ripemd160::hash(&[]),
-            e: Sha1::hash(&[]),
-            f: Sha256::hash(&[]),
-            g: Sha256d::hash(&[]),
-            h: tagged,
-            i: Sha384::hash(&[]),
-            j: Sha512::hash(&[]),
-            k: Sha512_256::hash(&[]),
-            l: siphash,
-        }
-    }
-}
-
-/// All the hash engines.
-#[derive(Clone)] // C-COMMON-TRAITS
-#[derive(Debug)] // All public types implement Debug (C-DEBUG).
-struct Engines {
-    a: hash160::HashEngine,
-    // We cannot derive `Debug` on a generic `HmacEngine<T>` engine.
-    b: hmac::HmacEngine<sha256::HashEngine>,
-    c: ripemd160::HashEngine,
-    d: sha1::HashEngine,
-    e: sha256::HashEngine,
-    f: sha256d::HashEngine,
-    g: sha256t::HashEngine<Tag>,
-    h: sha384::HashEngine,
-    i: sha512::HashEngine,
-    j: sha512_256::HashEngine,
-    k: siphash24::HashEngine,
-}
-
-impl Engines {
-    fn new_sha256() -> Self {
-        Engines {
-            a: hash160::HashEngine::new(),
-            b: hmac::HmacEngine::<sha256::HashEngine>::new(&[]),
-            c: ripemd160::HashEngine::new(),
-            d: sha1::HashEngine::new(),
-            e: sha256::HashEngine::new(),
-            f: sha256d::HashEngine::new(),
-            g: sha256t::Hash::<Tag>::engine(),
-            h: sha384::HashEngine::new(),
-            i: sha512::HashEngine::new(),
-            j: sha512_256::HashEngine::new(),
-            k: siphash24::HashEngine::with_keys(0, 0),
-        }
-    }
-}
-
-/// Public structs that are not hashes, engines, or errors.
-#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] // C-COMMON-TRAITS
-#[derive(Debug)] // All public types implement Debug (C-DEBUG).
-struct OtherStructs {
-    a: sha256::Midstate,
-    // There is no way to construct a `siphash24::State` so we cannot directly
-    // test it but `siphash24::HashEngine` includes one so `Engines` implicitly
-    // tests it (e.g. `Debug` and `Clone`).
-    //
-    // b: siphash24::State,
-
-    // Don't worry about including a tag because its tested in `primitives`.
-}
-
-impl OtherStructs {
-    fn new() -> Self { Self { a: sha256::Midstate::new(TEST_MIDSTATE, 0) } }
-}
-
-/// All hash engine types that implement `Default`.
-#[derive(Default)]
-struct Default {
-    a: hash160::HashEngine,
-    b: ripemd160::HashEngine,
-    c: sha1::HashEngine,
-    d: sha256::HashEngine,
-    e: sha256d::HashEngine,
-    f: sha256t::HashEngine<Tag>,
-    g: sha384::HashEngine,
-    h: sha512::HashEngine,
-    i: sha512_256::HashEngine,
-}
-
-/// Hash types that require a key.
-struct Keyed<T: Hash> {
-    a: Hmac<T>,
-    l: siphash24::Hash,
-}
-
-/// A struct that includes all public error types.
-// These derives are the policy of `rust-bitcoin` not Rust API guidelines.
-#[derive(Debug, Clone, PartialEq, Eq)] // All public types implement Debug (C-DEBUG).
-struct Errors {
-    a: FromSliceError,
-    b: hkdf::MaxLengthError,
-    c: sha256::MidstateError,
-}
-
-#[test]
-fn api_can_use_modules_from_crate_root() {
-    use bitcoin_hashes::{
-        hash160, hkdf, hmac, ripemd160, sha1, sha256, sha256d, sha256t, sha384, sha512, sha512_256,
-        siphash24,
-    };
-}
-
-#[test]
-fn api_can_use_alias_from_crate_root() {
-    use bitcoin_hashes::{
-        Hash160, Hkdf, Hmac, Ripemd160, Sha1, Sha256, Sha256d, Sha256t, Sha384, Sha512, Sha512_256,
-        Siphash24,
-    };
-}
-
-// `Debug` representation is never empty (C-DEBUG-NONEMPTY).
-#[test]
-fn api_all_non_error_types_have_non_empty_debug() {
-    macro_rules! check_debug {
-        ($t:tt; $($field:tt),* $(,)?) => {
-            $(
-                let debug = format!("{:?}", $t.$field);
-                assert!(!debug.is_empty());
-            )*
-        }
-    }
-
-    let t = Hashes::<Sha256>::new_sha256();
-    check_debug!(t; a, c, d, e, f, g, h, i, j, k, l);
-
-    // This tests `Debug` on `Hkdf` but not for all `T: GeneralHash`.
-    let t = Hkdf::<sha256::HashEngine>::new(&[], &[]);
-    let debug = format!("{:?}", t);
-    assert!(!debug.is_empty());
-
-    let t = Engines::new_sha256();
-    check_debug!(t; a, c, d, e, f, g, h, i, j, k);
-
-    let t = OtherStructs::new();
-    check_debug!(t; a);
-}
-
-#[test]
-fn all_types_implement_send_sync() {
-    fn assert_send<T: Send>() {}
-    fn assert_sync<T: Sync>() {}
-
-    //  Types are `Send` and `Sync` where possible (C-SEND-SYNC).
-    assert_send::<Hashes<Sha256>>();
-    assert_sync::<Hashes<Sha256>>();
-    assert_send::<Engines>();
-    assert_sync::<Engines>();
-    assert_send::<OtherStructs>();
-    assert_sync::<OtherStructs>();
-
-    // Error types should implement the Send and Sync traits (C-GOOD-ERR).
-    assert_send::<Errors>();
-    assert_sync::<Errors>();
-}
diff --git a/units/src/amount/tests.rs b/units/src/amount/tests.rs
index d6752d45..f8aba133 100644
--- a/units/src/amount/tests.rs
+++ b/units/src/amount/tests.rs
@@ -13,7 +13,6 @@
 use ::serde::{Deserialize, Serialize};
 
 use super::*;
-use crate::NumOpResult;
 #[cfg(feature = "alloc")]
 use crate::{FeeRate, Weight};
 
@@ -1231,31 +1230,27 @@ fn op_int_combos() {
 
 #[test]
 fn unsigned_amount_div_by_amount() {
-    assert_eq!((sat(0) / sat(7)).unwrap(), 0);
-    assert_eq!((sat(1897) / sat(7)).unwrap(), 271);
+    assert_eq!(sat(0) / sat(7), 0);
+    assert_eq!(sat(1897) / sat(7), 271);
 }
 
 #[test]
-fn unsigned_amount_div_by_amount_zero() {
-    let res = sat(1897) / Amount::ZERO;
-    assert!(res.into_result().is_err());
-}
+#[should_panic(expected = "attempt to divide by zero")]
+fn unsigned_amount_div_by_amount_zero() { let _ = sat(1897) / Amount::ZERO; }
 
 #[test]
 fn signed_amount_div_by_amount() {
-    assert_eq!((ssat(0) / ssat(7)).unwrap(), 0);
+    assert_eq!(ssat(0) / ssat(7), 0);
 
-    assert_eq!((ssat(1897) / ssat(7)).unwrap(), 271);
-    assert_eq!((ssat(1897) / ssat(-7)).unwrap(), -271);
-    assert_eq!((ssat(-1897) / ssat(7)).unwrap(), -271);
-    assert_eq!((ssat(-1897) / ssat(-7)).unwrap(), 271);
+    assert_eq!(ssat(1897) / ssat(7), 271);
+    assert_eq!(ssat(1897) / ssat(-7), -271);
+    assert_eq!(ssat(-1897) / ssat(7), -271);
+    assert_eq!(ssat(-1897) / ssat(-7), 271);
 }
 
 #[test]
-fn signed_amount_div_by_amount_zero() {
-    let res = ssat(1897) / SignedAmount::ZERO;
-    assert!(res.into_result().is_err());
-}
+#[should_panic(expected = "attempt to divide by zero")]
+fn signed_amount_div_by_amount_zero() { let _ = ssat(1897) / SignedAmount::ZERO; }
 
 #[test]
 fn check_const() {
@@ -1405,22 +1400,3 @@ fn amount_op_result_sum() {
     let _ = amount_refs.iter().copied().sum::<NumOpResult<Amount>>();
     let _ = amount_refs.into_iter().sum::<NumOpResult<Amount>>();
 }
-
-#[test]
-fn math_op_errors() {
-    let overflow = Amount::MAX + Amount::from_sat(1).unwrap();
-    if let NumOpResult::Error(err) = overflow {
-        assert!(err.operation().is_overflow());
-        assert!(!err.operation().is_div_by_zero());
-    } else {
-        panic!("Expected an overflow error, but got a valid result");
-    }
-
-    let div_by_zero = Amount::from_sat(10).unwrap() / Amount::ZERO;
-    if let NumOpResult::Error(err) = div_by_zero {
-        assert!(!err.operation().is_overflow());
-        assert!(err.operation().is_div_by_zero());
-    } else {
-        panic!("Expected a division by zero error, but got a valid result");
-    }
-}