Repository: rust-bitcoin/rust-bitcoin
Pull request #2530 (2024-03-01 22:07:42)

Improve leaf errors
In light of recent discussion go over the codebase and look for some places that the leaf errors are wrong. Does not do the whole code base, excludes `p2p` and a couple of other places.

tcharding: I'm guessing a better naming convention will fall out once I've done this a few more times. I"m was leaning towards making the names a bit longer than usual but @Kixunil suggested shortening one recently. Not totally sure either way. Shall we merge and iterate?
coveralls: ## Pull Request Test Coverage Report for [Build 8117761986](https://coveralls.io/builds/66052432)


### Details

* **0** of **3**   **(0.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.007%**) to **83.865%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [bitcoin/src/blockdata/script/witness_version.rs](https://coveralls.io/builds/66052432/source?filename=bitcoin%2Fsrc%2Fblockdata%2Fscript%2Fwitness_version.rs#L255) | 0 | 1 | 0.0%
| [hashes/src/lib.rs](https://coveralls.io/builds/66052432/source?filename=hashes%2Fsrc%2Flib.rs#L244) | 0 | 2 | 0.0%
<!-- | **Total:** | **0** | **3** | **0.0%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/66052432/badge)](https://coveralls.io/builds/66052432) |
| :-- | --: |
| Change from base [Build 8113533131](https://coveralls.io/builds/66026779): |  0.007% |
| Covered Lines: | 19460 |
| Relevant Lines: | 23204 |

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


Patch:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 884b5961..986c15a0 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -6,10 +6,8 @@ test:
     - any-glob-to-any-file: fuzz/**
     - any-glob-to-any-file: '*/tests/**'
     - any-glob-to-any-file: 'dep_test'
-    - any-glob-to-any-file: 'contrib/run_task.sh'
-    - any-glob-to-any-file: 'contrib/test_vars.sh'
-    - any-glob-to-any-file: '*/contrib/extra_tests.sh'
-    - any-glob-to-any-file: '*/contrib/test_vars.sh'
+    - any-glob-to-any-file: 'contrib/test.sh'
+    - any-glob-to-any-file: '*/contrib/test.sh'
 doc:
   - changed-files:
     - any-glob-to-any-file: '**/*.md'
diff --git a/.github/nightly-version b/.github/nightly-version
deleted file mode 100644
index 31d76b1c..00000000
--- a/.github/nightly-version
+++ /dev/null
@@ -1,1 +0,0 @@
-nightly-2024-03-05
diff --git a/.github/workflows/cron-daily-update-nightly.yml b/.github/workflows/cron-daily-update-nightly.yml
deleted file mode 100644
index 0553d524..00000000
--- a/.github/workflows/cron-daily-update-nightly.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: Update Nightly rustc
-on:
-  schedule:
-    - cron: "0 0 * * *" # runs daily at 00:00
-  workflow_dispatch: # allows manual triggering
-jobs:
-  format:
-    name: Update nightly rustc
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v4
-      - uses: dtolnay/rust-toolchain@nightly
-      - name: Update rust.yml to use latest nightly
-        run: |
-          set -x
-          # Not every night has a nightly, so extract the date from whatever
-          # version of the compiler dtolnay/rust-toolchain gives us.
-          NIGHTLY_DATE=$(rustc +nightly --verbose --version | sed -ne 's/^commit-date: //p')
-          # Update the nightly version in the reference file.
-          echo "nightly-${NIGHTLY_DATE}" > .github/nightly-version
-          echo "nightly_date=${NIGHTLY_DATE}" >> $GITHUB_ENV
-          # Some days there is no new nightly. In this case don't make an empty PR.
-          if ! git diff --exit-code > /dev/null; then
-              echo "Updated nightly. Opening PR."
-              echo "changes_made=true" >> $GITHUB_ENV
-          else
-              echo "Attempted to update nightly but the latest-nightly date did not change. Not opening any PR."
-              echo "changes_made=false" >> $GITHUB_ENV
-          fi
-      - name: Create Pull Request
-        if: env.changes_made == 'true'
-        uses: peter-evans/create-pull-request@v6
-        with:
-          token: ${{ secrets.APOELSTRA_CREATE_PR_TOKEN }}
-          author: Update Nightly Rustc Bot <bot@example.com>
-          committer: Update Nightly Rustc Bot <bot@example.com>
-          title: Automated daily update to rustc (to nightly-${{ env.nightly_date }})
-          body: |
-           Automated update to Github CI workflow `rust.yml` by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action
-          commit-message: Automated update to Github CI to rustc nightly-${{ env.nightly_date }}
-          branch: create-pull-request/daily-nightly-update
diff --git a/.github/workflows/cron-daily-kani.yml b/.github/workflows/kani.yml
similarity index 100%
rename from .github/workflows/cron-daily-kani.yml
rename to .github/workflows/kani.yml
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 169e2e5f..4769e435 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -13,13 +13,9 @@ jobs:
     outputs:
       crates: ${{ steps.get_matrix.outputs.crates }}
       deps: ${{ steps.get_matrix.outputs.deps }}
-      nightly_version: ${{ steps.read_toolchain.outputs.nightly_version }}
     steps:
       - name: Checkout Crate
         uses: actions/checkout@v4
-      - name: Read nightly version
-        id: read_toolchain
-        run: echo "nightly_version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
       - name: Prepare tests
         id: get_matrix
         run: contrib/get_matrix.sh
@@ -79,9 +75,7 @@ jobs:
       - name: Checkout Crate
         uses: actions/checkout@v4
       - name: Checkout Toolchain
-        uses: dtolnay/rust-toolchain@v1
-        with:
-          toolchain: ${{ needs.Prepare.outputs.nightly_version }}
+        uses: dtolnay/rust-toolchain@nightly
       - name: Install clippy
         run: rustup component add clippy
       - name: Set dependencies
@@ -145,7 +139,6 @@ jobs:
         run: cross test --target s390x-unknown-linux-gnu
 
   Embedded:
-    needs: Prepare
     runs-on: ubuntu-latest
     env:
       RUSTFLAGS: "-C link-arg=-Tlink.x"
@@ -156,9 +149,8 @@ jobs:
       - name: Set up QEMU
         run: sudo apt update && sudo apt install -y qemu-system-arm gcc-arm-none-eabi
       - name: Checkout Toolchain
-        uses: dtolnay/rust-toolchain@v1
+        uses: dtolnay/rust-toolchain@nightly
         with:
-          toolchain: ${{ needs.Prepare.outputs.nightly_version }}
           targets: thumbv7m-none-eabi
       - name: Install src
         run: rustup component add rust-src
@@ -183,9 +175,7 @@ jobs:
       - name: Checkout Crate
         uses: actions/checkout@v4
       - name: Checkout Toolchain
-        uses: dtolnay/rust-toolchain@v1
-        with:
-          toolchain: ${{ needs.Prepare.outputs.nightly_version }}
+        uses: dtolnay/rust-toolchain@nightly
       - name: Install src
         run: rustup component add rust-src
       - name: Running address sanitizer
diff --git a/.github/workflows/cron-weekly-rustfmt.yml b/.github/workflows/rustfmt.yml
similarity index 100%
rename from .github/workflows/cron-weekly-rustfmt.yml
rename to .github/workflows/rustfmt.yml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 124bb284..595d7d22 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -78,7 +78,7 @@ To contribute a patch, the workflow is a as follows:
 2. Create topic branch
 3. Commit patches
 
-Please keep commits atomic and diffs easy to read. For this reason
+Please keep commits should atomic and diffs easy to read. For this reason
 do not mix any formatting fixes or code moves with actual code changes.
 Further, each commit, individually, should compile and pass tests, in order to
 ensure git bisect and other automated tools function properly.
@@ -91,6 +91,12 @@ to split it into multiple small, focused PRs.
 Commits should cover both the issue fixed and the solution's rationale.
 Please keep these [guidelines](https://chris.beams.io/posts/git-commit/) in mind.
 
+To facilitate communication with other contributors, the project is making use
+of GitHub's "assignee" field. First check that no one is assigned and then
+comment suggesting that you're working on it. If someone is already assigned,
+don't hesitate to ask if the assigned party or previous commenters are still
+working on it if it has been awhile.
+
 
 ## Preparing PRs
 
@@ -118,6 +124,19 @@ NB: reviewers may run more complex test/CI scripts, thus, satisfying all the
 requirements above is just a preliminary, but not necessary sufficient step for
 getting the PR accepted as a valid candidate PR for the `master` branch.
 
+PR authors may also find it useful to run the following script locally in order
+to check that each of the commits within the PR satisfies the requirements
+above, before submitting the PR to review:
+```shell script
+RUSTUP_TOOLCHAIN=1.41.1 ./contrib/test.sh
+```
+Please replace the value in `RUSTUP_TOOLCHAIN=1.41.1` with the current MSRV from
+[README.md].
+
+NB: Please keep in mind that the script above replaces `Cargo.lock` file, which
+is necessary to support current MSRV, incompatible with `stable` and newer cargo
+versions.
+
 ### Peer review
 
 Anyone may participate in peer review which is expressed by comments in the pull
@@ -290,7 +309,6 @@ More specifically an error should
 - derive `Debug, Clone, PartialEq, Eq` (and `Copy` iff not `non_exhaustive`).
 - implement Display using `write_err!()` macro if a variant contains an inner error source.
 - have `Error` suffix
-- call `internals::impl_from_infallible!
 - implement `std::error::Error` if they are public (feature gated on "std").
 
 ```rust
@@ -303,9 +321,6 @@ pub enum Error {
     /// Documentation for variant B.
     B,
 }
-
-internals::impl_from_infallible!(Error);
-
 ```
 
 
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index c81b5361..89b7982d 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -8,17 +8,10 @@ version = "1.0.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
 
-[[package]]
-name = "arrayvec"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
-
 [[package]]
 name = "base58check"
 version = "0.1.0"
 dependencies = [
- "bitcoin-internals",
  "bitcoin_hashes",
  "hex-conservative",
 ]
@@ -165,12 +158,9 @@ checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
 
 [[package]]
 name = "hex-conservative"
-version = "0.2.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1aa273bf451e37ed35ced41c71a5e2a4e29064afb104158f2514bcd71c2c986"
-dependencies = [
- "arrayvec",
-]
+checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
 
 [[package]]
 name = "hex_lit"
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index 2b0e4072..29642168 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -8,17 +8,10 @@ version = "1.0.71"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 
-[[package]]
-name = "arrayvec"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
-
 [[package]]
 name = "base58check"
 version = "0.1.0"
 dependencies = [
- "bitcoin-internals",
  "bitcoin_hashes",
  "hex-conservative",
 ]
@@ -164,12 +157,9 @@ checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
 
 [[package]]
 name = "hex-conservative"
-version = "0.2.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1aa273bf451e37ed35ced41c71a5e2a4e29064afb104158f2514bcd71c2c986"
-dependencies = [
- "arrayvec",
-]
+checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
 
 [[package]]
 name = "hex_lit"
diff --git a/base58/Cargo.toml b/base58/Cargo.toml
index fb312dd3..d51fb5bd 100644
--- a/base58/Cargo.toml
+++ b/base58/Cargo.toml
@@ -22,7 +22,6 @@ rustdoc-args = ["--cfg", "docsrs"]
 
 [dependencies]
 hashes = { package = "bitcoin_hashes", version = "0.13.0", default-features = false, features = ["alloc"] }
-internals = { package = "bitcoin-internals", version = "0.2.0" }
 
 [dev-dependencies]
-hex = { package = "hex-conservative", version = "0.2.0", default-features = false, features = ["alloc"] }
+hex = { package = "hex-conservative", version = "0.1.1", default-features = false, features = ["alloc"] }
diff --git a/base58/src/lib.rs b/base58/src/lib.rs
index f40e64e8..a0aa04ca 100644
--- a/base58/src/lib.rs
+++ b/base58/src/lib.rs
@@ -228,8 +228,6 @@ pub enum Error {
     TooShort(usize),
 }
 
-internals::impl_from_infallible!(Error);
-
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use Error::*;
diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml
index 8a46cb1b..ffc02bb1 100644
--- a/bitcoin/Cargo.toml
+++ b/bitcoin/Cargo.toml
@@ -31,7 +31,7 @@ rustdoc-args = ["--cfg", "docsrs"]
 base58 = { package = "base58check", version = "0.1.0", default-features = false }
 bech32 = { version = "0.11.0", default-features = false, features = ["alloc"] }
 hashes = { package = "bitcoin_hashes", version = "0.13.0", default-features = false, features = ["alloc", "io"] }
-hex = { package = "hex-conservative", version = "0.2.0", default-features = false, features = ["alloc"] }
+hex = { package = "hex-conservative", version = "0.1.1", default-features = false, features = ["alloc"] }
 hex_lit = "0.1.1"
 internals = { package = "bitcoin-internals", version = "0.2.0" }
 io = { package = "bitcoin-io", version = "0.1.1", default-features = false, features = ["alloc"] }
diff --git a/bitcoin/src/address/error.rs b/bitcoin/src/address/error.rs
index ac0073d5..a5be763e 100644
--- a/bitcoin/src/address/error.rs
+++ b/bitcoin/src/address/error.rs
@@ -41,8 +41,6 @@ pub enum FromScriptError {
     WitnessVersion(witness_version::TryFromError),
 }
 
-internals::impl_from_infallible!(FromScriptError);
-
 impl fmt::Display for FromScriptError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use FromScriptError::*;
@@ -84,8 +82,6 @@ pub enum P2shError {
     ExcessiveScriptSize,
 }
 
-internals::impl_from_infallible!(P2shError);
-
 impl fmt::Display for P2shError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use P2shError::*;
@@ -139,8 +135,6 @@ pub enum ParseError {
     UnknownHrp(UnknownHrpError),
 }
 
-internals::impl_from_infallible!(ParseError);
-
 impl fmt::Display for ParseError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use ParseError::*;
diff --git a/bitcoin/src/bip152.rs b/bitcoin/src/bip152.rs
index db871dfa..8c71a01f 100644
--- a/bitcoin/src/bip152.rs
+++ b/bitcoin/src/bip152.rs
@@ -28,8 +28,6 @@ pub enum Error {
     InvalidPrefill,
 }
 
-internals::impl_from_infallible!(Error);
-
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match *self {
diff --git a/bitcoin/src/bip158.rs b/bitcoin/src/bip158.rs
index 66bef777..ee9ea7c3 100644
--- a/bitcoin/src/bip158.rs
+++ b/bitcoin/src/bip158.rs
@@ -77,8 +77,6 @@ pub enum Error {
     Io(io::Error),
 }
 
-internals::impl_from_infallible!(Error);
-
 impl Display for Error {
     fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
         use Error::*;
diff --git a/bitcoin/src/bip32.rs b/bitcoin/src/bip32.rs
index 22242f12..7a7432a3 100644
--- a/bitcoin/src/bip32.rs
+++ b/bitcoin/src/bip32.rs
@@ -251,7 +251,7 @@ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
 /// Trait that allows possibly failable conversion from a type into a
 /// derivation path
 pub trait IntoDerivationPath {
-    /// Converts a given type into a [`DerivationPath`] with possible error
+    /// Convers a given type into a [`DerivationPath`] with possible error
     fn into_derivation_path(self) -> Result<DerivationPath, Error>;
 }
 
@@ -491,8 +491,6 @@ pub enum Error {
     InvalidPublicKeyHexLength(usize),
 }
 
-internals::impl_from_infallible!(Error);
-
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use Error::*;
diff --git a/bitcoin/src/blockdata/block.rs b/bitcoin/src/blockdata/block.rs
index 504813bf..49ea80f5 100644
--- a/bitcoin/src/blockdata/block.rs
+++ b/bitcoin/src/blockdata/block.rs
@@ -47,7 +47,7 @@ fn from(wtxid: Wtxid) -> Self { Self::from_byte_array(wtxid.to_byte_array()) }
 /// Bitcoin block header.
 ///
 /// Contains all the block's information except the actual transactions, but
-/// including a root of a [merkle tree] committing to all transactions in the block.
+/// including a root of a [merkle tree] commiting to all transactions in the block.
 ///
 /// [merkle tree]: https://en.wikipedia.org/wiki/Merkle_tree
 ///
@@ -420,8 +420,6 @@ pub enum Bip34Error {
     NegativeHeight,
 }
 
-internals::impl_from_infallible!(Bip34Error);
-
 impl fmt::Display for Bip34Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use Bip34Error::*;
@@ -458,8 +456,6 @@ pub enum ValidationError {
     BadTarget,
 }
 
-internals::impl_from_infallible!(ValidationError);
-
 impl fmt::Display for ValidationError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use ValidationError::*;
diff --git a/bitcoin/src/blockdata/locktime/absolute.rs b/bitcoin/src/blockdata/locktime/absolute.rs
index 6cb52c85..96615b43 100644
--- a/bitcoin/src/blockdata/locktime/absolute.rs
+++ b/bitcoin/src/blockdata/locktime/absolute.rs
@@ -17,9 +17,10 @@
 #[cfg(doc)]
 use crate::absolute;
 use crate::consensus::encode::{self, Decodable, Encodable};
-use crate::error::{ParseIntError, PrefixedHexError, UnprefixedHexError, ContainsPrefixError, MissingPrefixError};
-use crate::parse::{self, impl_parse_str, impl_parse_str_from_int_infallible};
+use crate::error::ParseIntError;
+use crate::parse::{impl_parse_str, impl_parse_str_from_int_infallible};
 use crate::prelude::*;
+use crate::string::FromHexStr;
 
 /// The Threshold for deciding whether a lock time value is a height or a time (see [Bitcoin Core]).
 ///
@@ -101,29 +102,6 @@ impl LockTime {
     /// The number of bytes that the locktime contributes to the size of a transaction.
     pub const SIZE: usize = 4; // Serialized length of a u32.
 
-    /// Creates a `LockTime` from an prefixed hex string.
-    pub fn from_hex(s: &str) -> Result<Self, PrefixedHexError> {
-        let stripped = if let Some(stripped) = s.strip_prefix("0x") {
-            stripped
-        } else if let Some(stripped) = s.strip_prefix("0X") {
-            stripped
-        } else {
-            return Err(MissingPrefixError::new(s).into());
-        };
-
-        let lock_time = parse::hex_u32(stripped)?;
-        Ok(Self::from_consensus(lock_time))
-    }
-
-    /// Creates a `LockTime` from an unprefixed hex string.
-    pub fn from_unprefixed_hex(s: &str) -> Result<Self, UnprefixedHexError> {
-        if s.starts_with("0x") || s.starts_with("0X") {
-            return Err(ContainsPrefixError::new(s).into());
-        }
-        let lock_time = parse::hex_u32(s)?;
-        Ok(Self::from_consensus(lock_time))
-    }
-
     /// Constructs a `LockTime` from an nLockTime value or the argument to OP_CHEKCLOCKTIMEVERIFY.
     ///
     /// # Examples
@@ -347,6 +325,16 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     }
 }
 
+impl FromHexStr for LockTime {
+    type Error = ParseIntError;
+
+    #[inline]
+    fn from_hex_str_no_prefix<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error> {
+        let packed_lock_time = crate::parse::hex_u32(s)?;
+        Ok(Self::from_consensus(packed_lock_time))
+    }
+}
+
 impl Encodable for LockTime {
     #[inline]
     fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
@@ -431,11 +419,6 @@ impl Height {
     /// The maximum absolute block height.
     pub const MAX: Self = Height(LOCK_TIME_THRESHOLD - 1);
 
-    /// Creates a `Height` from a hex string.
-    ///
-    /// The input string is may or may not contain a typical hex prefix e.g., `0x`.
-    pub fn from_hex(s: &str) -> Result<Self, ParseHeightError> { parse_hex(s, Self::from_consensus) }
-
     /// Constructs a new block height.
     ///
     /// # Errors
@@ -477,6 +460,15 @@ impl fmt::Display for Height {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
 }
 
+impl FromHexStr for Height {
+    type Error = ParseHeightError;
+
+    #[inline]
+    fn from_hex_str_no_prefix<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error> {
+        parse_hex(s, Self::from_consensus)
+    }
+}
+
 impl_parse_str!(Height, ParseHeightError, parser(Height::from_consensus));
 
 /// Error returned when parsing block height fails.
@@ -516,11 +508,6 @@ impl Time {
     /// The maximum absolute block time (Sun Feb 07 2106 06:28:15 GMT+0000).
     pub const MAX: Self = Time(u32::max_value());
 
-    /// Creates a `Time` from a hex string.
-    ///
-    /// The input string is may or may not contain a typical hex prefix e.g., `0x`.
-    pub fn from_hex(s: &str) -> Result<Self, ParseTimeError> { parse_hex(s, Self::from_consensus) }
-
     /// Constructs a new block time.
     ///
     /// # Errors
@@ -562,6 +549,15 @@ impl fmt::Display for Time {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
 }
 
+impl FromHexStr for Time {
+    type Error = ParseTimeError;
+
+    #[inline]
+    fn from_hex_str_no_prefix<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error> {
+        parse_hex(s, Self::from_consensus)
+    }
+}
+
 impl_parse_str!(Time, ParseTimeError, parser(Time::from_consensus));
 
 /// Error returned when parsing block time fails.
@@ -603,7 +599,7 @@ fn parse_hex<T, E, S, F>(s: S, f: F) -> Result<T, E>
     S: AsRef<str> + Into<String>,
     F: FnOnce(u32) -> Result<T, ConversionError>,
 {
-    let n = i64::from_str_radix(parse::strip_hex_prefix(s.as_ref()), 16).map_err(ParseError::invalid_int(s))?;
+    let n = i64::from_str_radix(s.as_ref(), 16).map_err(ParseError::invalid_int(s))?;
     let n = u32::try_from(n).map_err(|_| ParseError::Conversion(n))?;
     f(n).map_err(ParseError::from).map_err(Into::into)
 }
@@ -626,8 +622,6 @@ pub enum Error {
     Parse(ParseIntError),
 }
 
-internals::impl_from_infallible!(Error);
-
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use Error::*;
@@ -725,8 +719,6 @@ pub enum OperationError {
     InvalidComparison,
 }
 
-internals::impl_from_infallible!(OperationError);
-
 impl fmt::Display for OperationError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use OperationError::*;
@@ -758,8 +750,6 @@ enum ParseError {
     Conversion(i64),
 }
 
-internals::impl_from_infallible!(ParseError);
-
 impl ParseError {
     fn invalid_int<S: Into<String>>(s: S) -> impl FnOnce(core::num::ParseIntError) -> Self {
         move |source| Self::InvalidInteger { source, input: s.into() }
@@ -830,73 +820,62 @@ fn display_and_alternate() {
     }
 
     #[test]
-    fn lock_time_from_hex_lower() {
-        let lock = LockTime::from_hex("0x6289c350").unwrap();
-        assert_eq!(lock, LockTime::from_consensus(0x6289C350));
-    }
-
-    #[test]
-    fn lock_time_from_hex_upper() {
-        let lock = LockTime::from_hex("0X6289C350").unwrap();
-        assert_eq!(lock, LockTime::from_consensus(0x6289C350));
-    }
-
-    #[test]
-    fn lock_time_from_unprefixed_hex_lower() {
-        let lock = LockTime::from_unprefixed_hex("6289c350").unwrap();
-        assert_eq!(lock, LockTime::from_consensus(0x6289C350));
+    fn time_from_str_hex_happy_path() {
+        let actual = Time::from_hex_str("0x6289C350").unwrap();
+        let expected = Time::from_consensus(0x6289C350).unwrap();
+        assert_eq!(actual, expected);
     }
 
     #[test]
-    fn lock_time_from_unprefixed_hex_upper() {
-        let lock = LockTime::from_unprefixed_hex("6289C350").unwrap();
-        assert_eq!(lock, LockTime::from_consensus(0x6289C350));
+    fn time_from_str_hex_no_prefix_happy_path() {
+        let time = Time::from_hex_str_no_prefix("6289C350").unwrap();
+        assert_eq!(time, Time(0x6289C350));
     }
 
     #[test]
-    fn lock_time_from_invalid_hex_should_err() {
+    fn time_from_str_hex_invalid_hex_should_err() {
         let hex = "0xzb93";
-        let result = LockTime::from_hex(hex);
+        let result = Time::from_hex_str(hex);
         assert!(result.is_err());
     }
 
     #[test]
-    fn time_from_str_hex_happy_path() {
-        let actual = Time::from_hex("0x6289C350").unwrap();
-        let expected = Time::from_consensus(0x6289C350).unwrap();
+    fn packed_lock_time_from_str_hex_happy_path() {
+        let actual = LockTime::from_hex_str("0xBA70D").unwrap();
+        let expected = LockTime::from_consensus(0xBA70D);
         assert_eq!(actual, expected);
     }
 
     #[test]
-    fn time_from_str_hex_no_prefix_happy_path() {
-        let time = Time::from_hex("6289C350").unwrap();
-        assert_eq!(time, Time(0x6289C350));
+    fn packed_lock_time_from_str_hex_no_prefix_happy_path() {
+        let lock_time = LockTime::from_hex_str_no_prefix("BA70D").unwrap();
+        assert_eq!(lock_time, LockTime::from_consensus(0xBA70D));
     }
 
     #[test]
-    fn time_from_str_hex_invalid_hex_should_err() {
+    fn packed_lock_time_from_str_hex_invalid_hex_should_ergr() {
         let hex = "0xzb93";
-        let result = Time::from_hex(hex);
+        let result = LockTime::from_hex_str(hex);
         assert!(result.is_err());
     }
 
     #[test]
     fn height_from_str_hex_happy_path() {
-        let actual = Height::from_hex("0xBA70D").unwrap();
+        let actual = Height::from_hex_str("0xBA70D").unwrap();
         let expected = Height(0xBA70D);
         assert_eq!(actual, expected);
     }
 
     #[test]
     fn height_from_str_hex_no_prefix_happy_path() {
-        let height = Height::from_hex("BA70D").unwrap();
+        let height = Height::from_hex_str_no_prefix("BA70D").unwrap();
         assert_eq!(height, Height(0xBA70D));
     }
 
     #[test]
     fn height_from_str_hex_invalid_hex_should_err() {
         let hex = "0xzb93";
-        let result = Height::from_hex(hex);
+        let result = Height::from_hex_str(hex);
         assert!(result.is_err());
     }
 
diff --git a/bitcoin/src/blockdata/locktime/relative.rs b/bitcoin/src/blockdata/locktime/relative.rs
index ad3b140b..8a0496cb 100644
--- a/bitcoin/src/blockdata/locktime/relative.rs
+++ b/bitcoin/src/blockdata/locktime/relative.rs
@@ -123,12 +123,12 @@ pub fn is_implied_by(&self, other: LockTime) -> bool {
     /// ```
     #[inline]
     #[cfg_attr(all(test, mutate), mutate)]
-    pub fn is_satisfied_by_height(&self, height: Height) -> Result<bool, IncompatibleHeightError> {
+    pub fn is_satisfied_by_height(&self, h: Height) -> Result<bool, Error> {
         use LockTime::*;
 
         match *self {
-            Blocks(ref h) => Ok(h.value() <= height.value()),
-            Time(time) => Err(IncompatibleHeightError { height, time })
+            Blocks(ref height) => Ok(height.value() <= h.value()),
+            Time(ref time) => Err(Error::IncompatibleTime(*self, *time)),
         }
     }
 
@@ -150,12 +150,12 @@ pub fn is_satisfied_by_height(&self, height: Height) -> Result<bool, Incompatibl
     /// ```
     #[inline]
     #[cfg_attr(all(test, mutate), mutate)]
-    pub fn is_satisfied_by_time(&self, time: Time) -> Result<bool, IncompatibleTimeError> {
+    pub fn is_satisfied_by_time(&self, t: Time) -> Result<bool, Error> {
         use LockTime::*;
 
         match *self {
-            Time(ref t) => Ok(t.value() <= time.value()),
-            Blocks(height) => Err(IncompatibleTimeError { time, height })
+            Time(ref time) => Ok(time.value() <= t.value()),
+            Blocks(ref height) => Err(Error::IncompatibleHeight(*self, *height)),
         }
     }
 }
@@ -251,11 +251,11 @@ pub fn from_512_second_intervals(intervals: u16) -> Self { Time(intervals) }
     ///
     /// Will return an error if the input cannot be encoded in 16 bits.
     #[inline]
-    pub fn from_seconds_ceil(seconds: u32) -> Result<Self, TimeOverflowError> {
+    pub fn from_seconds_ceil(seconds: u32) -> Result<Self, Error> {
         if let Ok(interval) = u16::try_from((seconds + 511) / 512) {
             Ok(Time::from_512_second_intervals(interval))
         } else {
-            Err(TimeOverflowError { seconds })
+            Err(Error::IntegerOverflow(seconds))
         }
     }
 
@@ -270,61 +270,47 @@ impl fmt::Display for Time {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
 }
 
-/// 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.
-    // Private because we maintain an invariant that the `seconds` value does actually overflow.
-    pub(crate) seconds: u32
-}
-
-impl fmt::Display for TimeOverflowError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{} seconds is too large to be encoded to a 16 bit 512 second interval", self.seconds)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for TimeOverflowError {}
-
-/// Tried to satisfy a lock-by-blocktime lock using a height value.
+/// Errors related to relative lock times.
 #[derive(Debug, Clone, PartialEq, Eq)]
 #[non_exhaustive]
-pub struct IncompatibleHeightError {
-    /// Attempted to satisfy a lock-by-blocktime lock with this height.
-    pub height: Height,
-    /// The inner time value of the lock-by-blocktime lock.
-    pub time: Time,
+pub enum Error {
+    /// Input time in seconds was too large to be encoded to a 16 bit 512 second interval.
+    IntegerOverflow(u32),
+    /// Tried to satisfy a lock-by-blocktime lock using a height value.
+    IncompatibleHeight(LockTime, Height),
+    /// Tried to satisfy a lock-by-blockheight lock using a time value.
+    IncompatibleTime(LockTime, Time),
 }
 
-impl fmt::Display for IncompatibleHeightError {
+impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "tried to satisfy a lock-by-blocktime lock {} with height: {}", self.time, self.height)
+        use Error::*;
+
+        match *self {
+            IntegerOverflow(val) => write!(
+                f,
+                "{} seconds is too large to be encoded to a 16 bit 512 second interval",
+                val
+            ),
+            IncompatibleHeight(lock, height) =>
+                write!(f, "tried to satisfy lock {} with height: {}", lock, height),
+            IncompatibleTime(lock, time) =>
+                write!(f, "tried to satisfy lock {} with time: {}", lock, time),
+        }
     }
 }
 
 #[cfg(feature = "std")]
-impl std::error::Error for IncompatibleHeightError {}
-
-/// Tried to satisfy a lock-by-blockheight lock using a time value.
-#[derive(Debug, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub struct IncompatibleTimeError {
-    /// Attempted to satisfy a lock-by-blockheight lock with this time.
-    pub time: Time,
-    /// The inner height value of the lock-by-blockheight lock.
-    pub height: Height,
-}
+impl std::error::Error for Error {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        use Error::*;
 
-impl fmt::Display for IncompatibleTimeError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "tried to satisfy a lock-by-blockheight lock {} with time: {}", self.height, self.time)
+        match *self {
+            IntegerOverflow(_) | IncompatibleHeight(_, _) | IncompatibleTime(_, _) => None,
+        }
     }
 }
 
-#[cfg(feature = "std")]
-impl std::error::Error for IncompatibleTimeError {}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/bitcoin/src/blockdata/script/borrowed.rs b/bitcoin/src/blockdata/script/borrowed.rs
index a4bb14fb..b8af7cf6 100644
--- a/bitcoin/src/blockdata/script/borrowed.rs
+++ b/bitcoin/src/blockdata/script/borrowed.rs
@@ -59,7 +59,7 @@
 /// ## Memory safety
 ///
 /// The type is `#[repr(transparent)]` for internal purposes only!
-/// No consumer crate may rely on the representation of the struct!
+/// No consumer crate may rely on the represenation of the struct!
 ///
 /// ## References
 ///
@@ -441,7 +441,7 @@ fn minimal_non_dust_inner(&self, dust_relay_fee: u64) -> crate::Amount {
     /// In Bitcoin Core, there are two ways to count sigops, "accurate" and "legacy".
     /// This method uses "accurate" counting. This means that OP_CHECKMULTISIG and its
     /// verify variant count for N sigops where N is the number of pubkeys used in the
-    /// multisig. However, it will count for 20 sigops if CHECKMULTISIG is not preceded by an
+    /// multisig. However, it will count for 20 sigops if CHECKMULTISIG is not preceeded by an
     /// OP_PUSHNUM from 1 - 16 (this would be an invalid script)
     ///
     /// Bitcoin Core uses accurate counting for sigops contained within redeemScripts (P2SH)
diff --git a/bitcoin/src/blockdata/script/mod.rs b/bitcoin/src/blockdata/script/mod.rs
index d0662d99..697ddbc2 100644
--- a/bitcoin/src/blockdata/script/mod.rs
+++ b/bitcoin/src/blockdata/script/mod.rs
@@ -699,8 +699,6 @@ pub enum Error {
     Serialization,
 }
 
-internals::impl_from_infallible!(Error);
-
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use Error::*;
@@ -739,8 +737,6 @@ enum UintError {
     NumericOverflow,
 }
 
-internals::impl_from_infallible!(UintError);
-
 impl From<UintError> for Error {
     fn from(error: UintError) -> Self {
         match error {
diff --git a/bitcoin/src/blockdata/script/witness_program.rs b/bitcoin/src/blockdata/script/witness_program.rs
index 9b76becc..7572c515 100644
--- a/bitcoin/src/blockdata/script/witness_program.rs
+++ b/bitcoin/src/blockdata/script/witness_program.rs
@@ -135,8 +135,6 @@ pub enum Error {
     InvalidSegwitV0Length(usize),
 }
 
-internals::impl_from_infallible!(Error);
-
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use Error::*;
diff --git a/bitcoin/src/blockdata/script/witness_version.rs b/bitcoin/src/blockdata/script/witness_version.rs
index a172a60d..943569fc 100644
--- a/bitcoin/src/blockdata/script/witness_version.rs
+++ b/bitcoin/src/blockdata/script/witness_version.rs
@@ -175,8 +175,6 @@ pub enum FromStrError {
     Invalid(TryFromError),
 }
 
-internals::impl_from_infallible!(FromStrError);
-
 impl fmt::Display for FromStrError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use FromStrError::*;
@@ -218,8 +216,6 @@ pub enum TryFromInstructionError {
     DataPush,
 }
 
-internals::impl_from_infallible!(TryFromInstructionError);
-
 impl fmt::Display for TryFromInstructionError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use TryFromInstructionError::*;
@@ -249,10 +245,14 @@ fn from(e: TryFromError) -> Self { Self::TryFrom(e) }
 
 /// Error attempting to create a [`WitnessVersion`] from an integer.
 #[derive(Clone, Debug, PartialEq, Eq)]
-#[non_exhaustive]
 pub struct TryFromError {
     /// The invalid non-witness version integer.
-    pub invalid: u8,
+    invalid: u8,
+}
+
+impl TryFromError {
+    /// Returns the invalid non-witness version integer.
+    pub fn invalid_version(&self) -> u8 { self.invalid }
 }
 
 impl fmt::Display for TryFromError {
@@ -262,6 +262,4 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 }
 
 #[cfg(feature = "std")]
-impl std::error::Error for TryFromError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
-}
+impl std::error::Error for TryFromError {}
diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index d2e0bfb9..7674824c 100644
--- a/bitcoin/src/blockdata/transaction.rs
+++ b/bitcoin/src/blockdata/transaction.rs
@@ -19,16 +19,16 @@
 
 use super::Weight;
 use crate::blockdata::locktime::absolute::{self, Height, Time};
-use crate::blockdata::locktime::relative::{self, TimeOverflowError};
+use crate::blockdata::locktime::relative;
 use crate::blockdata::script::{Script, ScriptBuf};
 use crate::blockdata::witness::Witness;
 use crate::blockdata::FeeRate;
 use crate::consensus::{encode, Decodable, Encodable};
-use crate::error::{PrefixedHexError, UnprefixedHexError, ContainsPrefixError, MissingPrefixError};
 use crate::internal_macros::{impl_consensus_encoding, impl_hashencode};
-use crate::parse::{self, impl_parse_str_from_int_infallible};
+use crate::parse::impl_parse_str_from_int_infallible;
 #[cfg(doc)]
 use crate::sighash::{EcdsaSighashType, TapSighashType};
+use crate::string::FromHexStr;
 use crate::prelude::*;
 use crate::{Amount, SignedAmount, VarInt};
 
@@ -130,8 +130,6 @@ pub enum ParseOutPointError {
     VoutNotCanonical,
 }
 
-internals::impl_from_infallible!(ParseOutPointError);
-
 impl fmt::Display for ParseOutPointError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use ParseOutPointError::*;
@@ -404,29 +402,6 @@ pub fn is_time_locked(&self) -> bool {
         self.is_relative_lock_time() & (self.0 & Sequence::LOCK_TYPE_MASK > 0)
     }
 
-    /// Creates a `Sequence` from an prefixed hex string.
-    pub fn from_hex(s: &str) -> Result<Self, PrefixedHexError> {
-        let stripped = if let Some(stripped) = s.strip_prefix("0x") {
-            stripped
-        } else if let Some(stripped) = s.strip_prefix("0X") {
-            stripped
-        } else {
-            return Err(MissingPrefixError::new(s).into());
-        };
-
-        let sequence = parse::hex_u32(stripped)?;
-        Ok(Self::from_consensus(sequence))
-    }
-
-    /// Creates a `Sequence` from an unprefixed hex string.
-    pub fn from_unprefixed_hex(s: &str) -> Result<Self, UnprefixedHexError> {
-        if s.starts_with("0x") || s.starts_with("0X") {
-            return Err(ContainsPrefixError::new(s).into());
-        }
-        let lock_time = parse::hex_u32(s)?;
-        Ok(Self::from_consensus(lock_time))
-    }
-
     /// Creates a relative lock-time using block height.
     #[inline]
     pub fn from_height(height: u16) -> Self { Sequence(u32::from(height)) }
@@ -445,11 +420,11 @@ pub fn from_512_second_intervals(intervals: u16) -> Self {
     ///
     /// Will return an error if the input cannot be encoded in 16 bits.
     #[inline]
-    pub fn from_seconds_floor(seconds: u32) -> Result<Self, TimeOverflowError> {
+    pub fn from_seconds_floor(seconds: u32) -> Result<Self, relative::Error> {
         if let Ok(interval) = u16::try_from(seconds / 512) {
             Ok(Sequence::from_512_second_intervals(interval))
         } else {
-            Err(TimeOverflowError { seconds })
+            Err(relative::Error::IntegerOverflow(seconds))
         }
     }
 
@@ -458,11 +433,11 @@ pub fn from_seconds_floor(seconds: u32) -> Result<Self, TimeOverflowError> {
     ///
     /// Will return an error if the input cannot be encoded in 16 bits.
     #[inline]
-    pub fn from_seconds_ceil(seconds: u32) -> Result<Self, TimeOverflowError> {
+    pub fn from_seconds_ceil(seconds: u32) -> Result<Self, relative::Error> {
         if let Ok(interval) = u16::try_from((seconds + 511) / 512) {
             Ok(Sequence::from_512_second_intervals(interval))
         } else {
-            Err(TimeOverflowError { seconds })
+            Err(relative::Error::IntegerOverflow(seconds))
         }
     }
 
@@ -498,6 +473,15 @@ pub fn to_relative_lock_time(&self) -> Option<relative::LockTime> {
     fn low_u16(&self) -> u16 { self.0 as u16 }
 }
 
+impl FromHexStr for Sequence {
+    type Error = crate::parse::ParseIntError;
+
+    fn from_hex_str_no_prefix<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error> {
+        let sequence = crate::parse::hex_u32(s)?;
+        Ok(Self::from_consensus(sequence))
+    }
+}
+
 impl Default for Sequence {
     /// The default value of sequence is 0xffffffff.
     fn default() -> Self { Sequence::MAX }
@@ -1960,10 +1944,6 @@ fn txid() {
             format!("{:x}", tx.compute_txid()),
             "9652aa62b0e748caeec40c4cb7bc17c6792435cc3dfe447dd1ca24f912a1c6ec"
         );
-        assert_eq!(
-            format!("{:.10x}", tx.compute_txid()),
-            "9652aa62b0"
-        );
         assert_eq!(tx.weight(), Weight::from_wu(2718));
 
         // non-segwit tx from my mempool
@@ -2139,33 +2119,21 @@ fn sequence_number() {
     }
 
     #[test]
-    fn sequence_from_hex_lower() {
-        let sequence = Sequence::from_hex("0xffffffff").unwrap();
-        assert_eq!(sequence, Sequence::MAX);
-    }
-
-    #[test]
-    fn sequence_from_hex_upper() {
-        let sequence = Sequence::from_hex("0XFFFFFFFF").unwrap();
-        assert_eq!(sequence, Sequence::MAX);
-    }
-
-    #[test]
-    fn sequence_from_unprefixed_hex_lower() {
-        let sequence = Sequence::from_unprefixed_hex("ffffffff").unwrap();
+    fn sequence_from_str_hex_happy_path() {
+        let sequence = Sequence::from_hex_str("0xFFFFFFFF").unwrap();
         assert_eq!(sequence, Sequence::MAX);
     }
 
     #[test]
-    fn sequence_from_unprefixed_hex_upper() {
-        let sequence = Sequence::from_unprefixed_hex("FFFFFFFF").unwrap();
+    fn sequence_from_str_hex_no_prefix_happy_path() {
+        let sequence = Sequence::from_hex_str_no_prefix("FFFFFFFF").unwrap();
         assert_eq!(sequence, Sequence::MAX);
     }
 
     #[test]
     fn sequence_from_str_hex_invalid_hex_should_err() {
         let hex = "0xzb93";
-        let result = Sequence::from_hex(hex);
+        let result = Sequence::from_hex_str(hex);
         assert!(result.is_err());
     }
 
diff --git a/bitcoin/src/blockdata/witness.rs b/bitcoin/src/blockdata/witness.rs
index d48d3b38..2974de43 100644
--- a/bitcoin/src/blockdata/witness.rs
+++ b/bitcoin/src/blockdata/witness.rs
@@ -506,19 +506,18 @@ fn visit_seq<A: serde::de::SeqAccess<'de>>(
 
                 while let Some(elem) = a.next_element::<String>()? {
                     let vec = Vec::<u8>::from_hex(&elem).map_err(|e| match e {
-                        InvalidChar(ref e) => match core::char::from_u32(e.invalid_char(
-                        ).into()) {
+                        InvalidChar(b) => match core::char::from_u32(b.into()) {
                             Some(c) => de::Error::invalid_value(
                                 Unexpected::Char(c),
                                 &"a valid hex character",
                             ),
                             None => de::Error::invalid_value(
-                                Unexpected::Unsigned(e.invalid_char().into()),
+                                Unexpected::Unsigned(b.into()),
                                 &"a valid hex character",
                             ),
                         },
-                        OddLengthString(ref e) =>
-                            de::Error::invalid_length(e.length(), &"an even length string"),
+                        OddLengthString(len) =>
+                            de::Error::invalid_length(len, &"an even length string"),
                     })?;
                     ret.push(vec);
                 }
diff --git a/bitcoin/src/consensus/encode.rs b/bitcoin/src/consensus/encode.rs
index fc5626b7..3fc036dd 100644
--- a/bitcoin/src/consensus/encode.rs
+++ b/bitcoin/src/consensus/encode.rs
@@ -61,8 +61,6 @@ pub enum Error {
     UnsupportedSegwitFlag(u8),
 }
 
-internals::impl_from_infallible!(Error);
-
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use Error::*;
diff --git a/bitcoin/src/consensus/params.rs b/bitcoin/src/consensus/params.rs
index ee309349..f181ab55 100644
--- a/bitcoin/src/consensus/params.rs
+++ b/bitcoin/src/consensus/params.rs
@@ -49,80 +49,65 @@ pub struct Params {
 }
 
 impl Params {
-    /// The mainnet parameters (alias for `Params::MAINNET`).
-    pub const BITCOIN: Params = Params::MAINNET;
-
-    /// The mainnet parameters.
-    pub const MAINNET: Params = Params {
-        network: Network::Bitcoin,
-        bip16_time: 1333238400,                 // Apr 1 2012
-        bip34_height: 227931, // 000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8
-        bip65_height: 388381, // 000000000000000004c2b624ed5d7756c508d90fd0da2c7c679febfa6c4735f0
-        bip66_height: 363725, // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931
-        rule_change_activation_threshold: 1916, // 95%
-        miner_confirmation_window: 2016,
-        pow_limit: Target::MAX_ATTAINABLE_MAINNET,
-        pow_target_spacing: 10 * 60,            // 10 minutes.
-        pow_target_timespan: 14 * 24 * 60 * 60, // 2 weeks.
-        allow_min_difficulty_blocks: false,
-        no_pow_retargeting: false,
-    };
-
-    /// The testnet parameters.
-    pub const TESTNET: Params = Params {
-        network: Network::Testnet,
-        bip16_time: 1333238400,                 // Apr 1 2012
-        bip34_height: 21111, // 0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8
-        bip65_height: 581885, // 00000000007f6655f22f98e72ed80d8b06dc761d5da09df0fa1dc4be4f861eb6
-        bip66_height: 330776, // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182
-        rule_change_activation_threshold: 1512, // 75%
-        miner_confirmation_window: 2016,
-        pow_limit: Target::MAX_ATTAINABLE_TESTNET,
-        pow_target_spacing: 10 * 60,            // 10 minutes.
-        pow_target_timespan: 14 * 24 * 60 * 60, // 2 weeks.
-        allow_min_difficulty_blocks: true,
-        no_pow_retargeting: false,
-    };
-
-    /// The signet parameters.
-    pub const SIGNET: Params = Params {
-        network: Network::Signet,
-        bip16_time: 1333238400, // Apr 1 2012
-        bip34_height: 1,
-        bip65_height: 1,
-        bip66_height: 1,
-        rule_change_activation_threshold: 1916, // 95%
-        miner_confirmation_window: 2016,
-        pow_limit: Target::MAX_ATTAINABLE_SIGNET,
-        pow_target_spacing: 10 * 60,            // 10 minutes.
-        pow_target_timespan: 14 * 24 * 60 * 60, // 2 weeks.
-        allow_min_difficulty_blocks: false,
-        no_pow_retargeting: false,
-    };
-
-    /// The regtest parameters.
-    pub const REGTEST: Params = Params {
-        network: Network::Regtest,
-        bip16_time: 1333238400,  // Apr 1 2012
-        bip34_height: 100000000, // not activated on regtest
-        bip65_height: 1351,
-        bip66_height: 1251,                    // used only in rpc tests
-        rule_change_activation_threshold: 108, // 75%
-        miner_confirmation_window: 144,
-        pow_limit: Target::MAX_ATTAINABLE_REGTEST,
-        pow_target_spacing: 10 * 60,            // 10 minutes.
-        pow_target_timespan: 14 * 24 * 60 * 60, // 2 weeks.
-        allow_min_difficulty_blocks: true,
-        no_pow_retargeting: true,
-    };
-
-    /// Creates parameters set for the given network.    /// Creates parameters set for the given network.
+    /// Creates parameters set for the given network.
     pub const fn new(network: Network) -> Self {
         match network {
-            Network::Bitcoin => Params::MAINNET,
-            Network::Testnet => Params::TESTNET,
-            Network::Signet => Params::SIGNET,
-            Network::Regtest => Params::REGTEST,
+            Network::Bitcoin => Params {
+                network: Network::Bitcoin,
+                bip16_time: 1333238400,                 // Apr 1 2012
+                bip34_height: 227931, // 000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8
+                bip65_height: 388381, // 000000000000000004c2b624ed5d7756c508d90fd0da2c7c679febfa6c4735f0
+                bip66_height: 363725, // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931
+                rule_change_activation_threshold: 1916, // 95%
+                miner_confirmation_window: 2016,
+                pow_limit: Target::MAX_ATTAINABLE_MAINNET,
+                pow_target_spacing: 10 * 60,            // 10 minutes.
+                pow_target_timespan: 14 * 24 * 60 * 60, // 2 weeks.
+                allow_min_difficulty_blocks: false,
+                no_pow_retargeting: false,
+            },
+            Network::Testnet => Params {
+                network: Network::Testnet,
+                bip16_time: 1333238400,                 // Apr 1 2012
+                bip34_height: 21111, // 0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8
+                bip65_height: 581885, // 00000000007f6655f22f98e72ed80d8b06dc761d5da09df0fa1dc4be4f861eb6
+                bip66_height: 330776, // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182
+                rule_change_activation_threshold: 1512, // 75%
+                miner_confirmation_window: 2016,
+                pow_limit: Target::MAX_ATTAINABLE_TESTNET,
+                pow_target_spacing: 10 * 60,            // 10 minutes.
+                pow_target_timespan: 14 * 24 * 60 * 60, // 2 weeks.
+                allow_min_difficulty_blocks: true,
+                no_pow_retargeting: false,
+            },
+            Network::Signet => Params {
+                network: Network::Signet,
+                bip16_time: 1333238400, // Apr 1 2012
+                bip34_height: 1,
+                bip65_height: 1,
+                bip66_height: 1,
+                rule_change_activation_threshold: 1916, // 95%
+                miner_confirmation_window: 2016,
+                pow_limit: Target::MAX_ATTAINABLE_SIGNET,
+                pow_target_spacing: 10 * 60,            // 10 minutes.
+                pow_target_timespan: 14 * 24 * 60 * 60, // 2 weeks.
+                allow_min_difficulty_blocks: false,
+                no_pow_retargeting: false,
+            },
+            Network::Regtest => Params {
+                network: Network::Regtest,
+                bip16_time: 1333238400,  // Apr 1 2012
+                bip34_height: 100000000, // not activated on regtest
+                bip65_height: 1351,
+                bip66_height: 1251,                    // used only in rpc tests
+                rule_change_activation_threshold: 108, // 75%
+                miner_confirmation_window: 144,
+                pow_limit: Target::MAX_ATTAINABLE_REGTEST,
+                pow_target_spacing: 10 * 60,            // 10 minutes.
+                pow_target_timespan: 14 * 24 * 60 * 60, // 2 weeks.
+                allow_min_difficulty_blocks: true,
+                no_pow_retargeting: true,
+            },
         }
     }
 
@@ -136,7 +121,6 @@ impl From<Network> for Params {
     fn from(value: Network) -> Self { Self::new(value) }
 }
 
-
 impl From<&Network> for Params {
     fn from(value: &Network) -> Self { Self::new(*value) }
 }
diff --git a/bitcoin/src/consensus/serde.rs b/bitcoin/src/consensus/serde.rs
index 5a096e6c..a6b88aed 100644
--- a/bitcoin/src/consensus/serde.rs
+++ b/bitcoin/src/consensus/serde.rs
@@ -71,11 +71,11 @@ impl Case for super::Upper {
 
     /// Hex byte encoder.
     // We wrap `BufEncoder` to not leak internal representation.
-    pub struct Encoder<C: Case>(BufEncoder<{ HEX_BUF_SIZE }>, PhantomData<C>);
+    pub struct Encoder<C: Case>(BufEncoder<[u8; HEX_BUF_SIZE]>, PhantomData<C>);
 
     impl<C: Case> From<super::Hex<C>> for Encoder<C> {
         fn from(_: super::Hex<C>) -> Self {
-            Encoder(BufEncoder::new(), Default::default())
+            Encoder(BufEncoder::new([0; HEX_BUF_SIZE]), Default::default())
         }
     }
 
@@ -100,15 +100,15 @@ fn flush<W: fmt::Write>(&mut self, writer: &mut W) -> fmt::Result {
     // Newtypes to hide internal details.
 
     /// Error returned when a hex string decoder can't be created.
-    #[derive(Debug, Clone, PartialEq, Eq)]
-    pub struct DecodeInitError(hex::OddLengthStringError);
+    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+    pub struct DecodeInitError(hex::HexToBytesError);
 
     /// Error returned when a hex string contains invalid characters.
-    #[derive(Debug, Clone, PartialEq, Eq)]
-    pub struct DecodeError(hex::InvalidCharError);
+    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+    pub struct DecodeError(hex::HexToBytesError);
 
     /// Hex decoder state.
-    pub struct Decoder<'a>(hex::HexSliceToBytesIter<'a>);
+    pub struct Decoder<'a>(hex::HexToBytesIter<'a>);
 
     impl<'a> Decoder<'a> {
         fn new(s: &'a str) -> Result<Self, DecodeInitError> {
@@ -137,19 +137,29 @@ fn from_str(s: &'a str) -> Result<Self::Decoder, Self::InitError> { Decoder::new
 
     impl super::IntoDeError for DecodeInitError {
         fn into_de_error<E: serde::de::Error>(self) -> E {
-            E::invalid_length(self.0.length(), &"an even number of ASCII-encoded hex digits")
+            use hex::HexToBytesError;
+
+            match self.0 {
+                HexToBytesError::OddLengthString(len) =>
+                    E::invalid_length(len, &"an even number of ASCII-encoded hex digits"),
+                error => panic!("unexpected error: {:?}", error),
+            }
         }
     }
 
     impl super::IntoDeError for DecodeError {
         fn into_de_error<E: serde::de::Error>(self) -> E {
+            use hex::HexToBytesError;
             use serde::de::Unexpected;
 
             const EXPECTED_CHAR: &str = "an ASCII-encoded hex digit";
 
-            match self.0.invalid_char() {
-                c if c.is_ascii() => E::invalid_value(Unexpected::Char(c as _), &EXPECTED_CHAR),
-                c => E::invalid_value(Unexpected::Unsigned(c.into()), &EXPECTED_CHAR),
+            match self.0 {
+                HexToBytesError::InvalidChar(c) if c.is_ascii() =>
+                    E::invalid_value(Unexpected::Char(c as _), &EXPECTED_CHAR),
+                HexToBytesError::InvalidChar(c) =>
+                    E::invalid_value(Unexpected::Unsigned(c.into()), &EXPECTED_CHAR),
+                error => panic!("unexpected error: {:?}", error),
             }
         }
     }
@@ -358,8 +368,6 @@ enum DecodeError<E> {
     Other(E),
 }
 
-internals::impl_from_infallible!(DecodeError<E>);
-
 // not a trait impl because we panic on some variants
 fn consensus_error_into_serde<E: serde::de::Error>(error: ConsensusError) -> E {
     match error {
diff --git a/bitcoin/src/consensus/validation.rs b/bitcoin/src/consensus/validation.rs
index 32b5d5d0..e55a0bd0 100644
--- a/bitcoin/src/consensus/validation.rs
+++ b/bitcoin/src/consensus/validation.rs
@@ -209,8 +209,6 @@ pub enum TxVerifyError {
     UnknownSpentOutput(OutPoint),
 }
 
-internals::impl_from_infallible!(TxVerifyError);
-
 impl fmt::Display for TxVerifyError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use TxVerifyError::*;
diff --git a/bitcoin/src/crypto/ecdsa.rs b/bitcoin/src/crypto/ecdsa.rs
index 4e1a8400..fd5cb4ae 100644
--- a/bitcoin/src/crypto/ecdsa.rs
+++ b/bitcoin/src/crypto/ecdsa.rs
@@ -213,8 +213,6 @@ pub enum Error {
     Secp256k1(secp256k1::Error),
 }
 
-internals::impl_from_infallible!(Error);
-
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use Error::*;
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index e3babd05..fd363813 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -10,7 +10,7 @@
 use core::str::FromStr;
 
 use hashes::{hash160, Hash};
-use hex::{FromHex, HexToArrayError};
+use hex::{FromHex, HexToArrayError, HexToBytesError};
 use internals::array_vec::ArrayVec;
 use internals::write_err;
 use io::{Read, Write};
@@ -233,22 +233,22 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 impl FromStr for PublicKey {
     type Err = ParsePublicKeyError;
     fn from_str(s: &str) -> Result<PublicKey, ParsePublicKeyError> {
-        use HexToArrayError::*;
-
         match s.len() {
             66 => {
-                let bytes = <[u8; 33]>::from_hex(s).map_err(|e| match e {
-                    InvalidChar(e) => ParsePublicKeyError::InvalidChar(e.invalid_char()),
-                    InvalidLength(_) => unreachable!("length checked already")
-                })?;
-                Ok(PublicKey::from_slice(&bytes).expect("length checked already"))
+                PublicKey::from_slice(&<[u8; 33]>::from_hex(s).map_err(|op| {
+                    match op {
+                        HexToArrayError::Conversion(HexToBytesError::InvalidChar(char)) => ParsePublicKeyError::InvalidChar(char),
+                        HexToArrayError::Conversion(HexToBytesError::OddLengthString(_)) | HexToArrayError::InvalidLength(_,_) => unreachable!("invalid length"),
+                    }
+                })?).map_err(From::from)
             },
             130 => {
-                let bytes = <[u8; 65]>::from_hex(s).map_err(|e| match e {
-                    InvalidChar(e) => ParsePublicKeyError::InvalidChar(e.invalid_char()),
-                    InvalidLength(_) => unreachable!("length checked already")
-                })?;
-                Ok(PublicKey::from_slice(&bytes).expect("length checked already"))
+                PublicKey::from_slice(&<[u8; 65]>::from_hex(s).map_err(|op| {
+                    match op {
+                        HexToArrayError::Conversion(HexToBytesError::InvalidChar(char)) => ParsePublicKeyError::InvalidChar(char),
+                        HexToArrayError::Conversion(HexToBytesError::OddLengthString(_)) | HexToArrayError::InvalidLength(_,_) => unreachable!("invalid length"),
+                    }
+                })?).map_err(From::from)
             }
             len => Err(ParsePublicKeyError::InvalidHexLength(len)),
         }
@@ -896,8 +896,6 @@ pub enum FromSliceError {
     InvalidLength(usize),
 }
 
-internals::impl_from_infallible!(FromSliceError);
-
 impl fmt::Display for FromSliceError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use FromSliceError::*;
@@ -936,8 +934,6 @@ pub enum FromWifError {
     Secp256k1(secp256k1::Error),
 }
 
-internals::impl_from_infallible!(FromWifError);
-
 impl fmt::Display for FromWifError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use FromWifError::*;
@@ -978,8 +974,6 @@ pub enum ParsePublicKeyError {
     InvalidHexLength(usize),
 }
 
-internals::impl_from_infallible!(ParsePublicKeyError);
-
 impl fmt::Display for ParsePublicKeyError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use ParsePublicKeyError::*;
@@ -1016,8 +1010,6 @@ pub enum ParseCompressedPublicKeyError {
     Hex(hex::HexToArrayError),
 }
 
-internals::impl_from_infallible!(ParseCompressedPublicKeyError);
-
 impl fmt::Display for ParseCompressedPublicKeyError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use ParseCompressedPublicKeyError::*;
diff --git a/bitcoin/src/crypto/sighash.rs b/bitcoin/src/crypto/sighash.rs
index 142a02ba..197f3728 100644
--- a/bitcoin/src/crypto/sighash.rs
+++ b/bitcoin/src/crypto/sighash.rs
@@ -275,8 +275,6 @@ pub enum PrevoutsIndexError {
     InvalidAllIndex,
 }
 
-internals::impl_from_infallible!(PrevoutsIndexError);
-
 impl fmt::Display for PrevoutsIndexError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use PrevoutsIndexError::*;
@@ -1153,8 +1151,6 @@ pub enum TaprootError {
     InvalidSighashType(u32),
 }
 
-internals::impl_from_infallible!(TaprootError);
-
 impl fmt::Display for TaprootError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use TaprootError::*;
@@ -1212,8 +1208,6 @@ pub enum P2wpkhError {
     NotP2wpkhScript,
 }
 
-internals::impl_from_infallible!(P2wpkhError);
-
 impl From<transaction::InputsIndexError> for P2wpkhError {
     fn from(value: transaction::InputsIndexError) -> Self {
         P2wpkhError::Sighash(value)
@@ -1279,8 +1273,6 @@ pub enum AnnexError {
     IncorrectPrefix(u8),
 }
 
-internals::impl_from_infallible!(AnnexError);
-
 impl fmt::Display for AnnexError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use AnnexError::*;
@@ -1387,8 +1379,6 @@ pub enum SigningDataError<E> {
     Sighash(E),
 }
 
-internals::impl_from_infallible!(SigningDataError<E>);
-
 impl<E> SigningDataError<E> {
     /// Returns the sighash variant, panicking if it's IO.
     ///
diff --git a/bitcoin/src/crypto/taproot.rs b/bitcoin/src/crypto/taproot.rs
index dee10f78..fb941e7c 100644
--- a/bitcoin/src/crypto/taproot.rs
+++ b/bitcoin/src/crypto/taproot.rs
@@ -97,8 +97,6 @@ pub enum SigFromSliceError {
     InvalidSignatureSize(usize),
 }
 
-internals::impl_from_infallible!(SigFromSliceError);
-
 impl fmt::Display for SigFromSliceError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use SigFromSliceError::*;
diff --git a/bitcoin/src/error.rs b/bitcoin/src/error.rs
index da9d6074..6adcbbb0 100644
--- a/bitcoin/src/error.rs
+++ b/bitcoin/src/error.rs
@@ -2,131 +2,6 @@
 
 //! Contains error types and other error handling tools.
 
-use core::fmt;
-
-use internals::write_err;
-
-use crate::prelude::*;
-
 #[rustfmt::skip]                // Keep public re-exports separate.
 #[doc(inline)]
 pub use crate::parse::ParseIntError;
-
-/// Error returned when parsing integer from an supposedly prefixed hex string for
-/// a type that can be created infallibly from an integer.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub enum PrefixedHexError {
-    /// Hex string is missing prefix.
-    MissingPrefix(MissingPrefixError),
-    /// Error parsing integer from hex string.
-    ParseInt(ParseIntError),
-}
-
-impl fmt::Display for PrefixedHexError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use PrefixedHexError::*;
-
-        match *self {
-            MissingPrefix(ref e) => write_err!(f, "hex string is missing prefix"; e),
-            ParseInt(ref e) => write_err!(f, "prefixed hex string invalid int"; e),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for PrefixedHexError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        use PrefixedHexError::*;
-
-        match *self {
-            MissingPrefix(ref e) => Some(e),
-            ParseInt(ref e) => Some(e),
-        }
-    }
-}
-
-impl From<MissingPrefixError> for PrefixedHexError {
-    fn from(e: MissingPrefixError) -> Self { Self::MissingPrefix(e) }
-}
-
-impl From<ParseIntError> for PrefixedHexError {
-    fn from(e: ParseIntError) -> Self { Self::ParseInt(e) }
-}
-
-/// Error returned when parsing integer from an supposedly un-prefixed hex string.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub enum UnprefixedHexError {
-    /// Hex string contains prefix.
-    ContainsPrefix(ContainsPrefixError),
-    /// Error parsing integer from string.
-    ParseInt(ParseIntError),
-}
-
-impl fmt::Display for UnprefixedHexError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use UnprefixedHexError::*;
-
-        match *self {
-            ContainsPrefix(ref e) => write_err!(f, "hex string is contains prefix"; e),
-            ParseInt(ref e) => write_err!(f, "hex string parse int"; e),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for UnprefixedHexError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        use UnprefixedHexError::*;
-
-        match *self {
-            ContainsPrefix(ref e) => Some(e),
-            ParseInt(ref e) => Some(e),
-        }
-    }
-}
-
-impl From<ContainsPrefixError> for UnprefixedHexError {
-    fn from(e: ContainsPrefixError) -> Self { Self::ContainsPrefix(e) }
-}
-
-impl From<ParseIntError> for UnprefixedHexError {
-    fn from(e: ParseIntError) -> Self { Self::ParseInt(e) }
-}
-
-/// Error when hex string is missing a prefix (e.g. 0x).
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct MissingPrefixError {
-    hex: String,
-}
-
-impl MissingPrefixError {
-    pub(crate) fn new(s: &str) -> Self { Self { hex: s.into() } }
-}
-
-impl fmt::Display for MissingPrefixError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "hex string is missing a prefix (e.g. 0x): {}", self.hex)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for MissingPrefixError {}
-
-/// Error when hex string contains a prefix (e.g. 0x).
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct ContainsPrefixError {
-    hex: String,
-}
-
-impl ContainsPrefixError {
-    pub(crate) fn new(s: &str) -> Self { Self { hex: s.into() } }
-}
-
-impl fmt::Display for ContainsPrefixError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "hex string contains a prefix: {}", self.hex)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for ContainsPrefixError {}
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index 70fc1048..a8540de1 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -111,6 +111,7 @@
 pub mod pow;
 pub mod psbt;
 pub mod sign_message;
+pub mod string;
 pub mod taproot;
 
 #[rustfmt::skip]                // Keep public re-exports separate.
diff --git a/bitcoin/src/merkle_tree/block.rs b/bitcoin/src/merkle_tree/block.rs
index 563966d7..8df17f2b 100644
--- a/bitcoin/src/merkle_tree/block.rs
+++ b/bitcoin/src/merkle_tree/block.rs
@@ -498,8 +498,6 @@ pub enum MerkleBlockError {
     IdenticalHashesFound,
 }
 
-internals::impl_from_infallible!(MerkleBlockError);
-
 impl fmt::Display for MerkleBlockError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use MerkleBlockError::*;
diff --git a/bitcoin/src/network.rs b/bitcoin/src/network.rs
index 4483bb8e..1dc3b106 100644
--- a/bitcoin/src/network.rs
+++ b/bitcoin/src/network.rs
@@ -347,7 +347,6 @@ fn service_flags_test() {
             ServiceFlags::WITNESS,
             ServiceFlags::COMPACT_FILTERS,
             ServiceFlags::NETWORK_LIMITED,
-            ServiceFlags::P2P_V2,
         ];
 
         let mut flags = ServiceFlags::NONE;
diff --git a/bitcoin/src/p2p/address.rs b/bitcoin/src/p2p/address.rs
index 2d417d2d..1383c86b 100644
--- a/bitcoin/src/p2p/address.rs
+++ b/bitcoin/src/p2p/address.rs
@@ -226,7 +226,7 @@ fn consensus_decode<R: BufRead + ?Sized>(r: &mut R) -> Result<Self, encode::Erro
                 }
                 let addr: [u16; 8] = read_be_address(r)?;
                 // check the first byte for the CJDNS marker
-                if addr[0] >> 8 != 0xFC{
+                if addr[0] != u16::from_be_bytes([0xFC, 0x00]) {
                     return Err(encode::Error::ParseFailed("Invalid CJDNS address"));
                 }
                 AddrV2::Cjdns(Ipv6Addr::new(
@@ -425,8 +425,8 @@ fn serialize_addrv2_test() {
             hex!("0520a2894dabaec08c0051a481a6dac88b64f98232ae42d4b6fd2fa81952dfe36a87")
         );
 
-        let ip = AddrV2::Cjdns(Ipv6Addr::from_str("fc01:1:2:3:4:5:6:7").unwrap());
-        assert_eq!(serialize(&ip), hex!("0610fc010001000200030004000500060007"));
+        let ip = AddrV2::Cjdns(Ipv6Addr::from_str("fc00:1:2:3:4:5:6:7").unwrap());
+        assert_eq!(serialize(&ip), hex!("0610fc000001000200030004000500060007"));
 
         let ip = AddrV2::Unknown(170, hex!("01020304"));
         assert_eq!(serialize(&ip), hex!("aa0401020304"));
diff --git a/bitcoin/src/p2p/mod.rs b/bitcoin/src/p2p/mod.rs
index 3d8709fa..ae42ae99 100644
--- a/bitcoin/src/p2p/mod.rs
+++ b/bitcoin/src/p2p/mod.rs
@@ -91,10 +91,6 @@ impl ServiceFlags {
     /// See BIP159 for details on how this is implemented.
     pub const NETWORK_LIMITED: ServiceFlags = ServiceFlags(1 << 10);
 
-    /// P2P_V2 indicates that the node supports the P2P v2 encrypted transport protocol.
-    /// See BIP324 for details on how this is implemented.
-    pub const P2P_V2: ServiceFlags = ServiceFlags(1 << 11);
-
     // NOTE: When adding new flags, remember to update the Display impl accordingly.
 
     /// Add [ServiceFlags] together.
@@ -154,7 +150,6 @@ macro_rules! write_flag {
         write_flag!(WITNESS);
         write_flag!(COMPACT_FILTERS);
         write_flag!(NETWORK_LIMITED);
-        write_flag!(P2P_V2);
         // If there are unknown flags left, we append them in hex.
         if flags != ServiceFlags::NONE {
             if !first {
diff --git a/bitcoin/src/parse.rs b/bitcoin/src/parse.rs
index 7e5b86c6..35eb4764 100644
--- a/bitcoin/src/parse.rs
+++ b/bitcoin/src/parse.rs
@@ -55,7 +55,7 @@ impl AsRef<core::num::ParseIntError> for ParseIntError {
     fn as_ref(&self) -> &core::num::ParseIntError { &self.source }
 }
 
-/// Not strictly necessary but serves as a lint - avoids weird behavior if someone accidentally
+/// Not strictly neccessary but serves as a lint - avoids weird behavior if someone accidentally
 /// passes non-integer to the `parse()` function.
 pub(crate) trait Integer:
     FromStr<Err = core::num::ParseIntError> + TryFrom<i8> + Sized
@@ -90,19 +90,8 @@ pub(crate) fn int<T: Integer, S: AsRef<str> + Into<String>>(s: S) -> Result<T, P
     })
 }
 
-/// Strips the hex prefix off `s` if one is present.
-pub(crate) fn strip_hex_prefix(s: &str) -> &str {
-    if let Some(stripped) = s.strip_prefix("0x") {
-        stripped
-    } else if let Some(stripped) = s.strip_prefix("0X") {
-        stripped
-    } else {
-        s
-    }
-}
-
 pub(crate) fn hex_u32<S: AsRef<str> + Into<String>>(s: S) -> Result<u32, ParseIntError> {
-    u32::from_str_radix(strip_hex_prefix(s.as_ref()), 16).map_err(|error| ParseIntError {
+    u32::from_str_radix(s.as_ref(), 16).map_err(|error| ParseIntError {
         input: s.into(),
         bits: u8::try_from(core::mem::size_of::<u32>() * 8).expect("max is 32 bits for u32"),
         is_signed: u32::try_from(-1i8).is_ok(),
diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs
index 3053d077..c8c1eea6 100644
--- a/bitcoin/src/pow.rs
+++ b/bitcoin/src/pow.rs
@@ -17,8 +17,9 @@
 use crate::consensus::encode::{self, Decodable, Encodable};
 #[cfg(doc)]
 use crate::consensus::Params;
-use crate::error::{PrefixedHexError, UnprefixedHexError, ContainsPrefixError, MissingPrefixError};
-use crate::{parse, Network};
+use crate::string::FromHexStr;
+use crate::prelude::*;
+use crate::Network;
 
 /// Implement traits and methods shared by `Target` and `Work`.
 macro_rules! do_impl {
@@ -271,29 +272,6 @@ pub fn max_difficulty_transition_threshold(&self) -> Self { Self(self.0 << 2) }
 pub struct CompactTarget(u32);
 
 impl CompactTarget {
-    /// Creates a `CompactTarget` from an prefixed hex string.
-    pub fn from_hex(s: &str) -> Result<Self, PrefixedHexError> {
-        let stripped = if let Some(stripped) = s.strip_prefix("0x") {
-            stripped
-        } else if let Some(stripped) = s.strip_prefix("0X") {
-            stripped
-        } else {
-            return Err(MissingPrefixError::new(s).into());
-        };
-
-        let target = parse::hex_u32(stripped)?;
-        Ok(Self::from_consensus(target))
-    }
-
-    /// Creates a `CompactTarget` from an unprefixed hex string.
-    pub fn from_unprefixed_hex(s: &str) -> Result<Self, UnprefixedHexError> {
-        if s.starts_with("0x") || s.starts_with("0X") {
-            return Err(ContainsPrefixError::new(s).into());
-        }
-        let lock_time = parse::hex_u32(s)?;
-        Ok(Self::from_consensus(lock_time))
-    }
-
     /// Creates a [`CompactTarget`] from a consensus encoded `u32`.
     pub fn from_consensus(bits: u32) -> Self { Self(bits) }
 
@@ -305,6 +283,15 @@ impl From<CompactTarget> for Target {
     fn from(c: CompactTarget) -> Self { Target::from_compact(c) }
 }
 
+impl FromHexStr for CompactTarget {
+    type Error = crate::parse::ParseIntError;
+
+    fn from_hex_str_no_prefix<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error> {
+        let compact_target = crate::parse::hex_u32(s)?;
+        Ok(Self::from_consensus(compact_target))
+    }
+}
+
 impl Encodable for CompactTarget {
     #[inline]
     fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
@@ -736,22 +723,6 @@ impl<T: Into<u128>> From<T> for U256 {
     fn from(x: T) -> Self { U256(0, x.into()) }
 }
 
-/// Error from `TryFrom<signed type>` implementations, occurs when input is negative.
-#[derive(Debug, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub struct TryFromError(i128);
-
-impl fmt::Display for TryFromError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "attempt to create unsigned integer type from negative number: {}", self.0)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for TryFromError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
-}
-
 impl Add for U256 {
     type Output = Self;
     fn add(self, rhs: Self) -> Self {
@@ -1537,33 +1508,23 @@ fn u256_is_max_correct_positive() {
     }
 
     #[test]
-    fn compact_target_from_hex_lower() {
-        let target = CompactTarget::from_hex("0x010034ab").unwrap();
-        assert_eq!(target, CompactTarget(0x010034ab));
-    }
-
-    #[test]
-    fn compact_target_from_hex_upper() {
-        let target = CompactTarget::from_hex("0X010034AB").unwrap();
-        assert_eq!(target, CompactTarget(0x010034ab));
-    }
-
-    #[test]
-    fn compact_target_from_unprefixed_hex_lower() {
-        let target = CompactTarget::from_unprefixed_hex("010034ab").unwrap();
-        assert_eq!(target, CompactTarget(0x010034ab));
+    fn compact_target_from_hex_str_happy_path() {
+        let actual = CompactTarget::from_hex_str("0x01003456").unwrap();
+        let expected = CompactTarget(0x01003456);
+        assert_eq!(actual, expected);
     }
 
     #[test]
-    fn compact_target_from_unprefixed_hex_upper() {
-        let target = CompactTarget::from_unprefixed_hex("010034AB").unwrap();
-        assert_eq!(target, CompactTarget(0x010034ab));
+    fn compact_target_from_hex_str_no_prefix_happy_path() {
+        let actual = CompactTarget::from_hex_str_no_prefix("01003456").unwrap();
+        let expected = CompactTarget(0x01003456);
+        assert_eq!(actual, expected);
     }
 
     #[test]
     fn compact_target_from_hex_invalid_hex_should_err() {
         let hex = "0xzbf9";
-        let result = CompactTarget::from_hex(hex);
+        let result = CompactTarget::from_hex_str(hex);
         assert!(result.is_err());
     }
 
diff --git a/bitcoin/src/psbt/error.rs b/bitcoin/src/psbt/error.rs
index d0e3cdea..64f0faa5 100644
--- a/bitcoin/src/psbt/error.rs
+++ b/bitcoin/src/psbt/error.rs
@@ -103,8 +103,6 @@ pub enum Error {
     Io(io::Error),
 }
 
-internals::impl_from_infallible!(Error);
-
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use Error::*;
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index 4eac052d..29a99f2c 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -675,8 +675,6 @@ pub enum GetKeyError {
     NotSupported,
 }
 
-internals::impl_from_infallible!(GetKeyError);
-
 impl fmt::Display for GetKeyError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use GetKeyError::*;
@@ -786,8 +784,6 @@ pub enum SignError {
     Unsupported,
 }
 
-internals::impl_from_infallible!(SignError);
-
 impl fmt::Display for SignError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use SignError::*;
@@ -869,8 +865,6 @@ pub enum ExtractTxError {
     },
 }
 
-internals::impl_from_infallible!(ExtractTxError);
-
 impl fmt::Display for ExtractTxError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use ExtractTxError::*;
@@ -921,8 +915,6 @@ pub enum IndexOutOfBoundsError {
     },
 }
 
-internals::impl_from_infallible!(IndexOutOfBoundsError);
-
 impl fmt::Display for IndexOutOfBoundsError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use IndexOutOfBoundsError::*;
@@ -974,8 +966,6 @@ pub enum PsbtParseError {
         Base64Encoding(::base64::DecodeError),
     }
 
-    internals::impl_from_infallible!(PsbtParseError);
-
     impl Display for PsbtParseError {
         fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
             use self::PsbtParseError::*;
@@ -1035,7 +1025,7 @@ mod tests {
 
     #[track_caller]
     pub fn hex_psbt(s: &str) -> Result<Psbt, crate::psbt::error::Error> {
-        let r = Vec::from_hex(s);
+        let r: Result<Vec<u8>, hex::HexToBytesError> = Vec::from_hex(s);
         match r {
             Err(_e) => panic!("unable to parse hex string {}", s),
             Ok(v) => Psbt::deserialize(&v),
diff --git a/bitcoin/src/sign_message.rs b/bitcoin/src/sign_message.rs
index b8232dab..f907a7a7 100644
--- a/bitcoin/src/sign_message.rs
+++ b/bitcoin/src/sign_message.rs
@@ -43,8 +43,6 @@ pub enum MessageSignatureError {
         UnsupportedAddressType(AddressType),
     }
 
-    internals::impl_from_infallible!(MessageSignatureError);
-
     impl fmt::Display for MessageSignatureError {
         fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
             use MessageSignatureError::*;
diff --git a/bitcoin/src/string.rs b/bitcoin/src/string.rs
new file mode 100644
index 00000000..c8bad0ba
--- /dev/null
+++ b/bitcoin/src/string.rs
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: CC0-1.0
+
+//! Bitcoin string parsing utilities.
+//!
+//! This module provides utility types and traits
+//! to support handling and parsing strings within `rust-bitcoin`.
+
+use core::fmt;
+
+use internals::write_err;
+
+use crate::prelude::*;
+
+/// Trait that allows types to be initialized from hex strings
+pub trait FromHexStr: Sized {
+    /// An error occurred while parsing the hex string.
+    type Error;
+
+    /// Parses provided string as hex requiring 0x prefix.
+    ///
+    /// This is intended for user-supplied inputs or already-existing protocols in which 0x prefix is used.
+    fn from_hex_str<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, FromHexError<Self::Error>> {
+        if !s.as_ref().starts_with("0x") {
+            Err(FromHexError::MissingPrefix(s.into()))
+        } else {
+            Ok(Self::from_hex_str_no_prefix(s.as_ref().trim_start_matches("0x"))?)
+        }
+    }
+
+    /// Parses provided string as hex without requiring 0x prefix.
+    ///
+    /// This is **not** recommended for user-supplied inputs because of possible confusion with decimals.
+    /// It should be only used for existing protocols which always encode values as hex without 0x prefix.
+    fn from_hex_str_no_prefix<S: AsRef<str> + Into<String>>(s: S) -> Result<Self, Self::Error>;
+}
+
+/// Hex parsing error
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum FromHexError<E> {
+    /// The input was not a valid hex string, contains the error that occurred while parsing.
+    ParseHex(E),
+    /// The input is missing `0x` prefix, contains the invalid input.
+    MissingPrefix(String),
+}
+
+impl<E> From<E> for FromHexError<E> {
+    fn from(e: E) -> Self { FromHexError::ParseHex(e) }
+}
+
+impl<E: fmt::Display> fmt::Display for FromHexError<E> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use FromHexError::*;
+
+        match *self {
+            ParseHex(ref e) => write_err!(f, "failed to parse hex string"; e),
+            MissingPrefix(ref value) =>
+                write_err!(f, "the input value `{}` is missing the `0x` prefix", value; self),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl<E> std::error::Error for FromHexError<E>
+where
+    E: std::error::Error + 'static,
+{
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        use FromHexError::*;
+
+        match *self {
+            ParseHex(ref e) => Some(e),
+            MissingPrefix(_) => None,
+        }
+    }
+}
diff --git a/bitcoin/src/taproot/merkle_branch.rs b/bitcoin/src/taproot/merkle_branch.rs
index 0698be26..887cbacf 100644
--- a/bitcoin/src/taproot/merkle_branch.rs
+++ b/bitcoin/src/taproot/merkle_branch.rs
@@ -43,7 +43,7 @@ pub fn is_empty(&self) -> bool { self.0.is_empty() }
     ///
     /// # Errors
     ///
-    /// The function returns an error if the number of bytes is not an integer multiple of 32 or
+    /// The function returns an error if the the number of bytes is not an integer multiple of 32 or
     /// if the number of hashes exceeds 128.
     pub fn decode(sl: &[u8]) -> Result<Self, TaprootError> {
         if sl.len() % TAPROOT_CONTROL_NODE_SIZE != 0 {
diff --git a/bitcoin/src/taproot/mod.rs b/bitcoin/src/taproot/mod.rs
index a84d1620..9c87218f 100644
--- a/bitcoin/src/taproot/mod.rs
+++ b/bitcoin/src/taproot/mod.rs
@@ -587,8 +587,6 @@ pub enum IncompleteBuilderError {
     HiddenParts(TaprootBuilder),
 }
 
-internals::impl_from_infallible!(IncompleteBuilderError);
-
 impl IncompleteBuilderError {
     /// Converts error into the original incomplete [`TaprootBuilder`] instance.
     pub fn into_builder(self) -> TaprootBuilder {
@@ -633,8 +631,6 @@ pub enum HiddenNodesError {
     HiddenParts(NodeInfo),
 }
 
-internals::impl_from_infallible!(HiddenNodesError);
-
 impl HiddenNodesError {
     /// Converts error into the original incomplete [`NodeInfo`] instance.
     pub fn into_node_info(self) -> NodeInfo {
@@ -1328,8 +1324,6 @@ pub enum TaprootBuilderError {
     EmptyTree,
 }
 
-internals::impl_from_infallible!(TaprootBuilderError);
-
 impl fmt::Display for TaprootBuilderError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use TaprootBuilderError::*;
@@ -1390,8 +1384,6 @@ pub enum TaprootError {
     EmptyTree,
 }
 
-internals::impl_from_infallible!(TaprootError);
-
 impl fmt::Display for TaprootError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         use TaprootError::*;
diff --git a/bitcoin/src/taproot/serialized_signature.rs b/bitcoin/src/taproot/serialized_signature.rs
index 4def2e57..337ced5f 100644
--- a/bitcoin/src/taproot/serialized_signature.rs
+++ b/bitcoin/src/taproot/serialized_signature.rs
@@ -28,7 +28,10 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(self, f
 
 impl fmt::Display for SerializedSignature {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        hex::fmt_hex_exact!(f, MAX_LEN, self, hex::Case::Lower)
+        let mut buf = [0u8; MAX_LEN * 2];
+        let mut encoder = hex::buf_encoder::BufEncoder::new(&mut buf);
+        encoder.put_bytes(self, hex::Case::Lower);
+        f.pad_integral(true, "0x", encoder.as_str())
     }
 }
 
diff --git a/contrib/run_task.sh b/contrib/run_task.sh
index f7aa21ac..105dd0cc 100755
--- a/contrib/run_task.sh
+++ b/contrib/run_task.sh
@@ -148,10 +148,8 @@ loop_features() {
 
 # Lint the workspace then the individual crate examples.
 do_lint() {
-    need_nightly
-
     # Use the current (recent/minimal) lock file.
-    local cargo="cargo --locked"
+    local cargo="cargo +nightly --locked"
 
     $cargo clippy --workspace -- -D warnings
 
@@ -183,8 +181,7 @@ do_dup_deps() {
 # Build the docs with a nightly toolchain, in unison with the function
 # below this checks that we feature guarded docs imports correctly.
 build_docs_with_nightly_toolchain() {
-    need_nightly
-    local cargo="cargo --locked"
+    local cargo="cargo +nightly --locked"
 
     RUSTDOCFLAGS="--cfg docsrs -D warnings -D rustdoc::broken-intra-doc-links" $cargo doc --all-features
 }
@@ -245,13 +242,6 @@ need_cmd() {
     fi
 }
 
-need_nightly() {
-    cargo_ver=$(cargo --version)
-    if echo "$cargo_ver" | grep -q -v nightly; then
-        err "Need a nightly compiler; have $(cargo --version)"
-    fi
-}
-
 err() {
     echo "$1" >&2
     exit 1
diff --git a/hashes/Cargo.toml b/hashes/Cargo.toml
index 14368252..70a6758e 100644
--- a/hashes/Cargo.toml
+++ b/hashes/Cargo.toml
@@ -27,7 +27,7 @@ all-features = true
 rustdoc-args = ["--cfg", "docsrs"]
 
 [dependencies]
-hex = { package = "hex-conservative", version = "0.2.0", default-features = false }
+hex = { package = "hex-conservative", version = "0.1.1", default-features = false }
 
 bitcoin-io = { version = "0.1.1", default-features = false, optional = true }
 schemars = { version = "0.8.3", default-features = false, optional = true }
diff --git a/hashes/src/internal_macros.rs b/hashes/src/internal_macros.rs
index 9b3f9dde..9352cde5 100644
--- a/hashes/src/internal_macros.rs
+++ b/hashes/src/internal_macros.rs
@@ -71,6 +71,22 @@ fn fmt(&self, f: &mut $crate::_export::_core::fmt::Formatter) -> $crate::_export
 macro_rules! hash_trait_impls {
     ($bits:expr, $reverse:expr $(, $gen:ident: $gent:ident)*) => {
         impl<$($gen: $gent),*> Hash<$($gen),*> {
+            /// Displays hex forwards, regardless of how this type would display it naturally.
+            ///
+            /// This is mainly intended as an internal method and you shouldn't need it unless
+            /// you're doing something special.
+            pub fn forward_hex(&self) -> impl '_ + core::fmt::LowerHex + core::fmt::UpperHex {
+                $crate::hex::DisplayHex::as_hex(&self.0)
+            }
+
+            /// Displays hex backwards, regardless of how this type would display it naturally.
+            ///
+            /// This is mainly intended as an internal method and you shouldn't need it unless
+            /// you're doing something special.
+            pub fn backward_hex(&self) -> impl '_ + core::fmt::LowerHex + core::fmt::UpperHex {
+                $crate::hex::display::DisplayArray::<_, [u8; $bits / 8 * 2]>::new(self.0.iter().rev())
+            }
+
             /// Zero cost conversion between a fixed length byte array shared reference and
             /// a shared reference to this Hash type.
             pub fn from_bytes_ref(bytes: &[u8; $bits / 8]) -> &Self {
@@ -89,13 +105,15 @@ impl<$($gen: $gent),*> Hash<$($gen),*> {
         impl<$($gen: $gent),*> str::FromStr for Hash<$($gen),*> {
             type Err = $crate::hex::HexToArrayError;
             fn from_str(s: &str) -> $crate::_export::_core::result::Result<Self, Self::Err> {
-                use $crate::{Hash, hex::{FromHex}};
+                use $crate::hex::{FromHex, HexToBytesIter};
+                use $crate::Hash;
 
-                let mut bytes = <[u8; $bits / 8]>::from_hex(s)?;
-                if $reverse {
-                    bytes.reverse();
-                }
-                Ok(Self::from_byte_array(bytes))
+                let inner: [u8; $bits / 8] = if $reverse {
+                    FromHex::from_byte_iter(HexToBytesIter::new(s)?.rev())?
+                } else {
+                    FromHex::from_byte_iter(HexToBytesIter::new(s)?)?
+                };
+                Ok(Self::from_byte_array(inner))
             }
         }
 
diff --git a/hashes/src/lib.rs b/hashes/src/lib.rs
index d612493f..e1381919 100644
--- a/hashes/src/lib.rs
+++ b/hashes/src/lib.rs
@@ -234,12 +234,19 @@ fn hash_byte_chunks<B, I>(byte_slices: I) -> Self
 
 /// Attempted to create a hash from an invalid length slice.
 #[derive(Debug, Clone, PartialEq, Eq)]
-#[non_exhaustive]
 pub struct FromSliceError {
     expected: usize,
     got: usize,
 }
 
+impl FromSliceError {
+    /// Returns the expected slice length.
+    pub fn expected_length(&self) -> usize { self.expected }
+
+    /// Returns the invalid slice length.
+    pub fn invalid_length(&self) -> usize { self.got }
+}
+
 impl fmt::Display for FromSliceError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "invalid slice length {} (expected {})", self.got, self.expected)
@@ -247,9 +254,7 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 }
 
 #[cfg(feature = "std")]
-impl std::error::Error for FromSliceError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
-}
+impl std::error::Error for FromSliceError {}
 
 #[cfg(test)]
 mod tests {
@@ -273,12 +278,4 @@ fn convert_newtypes() {
         let h2: TestNewtype = h.to_string().parse().unwrap();
         assert_eq!(h2.to_raw_hash(), h);
     }
-
-    #[test]
-    fn newtype_fmt_roundtrip() {
-        let orig = TestNewtype::hash(&[]);
-        let hex = format!("{}", orig);
-        let rinsed = hex.parse::<TestNewtype>().expect("failed to parse hex");
-        assert_eq!(rinsed, orig)
-    }
 }
diff --git a/hashes/src/sha256.rs b/hashes/src/sha256.rs
index 63e214a9..77eca04b 100644
--- a/hashes/src/sha256.rs
+++ b/hashes/src/sha256.rs
@@ -176,13 +176,15 @@ pub const fn hash_tag(tag: &[u8]) -> Self {
 }
 
 impl hex::FromHex for Midstate {
-    type Error = hex::HexToArrayError;
-
-    fn from_hex(s: &str) -> Result<Self, Self::Error> {
+    type Err = hex::HexToArrayError;
+    fn from_byte_iter<I>(iter: I) -> Result<Self, Self::Err>
+    where
+        I: Iterator<Item = Result<u8, hex::HexToBytesError>>
+            + ExactSizeIterator
+            + DoubleEndedIterator,
+    {
         // DISPLAY_BACKWARD is true
-        let mut bytes = <[u8; 32]>::from_hex(s)?;
-        bytes.reverse();
-        Ok(Midstate(bytes))
+        Ok(Midstate::from_byte_array(hex::FromHex::from_byte_iter(iter.rev())?))
     }
 }
 
@@ -815,8 +817,7 @@ fn software_process_block(&mut self) {
 
 #[cfg(test)]
 mod tests {
-    use crate::{sha256, Hash as _, HashEngine};
-    use super::*;
+    use crate::{sha256, Hash, HashEngine};
 
     #[test]
     #[cfg(feature = "alloc")]
@@ -881,14 +882,6 @@ struct Test {
         }
     }
 
-    #[test]
-    fn fmt_roundtrips() {
-        let hash = sha256::Hash::hash(b"some arbitrary bytes");
-        let hex = format!("{}", hash);
-        let rinsed = hex.parse::<sha256::Hash>().expect("failed to parse hex");
-        assert_eq!(rinsed, hash)
-    }
-
     #[test]
     #[rustfmt::skip]
     fn midstate() {
@@ -971,14 +964,14 @@ fn engine_with_state() {
 
     #[test]
     fn const_hash() {
-        assert_eq!(Hash::hash(&[]), Hash::const_hash(&[]));
+        assert_eq!(super::Hash::hash(&[]), super::Hash::const_hash(&[]));
 
         let mut bytes = Vec::new();
         for i in 0..256 {
             bytes.push(i as u8);
             assert_eq!(
-                Hash::hash(&bytes),
-                Hash::const_hash(&bytes),
+                super::Hash::hash(&bytes),
+                super::Hash::const_hash(&bytes),
                 "hashes don't match for length {}",
                 i + 1
             );
@@ -987,6 +980,8 @@ fn const_hash() {
 
     #[test]
     fn const_midstate() {
+        use super::Midstate;
+
         assert_eq!(
             Midstate::hash_tag(b"TapLeaf"),
             Midstate([
@@ -996,14 +991,6 @@ fn const_midstate() {
         )
     }
 
-    #[test]
-    fn midstate_fmt_roundtrip() {
-        let midstate = Midstate::hash_tag(b"ArbitraryTag");
-        let hex = format!("{}", midstate);
-        let rinsed = hex.parse::<Midstate>().expect("failed to parse hex");
-        assert_eq!(rinsed, midstate)
-    }
-
     #[cfg(feature = "serde")]
     #[test]
     fn sha256_serde() {
diff --git a/hashes/src/sha256d.rs b/hashes/src/sha256d.rs
index fb3b0c5e..4fb813bd 100644
--- a/hashes/src/sha256d.rs
+++ b/hashes/src/sha256d.rs
@@ -30,12 +30,10 @@ fn from_engine(e: sha256::HashEngine) -> Hash {
 
 #[cfg(test)]
 mod tests {
-    use crate::{sha256d, Hash as _};
-
     #[test]
     #[cfg(feature = "alloc")]
     fn test() {
-        use crate::{sha256, HashEngine};
+        use crate::{sha256, sha256d, Hash, HashEngine};
 
         #[derive(Clone)]
         struct Test {
@@ -83,19 +81,13 @@ struct Test {
         }
     }
 
-    #[test]
-    fn fmt_roundtrips() {
-        let hash = sha256d::Hash::hash(b"some arbitrary bytes");
-        let hex = format!("{}", hash);
-        let rinsed = hex.parse::<sha256d::Hash>().expect("failed to parse hex");
-        assert_eq!(rinsed, hash)
-    }
-
     #[cfg(feature = "serde")]
     #[test]
     fn sha256_serde() {
         use serde_test::{assert_tokens, Configure, Token};
 
+        use crate::{sha256d, Hash};
+
         #[rustfmt::skip]
         static HASH_BYTES: [u8; 32] = [
             0xef, 0x53, 0x7f, 0x25, 0xc8, 0x95, 0xbf, 0xa7,
diff --git a/hashes/src/util.rs b/hashes/src/util.rs
index c84b928a..92fd4855 100644
--- a/hashes/src/util.rs
+++ b/hashes/src/util.rs
@@ -3,17 +3,17 @@
 #[macro_export]
 /// Adds hexadecimal formatting implementation of a trait `$imp` to a given type `$ty`.
 macro_rules! hex_fmt_impl(
-    ($reverse:expr, $len:expr, $ty:ident) => (
-        $crate::hex_fmt_impl!($reverse, $len, $ty, );
+    ($reverse:expr, $ty:ident) => (
+        $crate::hex_fmt_impl!($reverse, $ty, );
     );
-    ($reverse:expr, $len:expr, $ty:ident, $($gen:ident: $gent:ident),*) => (
+    ($reverse:expr, $ty:ident, $($gen:ident: $gent:ident),*) => (
         impl<$($gen: $gent),*> $crate::_export::_core::fmt::LowerHex for $ty<$($gen),*> {
             #[inline]
             fn fmt(&self, f: &mut $crate::_export::_core::fmt::Formatter) -> $crate::_export::_core::fmt::Result {
                 if $reverse {
-                    $crate::hex::fmt_hex_exact!(f, $len, <Self as $crate::Hash>::as_byte_array(&self).iter().rev(), $crate::hex::Case::Lower)
+                    $crate::_export::_core::fmt::LowerHex::fmt(&self.0.backward_hex(), f)
                 } else {
-                    $crate::hex::fmt_hex_exact!(f, $len, <Self as $crate::Hash>::as_byte_array(&self), $crate::hex::Case::Lower)
+                    $crate::_export::_core::fmt::LowerHex::fmt(&self.0.forward_hex(), f)
                 }
             }
         }
@@ -22,9 +22,9 @@ impl<$($gen: $gent),*> $crate::_export::_core::fmt::UpperHex for $ty<$($gen),*>
             #[inline]
             fn fmt(&self, f: &mut $crate::_export::_core::fmt::Formatter) -> $crate::_export::_core::fmt::Result {
                 if $reverse {
-                    $crate::hex::fmt_hex_exact!(f, $len, <Self as $crate::Hash>::as_byte_array(&self).iter().rev(), $crate::hex::Case::Upper)
+                    $crate::_export::_core::fmt::UpperHex::fmt(&self.0.backward_hex(), f)
                 } else {
-                    $crate::hex::fmt_hex_exact!(f, $len, <Self as $crate::Hash>::as_byte_array(&self), $crate::hex::Case::Upper)
+                    $crate::_export::_core::fmt::UpperHex::fmt(&self.0.forward_hex(), f)
                 }
             }
         }
@@ -188,7 +188,7 @@ macro_rules! hash_newtype {
             $({ $($type_attrs)* })*
         }
 
-        $crate::hex_fmt_impl!(<$newtype as $crate::Hash>::DISPLAY_BACKWARD, <$newtype as $crate::Hash>::LEN, $newtype);
+        $crate::hex_fmt_impl!(<$newtype as $crate::Hash>::DISPLAY_BACKWARD, $newtype);
         $crate::serde_impl!($newtype, <$newtype as $crate::Hash>::LEN);
         $crate::borrow_slice_impl!($newtype);
 
@@ -270,13 +270,15 @@ fn all_zeros() -> Self {
         impl $crate::_export::_core::str::FromStr for $newtype {
             type Err = $crate::hex::HexToArrayError;
             fn from_str(s: &str) -> $crate::_export::_core::result::Result<$newtype, Self::Err> {
-                use $crate::{Hash, hex::FromHex};
+                use $crate::hex::{FromHex, HexToBytesIter};
+                use $crate::Hash;
 
-                let mut bytes = <[u8; <Self as $crate::Hash>::LEN]>::from_hex(s)?;
-                if <Self as $crate::Hash>::DISPLAY_BACKWARD {
-                    bytes.reverse();
+                let inner: <$hash as Hash>::Bytes = if <Self as $crate::Hash>::DISPLAY_BACKWARD {
+                    FromHex::from_byte_iter(HexToBytesIter::new(s)?.rev())?
+                } else {
+                    FromHex::from_byte_iter(HexToBytesIter::new(s)?)?
                 };
-                Ok($newtype(<$hash>::from_byte_array(bytes)))
+                Ok($newtype(<$hash>::from_byte_array(inner)))
             }
         }
 
diff --git a/internals/src/error/parse_error.rs b/internals/src/error/parse_error.rs
index 7f8903dd..d8a99d21 100644
--- a/internals/src/error/parse_error.rs
+++ b/internals/src/error/parse_error.rs
@@ -6,7 +6,7 @@
 /// [`InputString`](super::InputString), the type of `source` is specified as the second argument
 /// to the macro.
 ///
-/// The resulting type is public, conditionally implements `std::error::Error` and has a private
+/// The resulting type is public, conditionally implements [`std::error::Error`] and has a private
 /// `new()` method for convenience.
 ///
 /// ## Parameters
diff --git a/internals/src/macros.rs b/internals/src/macros.rs
index 8e69335b..91e60a1c 100644
--- a/internals/src/macros.rs
+++ b/internals/src/macros.rs
@@ -113,59 +113,3 @@ macro_rules! const_assert {
         const _: [(); 0 - !$x as usize] = [];
     }};
 }
-
-/// Derives `From<core::convert::Infallible>` for the given type.
-///
-/// Supports types with arbitrary combinations of lifetimes and type parameters.
-///
-/// Note: Paths are not supported (for ex. impl_from_infallible!(Hello<D: std::fmt::Display>).
-///
-/// ## Examples
-///
-/// ```rust
-/// # use core::fmt::{Display, Debug};
-/// use bitcoin_internals::impl_from_infallible;
-///
-/// enum AlphaEnum { Item }
-/// impl_from_infallible!(AlphaEnum);
-///
-/// enum BetaEnum<'b> { Item(&'b usize) }
-/// impl_from_infallible!(BetaEnum<'b>);
-///
-/// enum GammaEnum<T> { Item(T) };
-/// impl_from_infallible!(GammaEnum<T>);
-///
-/// enum DeltaEnum<'b, 'a: 'static + 'b, T: 'a, D: Debug + Display + 'a> {
-///     Item((&'b usize, &'a usize, T, D))
-/// }
-/// impl_from_infallible!(DeltaEnum<'b, 'a: 'static + 'b, T: 'a, D: Debug + Display + 'a>);
-///
-/// struct AlphaStruct;
-/// impl_from_infallible!(AlphaStruct);
-///
-/// struct BetaStruct<'b>(&'b usize);
-/// impl_from_infallible!(BetaStruct<'b>);
-///
-/// struct GammaStruct<T>(T);
-/// impl_from_infallible!(GammaStruct<T>);
-///
-/// struct DeltaStruct<'b, 'a: 'static + 'b, T: 'a, D: Debug + Display + 'a> {
-///     hello: &'a T,
-///     what: &'b D,
-/// }
-/// impl_from_infallible!(DeltaStruct<'b, 'a: 'static + 'b, T: 'a, D: Debug + Display + 'a>);
-/// ```
-///
-/// See <https://stackoverflow.com/a/61189128> for more information about this macro.
-#[macro_export]
-macro_rules! impl_from_infallible {
-    ( $name:ident $(< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)? ) => {
-        impl $(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
-            From<core::convert::Infallible>
-        for $name
-            $(< $( $lt ),+ >)?
-        {
-            fn from(never: core::convert::Infallible) -> Self { match never {} }
-        }
-    }
-}
diff --git a/io/src/error.rs b/io/src/error.rs
index 5e5e12d1..0218de5a 100644
--- a/io/src/error.rs
+++ b/io/src/error.rs
@@ -116,10 +116,6 @@ pub enum ErrorKind {
             ),*
         }
 
-        impl From<core::convert::Infallible> for ErrorKind {
-            fn from(never: core::convert::Infallible) -> Self { match never {} }
-        }
-
         impl ErrorKind {
             fn description(&self) -> &'static str {
                 match self {
diff --git a/units/src/amount.rs b/units/src/amount.rs
index 268ee6ca..3e83f4c6 100644
--- a/units/src/amount.rs
+++ b/units/src/amount.rs
@@ -156,13 +156,8 @@ pub enum ParseError {
 
     /// Invalid denomination.
     Denomination(ParseDenominationError),
-
-    /// The denomination was not identified.
-    MissingDenomination(MissingDenominationError),
 }
 
-internals::impl_from_infallible!(ParseError);
-
 impl From<ParseAmountError> for ParseError {
     fn from(e: ParseAmountError) -> Self { Self::Amount(e) }
 }
@@ -175,30 +170,11 @@ impl From<OutOfRangeError> for ParseError {
     fn from(e: OutOfRangeError) -> Self { Self::Amount(e.into()) }
 }
 
-impl From<TooPreciseError> for ParseError {
-    fn from(e: TooPreciseError) -> Self { Self::Amount(e.into()) }
-}
-
-impl From<MissingDigitsError> for ParseError {
-    fn from(e: MissingDigitsError) -> Self { Self::Amount(e.into()) }
-}
-
-impl From<InputTooLargeError> for ParseError {
-    fn from(e: InputTooLargeError) -> Self { Self::Amount(e.into()) }
-}
-
-impl From<InvalidCharacterError> for ParseError {
-    fn from(e: InvalidCharacterError) -> Self { Self::Amount(e.into()) }
-}
-
 impl fmt::Display for ParseError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
             ParseError::Amount(error) => write_err!(f, "invalid amount"; error),
             ParseError::Denomination(error) => write_err!(f, "invalid denomination"; error),
-            // We consider this to not be a source because it currently doesn't contain useful
-            // information
-            ParseError::MissingDenomination(_) => f.write_str("the input doesn't contain a denomination"),
         }
     }
 }
@@ -209,9 +185,6 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         match self {
             ParseError::Amount(error) => Some(error),
             ParseError::Denomination(error) => Some(error),
-            // We consider this to not be a source because it currently doesn't contain useful
-            // information
-            ParseError::MissingDenomination(_) => None,
         }
     }
 }
@@ -223,52 +196,25 @@ pub enum ParseAmountError {
     /// The amount is too big or too small.
     OutOfRange(OutOfRangeError),
     /// Amount has higher precision than supported by the type.
-    TooPrecise(TooPreciseError),
-    /// A digit was expected but not found.
-    MissingDigits(MissingDigitsError),
+    TooPrecise,
+    /// Invalid number format.
+    InvalidFormat,
     /// Input string was too large.
-    InputTooLarge(InputTooLargeError),
+    InputTooLarge,
     /// Invalid character in input.
-    InvalidCharacter(InvalidCharacterError),
+    InvalidCharacter(char),
 }
 
-impl From<TooPreciseError> for ParseAmountError {
-    fn from(value: TooPreciseError) -> Self {
-        Self::TooPrecise(value)
-    }
-}
-
-impl From<MissingDigitsError> for ParseAmountError {
-    fn from(value: MissingDigitsError) -> Self {
-        Self::MissingDigits(value)
-    }
-}
-
-impl From<InputTooLargeError> for ParseAmountError {
-    fn from(value: InputTooLargeError) -> Self {
-        Self::InputTooLarge(value)
-    }
-}
-
-
-impl From<InvalidCharacterError> for ParseAmountError {
-    fn from(value: InvalidCharacterError) -> Self {
-        Self::InvalidCharacter(value)
-    }
-}
-
-internals::impl_from_infallible!(ParseAmountError);
-
 impl fmt::Display for ParseAmountError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use ParseAmountError::*;
 
         match *self {
-            OutOfRange(ref error) => write_err!(f, "amount out of range"; error),
-            TooPrecise(ref error) => write_err!(f, "amount has a too high precision"; error),
-            MissingDigits(ref error) => write_err!(f, "the input has too few digits"; error),
-            InputTooLarge(ref error) => write_err!(f, "the input is too large"; error),
-            InvalidCharacter(ref error) => write_err!(f, "invalid character in the input"; error),
+            OutOfRange(error) => write_err!(f, "amount out of range"; error),
+            TooPrecise => f.write_str("amount has a too high precision"),
+            InvalidFormat => f.write_str("invalid number format"),
+            InputTooLarge => f.write_str("input string was too large"),
+            InvalidCharacter(c) => write!(f, "invalid character in input: {}", c),
         }
     }
 }
@@ -279,11 +225,9 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         use ParseAmountError::*;
 
         match *self {
-            TooPrecise(ref error) => Some(error),
-            InputTooLarge(ref error) => Some(error),
+            TooPrecise | InvalidFormat | InputTooLarge
+            | InvalidCharacter(_) => None,
             OutOfRange(ref error) => Some(error),
-            MissingDigits(ref error) => Some(error),
-            InvalidCharacter(ref error) => Some(error),
         }
     }
 }
@@ -359,87 +303,6 @@ fn from(value: OutOfRangeError) -> Self {
     }
 }
 
-/// Error returned when the input string has higher precision than satoshis.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct TooPreciseError {
-    position: usize,
-}
-
-impl fmt::Display for TooPreciseError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self.position {
-            0 => f.write_str("the amount is less than 1 satoshi but it's not zero"),
-            pos => write!(f, "the digits starting from position {} represent a sub-satoshi amount", pos),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for TooPreciseError {}
-
-/// Error returned when the input string is too large.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct InputTooLargeError {
-    len: usize,
-}
-
-impl fmt::Display for InputTooLargeError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self.len - INPUT_STRING_LEN_LIMIT {
-            1 => write!(f, "the input is one character longer than the maximum allowed length ({})", INPUT_STRING_LEN_LIMIT),
-            n => write!(f, "the input is {} characters longer than the maximum allowed length ({})", n, INPUT_STRING_LEN_LIMIT),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for InputTooLargeError {}
-
-/// Error returned when digits were expected in the input but there were none.
-///
-/// In particular, this is currently returned when the string is empty or only contains the minus sign.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct MissingDigitsError {
-    kind: MissingDigitsKind,
-}
-
-impl fmt::Display for MissingDigitsError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self.kind {
-            MissingDigitsKind::Empty => f.write_str("the input is empty"),
-            MissingDigitsKind::OnlyMinusSign => f.write_str("there are no digits following the minus (-) sign"),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for MissingDigitsError {}
-
-#[derive(Debug, Clone, Eq, PartialEq)]
-enum MissingDigitsKind {
-    Empty,
-    OnlyMinusSign,
-}
-
-/// Returned when the input contains an invalid character.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct InvalidCharacterError {
-    invalid_char: char,
-    position: usize,
-}
-
-impl fmt::Display for InvalidCharacterError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self.invalid_char {
-            '.' => f.write_str("there is more than one decimal separator (dot) in the input"),
-            '-' => f.write_str("there is more than one minus sign (-) in the input"),
-            c => write!(f, "the character '{}' at position {} is not a valid digit", c, self.position),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for InvalidCharacterError {}
 
 /// An error during amount parsing.
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -451,8 +314,6 @@ pub enum ParseDenominationError {
     PossiblyConfusing(PossiblyConfusingDenominationError),
 }
 
-internals::impl_from_infallible!(ParseDenominationError);
-
 impl fmt::Display for ParseDenominationError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use ParseDenominationError::*;
@@ -475,11 +336,6 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
     }
 }
 
-/// Error returned when the denomination is empty.
-#[derive(Debug, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub struct MissingDenominationError;
-
 /// Parsing error, unknown denomination.
 #[derive(Debug, Clone, PartialEq, Eq)]
 #[non_exhaustive]
@@ -512,27 +368,16 @@ impl std::error::Error for PossiblyConfusingDenominationError {
     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
 }
 
-/// Returns `Some(position)` if the precision is not supported.
-///
-/// The position indicates the first digit that is too precise.
-fn is_too_precise(s: &str, precision: usize) -> Option<usize> {
+fn is_too_precise(s: &str, precision: usize) -> bool {
     match s.find('.') {
-        Some(pos) if precision >= pos => { Some(0) },
-        Some(pos) => {
-            s[..pos].char_indices().rev().take(precision).find(|(_, d)| *d != '0').map(|(i, _)| i)
-                .or_else(|| {
-                    s[(pos + 1)..].char_indices().find(|(_, d)| *d != '0').map(|(i, _)| i + pos + 1)
-                })
-        },
-        None if precision >= s.len() => { Some(0) },
-        None => {
-            s.char_indices().rev().take(precision).find(|(_, d)| *d != '0').map(|(i, _)| i)
-        },
+        Some(pos) =>
+            s[(pos + 1)..].chars().any(|d| d != '0')
+                || precision >= pos
+                || s[..pos].chars().rev().take(precision).any(|d| d != '0'),
+        None => precision >= s.len() || s.chars().rev().take(precision).any(|d| d != '0'),
     }
 }
 
-const INPUT_STRING_LEN_LIMIT: usize = 50;
-
 /// Parse decimal string in the given denomination into a satoshi value and a
 /// bool indicator for a negative amount.
 fn parse_signed_to_satoshi(
@@ -540,16 +385,16 @@ fn parse_signed_to_satoshi(
     denom: Denomination,
 ) -> Result<(bool, u64), InnerParseError> {
     if s.is_empty() {
-        return Err(InnerParseError::MissingDigits(MissingDigitsError { kind: MissingDigitsKind::Empty }));
+        return Err(InnerParseError::InvalidFormat);
     }
-    if s.len() > INPUT_STRING_LEN_LIMIT {
-        return Err(InnerParseError::InputTooLarge(s.len()));
+    if s.len() > 50 {
+        return Err(InnerParseError::InputTooLarge);
     }
 
     let is_negative = s.starts_with('-');
     if is_negative {
         if s.len() == 1 {
-            return Err(InnerParseError::MissingDigits(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign }));
+            return Err(InnerParseError::InvalidFormat);
         }
         s = &s[1..];
     }
@@ -564,10 +409,10 @@ fn parse_signed_to_satoshi(
             // there are no decimals and the last digits are zeroes as
             // many as the difference in precision.
             let last_n = precision_diff.unsigned_abs().into();
-            if let Some(position) = is_too_precise(s, last_n) {
+            if is_too_precise(s, last_n) {
                 match s.parse::<i64>() {
                     Ok(0) => return Ok((is_negative, 0)),
-                    _ => return Err(InnerParseError::TooPrecise(TooPreciseError { position: position + is_negative as usize })),
+                    _ => return Err(InnerParseError::TooPrecise),
                 }
             }
             s = &s[0..s.find('.').unwrap_or(s.len()) - last_n];
@@ -579,7 +424,7 @@ fn parse_signed_to_satoshi(
 
     let mut decimals = None;
     let mut value: u64 = 0; // as satoshis
-    for (i, c) in s.char_indices() {
+    for c in s.chars() {
         match c {
             '0'..='9' => {
                 // Do `value = 10 * value + digit`, catching overflows.
@@ -594,16 +439,16 @@ fn parse_signed_to_satoshi(
                 decimals = match decimals {
                     None => None,
                     Some(d) if d < max_decimals => Some(d + 1),
-                    _ => return Err(InnerParseError::TooPrecise(TooPreciseError { position: i + is_negative as usize, })),
+                    _ => return Err(InnerParseError::TooPrecise),
                 };
             }
             '.' => match decimals {
                 None if max_decimals <= 0 => break,
                 None => decimals = Some(0),
                 // Double decimal dot.
-                _ => return Err(InnerParseError::InvalidCharacter(InvalidCharacterError { invalid_char: '.', position: i + is_negative as usize })),
+                _ => return Err(InnerParseError::InvalidFormat),
             },
-            c => return Err(InnerParseError::InvalidCharacter(InvalidCharacterError { invalid_char: c, position: i + is_negative as usize })),
+            c => return Err(InnerParseError::InvalidCharacter(c)),
         }
     }
 
@@ -621,22 +466,20 @@ fn parse_signed_to_satoshi(
 
 enum InnerParseError {
     Overflow { is_negative: bool },
-    TooPrecise(TooPreciseError),
-    MissingDigits(MissingDigitsError),
-    InputTooLarge(usize),
-    InvalidCharacter(InvalidCharacterError),
+    TooPrecise,
+    InvalidFormat,
+    InputTooLarge,
+    InvalidCharacter(char),
 }
 
-internals::impl_from_infallible!(InnerParseError);
-
 impl InnerParseError {
     fn convert(self, is_signed: bool) -> ParseAmountError {
         match self {
             Self::Overflow { is_negative } => OutOfRangeError { is_signed, is_greater_than_max: !is_negative }.into(),
-            Self::TooPrecise(error) => ParseAmountError::TooPrecise(error),
-            Self::MissingDigits(error) => ParseAmountError::MissingDigits(error),
-            Self::InputTooLarge(len) => ParseAmountError::InputTooLarge(InputTooLargeError { len }),
-            Self::InvalidCharacter(error) => ParseAmountError::InvalidCharacter(error),
+            Self::TooPrecise => ParseAmountError::TooPrecise,
+            Self::InvalidFormat => ParseAmountError::InvalidFormat,
+            Self::InputTooLarge => ParseAmountError::InputTooLarge,
+            Self::InvalidCharacter(c) => ParseAmountError::InvalidCharacter(c),
         }
     }
 }
@@ -645,7 +488,7 @@ fn split_amount_and_denomination(s: &str) -> Result<(&str, Denomination), ParseE
     let (i, j) = if let Some(i) = s.find(' ') {
         (i, i + 1)
     } else {
-        let i = s.find(|c: char| c.is_alphabetic()).ok_or(ParseError::MissingDenomination(MissingDenominationError))?;
+        let i = s.find(|c: char| c.is_alphabetic()).ok_or(ParseAmountError::InvalidFormat)?;
         (i, i)
     };
     Ok((&s[..i], s[j..].parse()?))
@@ -996,38 +839,34 @@ pub fn to_string_with_denomination(self, denom: Denomination) -> String {
     // Some arithmetic that doesn't fit in `core::ops` traits.
 
     /// Checked addition.
-    ///
     /// Returns [None] if overflow occurred.
     pub fn checked_add(self, rhs: Amount) -> Option<Amount> {
         self.0.checked_add(rhs.0).map(Amount)
     }
 
     /// Checked subtraction.
-    ///
     /// Returns [None] if overflow occurred.
     pub fn checked_sub(self, rhs: Amount) -> Option<Amount> {
         self.0.checked_sub(rhs.0).map(Amount)
     }
 
     /// Checked multiplication.
-    ///
     /// Returns [None] if overflow occurred.
     pub fn checked_mul(self, rhs: u64) -> Option<Amount> { self.0.checked_mul(rhs).map(Amount) }
 
     /// Checked integer division.
-    ///
     /// Be aware that integer division loses the remainder if no exact division
     /// can be made.
     /// Returns [None] if overflow occurred.
     pub fn checked_div(self, rhs: u64) -> Option<Amount> { self.0.checked_div(rhs).map(Amount) }
 
     /// Checked remainder.
-    ///
     /// Returns [None] if overflow occurred.
     pub fn checked_rem(self, rhs: u64) -> Option<Amount> { self.0.checked_rem(rhs).map(Amount) }
 
     /// Unchecked addition.
     ///
+    ///
     /// Computes `self + rhs`.  Panics in debug mode, wraps in release mode.
     pub fn unchecked_add(self, rhs: Amount) -> Amount {
         Self(self.0 + rhs.0)
@@ -2164,9 +2003,9 @@ fn floating_point() {
         assert_eq!(sf(-0.00012345, D::Bitcoin), Ok(ssat(-12345)));
 
         assert_eq!(f(-100.0, D::MilliSatoshi), Err(OutOfRangeError::negative().into()));
-        assert_eq!(f(11.22, D::Satoshi), Err(TooPreciseError { position: 3 }.into()));
-        assert_eq!(sf(-100.0, D::MilliSatoshi), Err(TooPreciseError { position: 1 }.into()));
-        assert_eq!(f(42.123456781, D::Bitcoin), Err(TooPreciseError { position: 11 }.into()));
+        assert_eq!(f(11.22, D::Satoshi), Err(ParseAmountError::TooPrecise));
+        assert_eq!(sf(-100.0, D::MilliSatoshi), Err(ParseAmountError::TooPrecise));
+        assert_eq!(f(42.123456781, D::Bitcoin), Err(ParseAmountError::TooPrecise));
         assert_eq!(sf(-184467440738.0, D::Bitcoin), Err(OutOfRangeError::too_small().into()));
         assert_eq!(f(18446744073709551617.0, D::Satoshi), Err(OutOfRangeError::too_big(false).into()));
 
@@ -2203,26 +2042,26 @@ fn parsing() {
         let p = Amount::from_str_in;
         let sp = SignedAmount::from_str_in;
 
-        assert_eq!(p("x", btc), Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 0 })));
-        assert_eq!(p("-", btc), Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })));
-        assert_eq!(sp("-", btc), Err(E::from(MissingDigitsError { kind: MissingDigitsKind::OnlyMinusSign })));
-        assert_eq!(p("-1.0x", btc), Err(E::from(InvalidCharacterError { invalid_char: 'x', position: 4 })));
-        assert_eq!(p("0.0 ", btc), Err(E::from(InvalidCharacterError { invalid_char: ' ', position: 3 })));
-        assert_eq!(p("0.000.000", btc), Err(E::from(InvalidCharacterError { invalid_char: '.', position: 5 })));
+        assert_eq!(p("x", btc), Err(E::InvalidCharacter('x')));
+        assert_eq!(p("-", btc), Err(E::InvalidFormat));
+        assert_eq!(sp("-", btc), Err(E::InvalidFormat));
+        assert_eq!(p("-1.0x", btc), Err(E::InvalidCharacter('x')));
+        assert_eq!(p("0.0 ", btc), Err(ParseAmountError::InvalidCharacter(' ')));
+        assert_eq!(p("0.000.000", btc), Err(E::InvalidFormat));
         #[cfg(feature = "alloc")]
         let more_than_max = format!("1{}", Amount::MAX);
         #[cfg(feature = "alloc")]
         assert_eq!(p(&more_than_max, btc), Err(OutOfRangeError::too_big(false).into()));
-        assert_eq!(p("0.000000042", btc), Err(TooPreciseError { position: 10 }.into()));
-        assert_eq!(p("999.0000000", msat), Err(TooPreciseError { position: 0 }.into()));
-        assert_eq!(p("1.0000000", msat), Err(TooPreciseError { position: 0 }.into()));
-        assert_eq!(p("1.1", msat), Err(TooPreciseError { position: 0 }.into()));
-        assert_eq!(p("1000.1", msat), Err(TooPreciseError { position: 5 }.into()));
-        assert_eq!(p("1001.0000000", msat), Err(TooPreciseError { position: 3 }.into()));
-        assert_eq!(p("1000.0000001", msat), Err(TooPreciseError { position: 11 }.into()));
-        assert_eq!(p("1000.1000000", msat), Err(TooPreciseError { position: 5 }.into()));
-        assert_eq!(p("1100.0000000", msat), Err(TooPreciseError { position: 1 }.into()));
-        assert_eq!(p("10001.0000000", msat), Err(TooPreciseError { position: 4 }.into()));
+        assert_eq!(p("0.000000042", btc), Err(E::TooPrecise));
+        assert_eq!(p("999.0000000", msat), Err(E::TooPrecise));
+        assert_eq!(p("1.0000000", msat), Err(E::TooPrecise));
+        assert_eq!(p("1.1", msat), Err(E::TooPrecise));
+        assert_eq!(p("1000.1", msat), Err(E::TooPrecise));
+        assert_eq!(p("1001.0000000", msat), Err(E::TooPrecise));
+        assert_eq!(p("1000.0000001", msat), Err(E::TooPrecise));
+        assert_eq!(p("1000.1000000", msat), Err(E::TooPrecise));
+        assert_eq!(p("1100.0000000", msat), Err(E::TooPrecise));
+        assert_eq!(p("10001.0000000", msat), Err(E::TooPrecise));
 
         assert_eq!(p("1", btc), Ok(Amount::from_sat(1_000_000_00)));
         assert_eq!(sp("-.5", btc), Ok(SignedAmount::from_sat(-500_000_00)));
@@ -2250,7 +2089,7 @@ fn parsing() {
             assert!(Amount::from_str_in(&(amount + Amount(1)).to_string_in(sat), sat).is_ok());
         }
 
-        assert_eq!(p("12.000", Denomination::MilliSatoshi), Err(TooPreciseError { position: 0 }.into()));
+        assert_eq!(p("12.000", Denomination::MilliSatoshi), Err(E::TooPrecise));
         // exactly 50 chars.
         assert_eq!(
             p("100000000000000.0000000000000000000000000000000000", Denomination::Bitcoin),
@@ -2259,7 +2098,7 @@ fn parsing() {
         // more than 50 chars.
         assert_eq!(
             p("100000000000000.00000000000000000000000000000000000", Denomination::Bitcoin),
-            Err(E::InputTooLarge(InputTooLargeError { len: 51 }))
+            Err(E::InputTooLarge)
         );
     }
 
@@ -2493,7 +2332,7 @@ fn from_str() {
 
         use super::ParseAmountError as E;
 
-        assert_eq!(Amount::from_str("x BTC"), Err(E::from(E::from(InvalidCharacterError { invalid_char: 'x', position: 0 })).into()));
+        assert_eq!(Amount::from_str("x BTC"), Err(E::InvalidCharacter('x').into()));
         assert_eq!(
             Amount::from_str("xBTC"),
             Err(Unknown(UnknownDenominationError("xBTC".into())).into()),
@@ -2502,7 +2341,7 @@ fn from_str() {
             Amount::from_str("5 BTC BTC"),
             Err(Unknown(UnknownDenominationError("BTC BTC".into())).into()),
         );
-        assert_eq!(Amount::from_str("5BTC BTC"), Err(E::from(InvalidCharacterError { invalid_char: 'B', position: 1 }).into()));
+        assert_eq!(Amount::from_str("5BTC BTC"), Err(E::InvalidCharacter('B').into()));
         assert_eq!(
             Amount::from_str("5 5 BTC"),
             Err(Unknown(UnknownDenominationError("5 BTC".into())).into()),
@@ -2538,10 +2377,10 @@ fn scase(s: &str, expected: Result<SignedAmount, impl Into<ParseError>>) {
 
         case("-1 BTC", Err(OutOfRangeError::negative()));
         case("-0.0 BTC", Err(OutOfRangeError::negative()));
-        case("0.123456789 BTC", Err(TooPreciseError { position: 10 }));
-        scase("-0.1 satoshi", Err(TooPreciseError { position: 3 }));
-        case("0.123456 mBTC", Err(TooPreciseError { position: 7 }));
-        scase("-1.001 bits", Err(TooPreciseError { position: 5 }));
+        case("0.123456789 BTC", Err(E::TooPrecise));
+        scase("-0.1 satoshi", Err(E::TooPrecise));
+        case("0.123456 mBTC", Err(E::TooPrecise));
+        scase("-1.001 bits", Err(E::TooPrecise));
         scase("-200000000000 BTC", Err(OutOfRangeError::too_small()));
         case("18446744073709551616 sat", Err(OutOfRangeError::too_big(false)));
 
@@ -2621,11 +2460,11 @@ fn to_from_string_in() {
         );
         assert_eq!(
             sa_str(&sa_sat(i64::MAX).to_string_in(D::Satoshi), D::NanoBitcoin),
-            Err(TooPreciseError { position: 18 }.into())
+            Err(ParseAmountError::TooPrecise)
         );
         assert_eq!(
             sa_str(&sa_sat(i64::MIN).to_string_in(D::Satoshi), D::NanoBitcoin),
-            Err(TooPreciseError { position: 19 }.into())
+            Err(ParseAmountError::TooPrecise)
         );
 
         assert_eq!(
@@ -2634,11 +2473,11 @@ fn to_from_string_in() {
         );
         assert_eq!(
             sa_str(&sa_sat(i64::MAX).to_string_in(D::Satoshi), D::PicoBitcoin),
-            Err(TooPreciseError { position: 18 }.into())
+            Err(ParseAmountError::TooPrecise)
         );
         assert_eq!(
             sa_str(&sa_sat(i64::MIN).to_string_in(D::Satoshi), D::PicoBitcoin),
-            Err(TooPreciseError { position: 19 }.into())
+            Err(ParseAmountError::TooPrecise)
         );
     }
 
@@ -2725,7 +2564,7 @@ struct T {
         // errors
         let t: Result<T, serde_json::Error> =
             serde_json::from_str("{\"amt\": 1000000.000000001, \"samt\": 1}");
-        assert!(t.unwrap_err().to_string().contains(&ParseAmountError::TooPrecise(TooPreciseError { position: 16 }).to_string()));
+        assert!(t.unwrap_err().to_string().contains(&ParseAmountError::TooPrecise.to_string()));
         let t: Result<T, serde_json::Error> = serde_json::from_str("{\"amt\": -1, \"samt\": 1}");
         assert!(t.unwrap_err().to_string().contains(&OutOfRangeError::negative().to_string()));
     }


Test patch:
diff --git a/bitcoin/contrib/test.sh b/bitcoin/contrib/test.sh
new file mode 100755
index 00000000..d1b51511
--- /dev/null
+++ b/bitcoin/contrib/test.sh
@@ -0,0 +1,131 @@
+#!/usr/bin/env bash
+
+set -ex
+
+FEATURES="std rand-std rand serde secp-recovery bitcoinconsensus-std base64 bitcoinconsensus"
+
+cargo --version
+rustc --version
+
+# Some tests require certain toolchain types.
+NIGHTLY=false
+STABLE=true
+if cargo --version | grep nightly; then
+    STABLE=false
+    NIGHTLY=true
+fi
+if cargo --version | grep beta; then
+    STABLE=false
+fi
+
+# Make all cargo invocations verbose
+export CARGO_TERM_VERBOSE=true
+
+# Defaults / sanity checks
+cargo build
+cargo test
+
+if [ "$DO_LINT" = true ]
+then
+    cargo clippy --locked --all-features --all-targets -- -D warnings
+    cargo clippy --locked --example bip32 -- -D warnings
+    cargo clippy --locked --example handshake --features=rand-std -- -D warnings
+    cargo clippy --locked --example ecdsa-psbt --features=bitcoinconsensus -- -D warnings
+    cargo clippy --locked --example sign-tx-segwit-v0 --features=rand-std -- -D warnings
+    cargo clippy --locked --example sign-tx-taproot --features=rand-std -- -D warnings
+    cargo clippy --locked --example taproot-psbt --features=rand-std,bitcoinconsensus -- -D warnings
+
+    # We should not have any duplicate dependencies. This catches mistakes made upgrading dependencies
+    # in one crate and not in another (e.g. upgrade bitcoin_hashes in bitcoin but not in secp).
+    duplicate_dependencies=$(
+        # Only show the actual duplicated deps, not their reverse tree, then
+        # whitelist the 'syn' crate which is duplicated but it's not our fault.
+        #
+        # Whitelist `bitcoin_hashes` while we release it and until secp v0.28.0 comes out.
+        cargo tree  --target=all --all-features --duplicates \
+            | grep '^[0-9A-Za-z]' \
+            | grep -v 'syn' \
+            | grep -v 'bitcoin_hashes' \
+            | wc -l
+                          )
+    if [ "$duplicate_dependencies" -ne 0 ]; then
+        echo "Dependency tree is broken, contains duplicates"
+        cargo tree  --target=all --all-features --duplicates
+        exit 1
+    fi
+fi
+
+if [ "$DO_FEATURE_MATRIX" = true ]; then
+    cargo build --locked --no-default-features
+    cargo test --locked --no-default-features
+
+    # All features
+    cargo build --locked --no-default-features --features="$FEATURES"
+    cargo test --locked --no-default-features --features="$FEATURES"
+    # Single features
+    for feature in ${FEATURES}
+    do
+        cargo build --locked --no-default-features --features="$feature"
+        cargo test --locked --no-default-features --features="$feature"
+		# All combos of two features
+		for featuretwo in ${FEATURES}; do
+			cargo build --locked --no-default-features --features="$feature $featuretwo"
+			cargo test --locked --no-default-features --features="$feature $featuretwo"
+		done
+    done
+fi
+
+cargo run --locked --example bip32 7934c09359b234e076b9fa5a1abfd38e3dc2a9939745b7cc3c22a48d831d14bd
+cargo run --locked --no-default-features --example bip32 7934c09359b234e076b9fa5a1abfd38e3dc2a9939745b7cc3c22a48d831d14bd
+
+cargo run --locked --example ecdsa-psbt --features=bitcoinconsensus
+cargo run --locked --example sign-tx-segwit-v0 --features=rand-std -- -D warnings
+cargo run --locked --example sign-tx-taproot --features=rand-std -- -D warnings
+cargo run --locked --example taproot-psbt --features=rand-std,bitcoinconsensus
+
+# Build the docs if told to (this only works with the nightly toolchain)
+if [ "$DO_DOCSRS" = true ]; then
+    RUSTDOCFLAGS="--cfg docsrs -D warnings -D rustdoc::broken-intra-doc-links" cargo +nightly doc --all-features
+fi
+
+# Build the docs with a stable toolchain, in unison with the DO_DOCSRS command
+# above this checks that we feature guarded docs imports correctly.
+if [ "$DO_DOCS" = true ]; then
+    RUSTDOCFLAGS="-D warnings" cargo +stable doc --all-features
+fi
+
+# Run formatter if told to.
+if [ "$DO_FMT" = true ]; then
+    if [ "$NIGHTLY" = false ]; then
+        echo "DO_FMT requires a nightly toolchain (consider using RUSTUP_TOOLCHAIN)"
+        exit 1
+    fi
+    rustup component add rustfmt
+    cargo fmt --check
+fi
+
+# Bench if told to, only works with non-stable toolchain (nightly, beta).
+if [ "$DO_BENCH" = true ]
+then
+    if [ "$STABLE" = true ]; then
+        if [ -n "$RUSTUP_TOOLCHAIN" ]; then
+            echo "RUSTUP_TOOLCHAIN is set to a stable toolchain but DO_BENCH requires a non-stable (beta, nightly) toolchain"
+        else
+            echo "DO_BENCH requires a non-stable (beta, nightly) toolchain"
+        fi
+        exit 1
+    fi
+    RUSTFLAGS='--cfg=bench' cargo bench
+fi
+
+# Use as dependency if told to
+if [ "$AS_DEPENDENCY" = true ]
+then
+    cargo new dep_test 2> /dev/null # Mute warning about workspace, fixed below.
+    cd dep_test
+    echo 'bitcoin = { path = "..", features = ["serde"] }\n\n' >> Cargo.toml
+    # Adding an empty workspace section excludes this crate from the rust-bitcoin workspace.
+    echo '[workspace]\n\n' >> Cargo.toml
+
+    cargo test --verbose
+fi
diff --git a/bitcoin/src/blockdata/script/tests.rs b/bitcoin/src/blockdata/script/tests.rs
index 9b4e1f71..7fa1640c 100644
--- a/bitcoin/src/blockdata/script/tests.rs
+++ b/bitcoin/src/blockdata/script/tests.rs
@@ -354,15 +354,15 @@ fn script_hashes() {
 #[test]
 fn provably_unspendable_test() {
     // p2pk
-    assert!(!ScriptBuf::from_hex("410446ef0102d1ec5240f0d061a4246c1bdef63fc3dbab7733052fbbf0ecd8f41fc26bf049ebb4f9527f374280259e7cfa99c48b0e3f39c51347a19a5819651503a5ac").unwrap().is_op_return());
-    assert!(!ScriptBuf::from_hex("4104ea1feff861b51fe3f5f8a3b12d0f4712db80e919548a80839fc47c6a21e66d957e9c5d8cd108c7a2d2324bad71f9904ac0ae7336507d785b17a2c115e427a32fac").unwrap().is_op_return());
+    assert!(!ScriptBuf::from_hex("410446ef0102d1ec5240f0d061a4246c1bdef63fc3dbab7733052fbbf0ecd8f41fc26bf049ebb4f9527f374280259e7cfa99c48b0e3f39c51347a19a5819651503a5ac").unwrap().is_provably_unspendable());
+    assert!(!ScriptBuf::from_hex("4104ea1feff861b51fe3f5f8a3b12d0f4712db80e919548a80839fc47c6a21e66d957e9c5d8cd108c7a2d2324bad71f9904ac0ae7336507d785b17a2c115e427a32fac").unwrap().is_provably_unspendable());
     // p2pkhash
     assert!(!ScriptBuf::from_hex("76a914ee61d57ab51b9d212335b1dba62794ac20d2bcf988ac")
         .unwrap()
-        .is_op_return());
+        .is_provably_unspendable());
     assert!(ScriptBuf::from_hex("6aa9149eb21980dc9d413d8eac27314938b9da920ee53e87")
         .unwrap()
-        .is_op_return());
+        .is_provably_unspendable());
 }
 
 #[test]
diff --git a/contrib/test.sh b/contrib/test.sh
new file mode 100755
index 00000000..a825919c
--- /dev/null
+++ b/contrib/test.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+set -ex
+
+CRATES="bitcoin hashes units internals fuzz"
+DEPS="recent minimal"
+
+for dep in $DEPS
+do
+    cp "Cargo-$dep.lock" Cargo.lock
+    for crate in ${CRATES}
+    do
+        (
+            cd "$crate"
+            ./contrib/test.sh
+        )
+    done
+    if [ "$dep" = recent ];
+    then
+        # We always test committed dependencies but we want to warn if they could've been updated
+        cargo update
+        if diff Cargo-recent.lock Cargo.lock;
+        then
+            echo Dependencies are up to date
+        else
+            echo "::warning file=Cargo-recent.lock::Dependencies could be updated"
+        fi
+    fi
+done
+
+exit 0
diff --git a/fuzz/contrib/test.sh b/fuzz/contrib/test.sh
new file mode 100755
index 00000000..becc2953
--- /dev/null
+++ b/fuzz/contrib/test.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+
+set -ex
+
+FEATURES=""
+
+cargo --version
+rustc --version
+
+# Make all cargo invocations verbose
+export CARGO_TERM_VERBOSE=true
+
+# Pin dependencies as required if we are using MSRV toolchain.
+if cargo --version | grep "1\.41"; then
+    # 1.0.157 uses syn 2.0 which requires edition 2021
+    cargo update -p serde --precise 1.0.156
+    # 1.0.108 uses `matches!` macro so does not work with Rust 1.41.1, bad `syn` no biscuit.
+    cargo update -p syn --precise 1.0.107
+    # Half 1.8 uses edition 2021 features
+    cargo update -p half --precise 1.7.1
+fi
+
+if [ "$DO_LINT" = true ]
+then
+    cargo clippy --all-features --all-targets -- -D warnings
+fi
+
+# Defaults / sanity checks
+cargo build
+cargo test
+
+# Address Sanitizer
+if [ "$DO_ASAN" = true ]; then
+    cargo clean
+    CC='clang -fsanitize=address -fno-omit-frame-pointer'                                        \
+    RUSTFLAGS='-Zsanitizer=address -Clinker=clang -Cforce-frame-pointers=yes'                    \
+    ASAN_OPTIONS='detect_leaks=1 detect_invalid_pointer_pairs=1 detect_stack_use_after_return=1' \
+    cargo test --lib --no-default-features --features="$FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
+    cargo clean
+    CC='clang -fsanitize=memory -fno-omit-frame-pointer'                                         \
+    RUSTFLAGS='-Zsanitizer=memory -Zsanitizer-memory-track-origins -Cforce-frame-pointers=yes'   \
+    cargo test --lib --no-default-features --features="$FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
+fi
+
diff --git a/internals/contrib/test.sh b/internals/contrib/test.sh
new file mode 100755
index 00000000..e8e375d5
--- /dev/null
+++ b/internals/contrib/test.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+
+set -ex
+
+FEATURES="std alloc"
+
+cargo --version
+rustc --version
+
+# Work out if we are using a nightly toolchain.
+NIGHTLY=false
+if cargo --version | grep nightly >/dev/null; then
+    NIGHTLY=true
+fi
+
+# Make all cargo invocations verbose
+export CARGO_TERM_VERBOSE=true
+
+# Defaults / sanity checks
+cargo --locked build
+cargo --locked test
+
+if [ "$DO_LINT" = true ]
+then
+    cargo clippy --locked --all-features --all-targets -- -D warnings
+fi
+
+if [ "$DO_FEATURE_MATRIX" = true ]; then
+    # No features
+    cargo build --locked --no-default-features
+    cargo test --locked --no-default-features
+
+    # All features
+    cargo build --locked --no-default-features --features="$FEATURES"
+    cargo test --locked --no-default-features --features="$FEATURES"
+
+    # Single features
+    for feature in ${FEATURES}
+    do
+        cargo build --locked --no-default-features --features="$feature"
+        cargo test --locked --no-default-features --features="$feature"
+    done
+fi
+
+# Build the docs if told to (this only works with the nightly toolchain)
+if [ "$DO_DOCSRS" = true ]; then
+    RUSTDOCFLAGS="--cfg docsrs -D warnings -D rustdoc::broken-intra-doc-links" cargo +nightly doc --all-features
+fi
+
+# Build the docs with a stable toolchain, in unison with the DO_DOCSRS command
+# above this checks that we feature guarded docs imports correctly.
+if [ "$DO_DOCS" = true ]; then
+    RUSTDOCFLAGS="-D warnings" cargo +stable doc --locked --all-features
+fi
+
+# Run formatter if told to.
+if [ "$DO_FMT" = true ]; then
+    if [ "$NIGHTLY" = false ]; then
+        echo "DO_FMT requires a nightly toolchain (consider using RUSTUP_TOOLCHAIN)"
+        exit 1
+    fi
+    rustup component add rustfmt
+    cargo fmt --check
+fi