Repository: rust-bitcoin/rust-bitcoin
Pull request #2503 (2024-02-25 21:45:04)

base58: Re-name crate to base58ck
The current name `base58check` is taken, as is `base58`. Use `base58ck` instead.

Add a brief section to the readme about the crate naming.

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


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **83.907%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/65901232/badge)](https://coveralls.io/builds/65901232) |
| :-- | --: |
| Change from base [Build 8031644825](https://coveralls.io/builds/65894928): |  0.0% |
| Covered Lines: | 19438 |
| Relevant Lines: | 23166 |

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

tcharding: Now we have a nice short name perhaps we should use it for the crate's directory name as well? (Currently `base58`.)
tcharding: I removed all mention of "appears unmaintained" just to be less inflammatory, we don't know they are not maintained and we get in enough fights wit the rest of the Rust community as it is.
tcharding: You were reviewing while was force pushing to fix my mistakes @Kixunil, sorry about that.
Kixunil: > "appears unmaintained" just to be less inflammatory, we don't know they are not maintained and we get in enough fights with the rest of the Rust community as it is.

If anyone is offended by simply stating that their crate which is not 1.0 didn't update for years and didn't respond to messages **appears** unmaintained then better know that they're that kind of person. You can't please everyone and I would definitely not go out of my way to please those kinds of people.
tcharding: Fair points, will add it back it.
tcharding: Will fix to use correct spelling of whereas.
Kixunil: Looks like the maintainer of `base58` is willing to transfer ownership, so let's just wait if he does.
tcharding: I kind of think we should just use the new name because of how the current `base58check` crate depends on `base58` and `sha2`. Our base58 crate is quite different.
tcharding: Its not like we are trying to sell our crate to the world, its mainly just so `rust-bitcoin` can use it. People who want it will find it.
apoelstra: I have the same feeling -- we should leave `base58` and `base58check` as the existing pair.

But let's wait a bit to see what the base58 maintainer thinks.
apoelstra: In 76aa10bd2aeb2ebc8094780ff42198b41999ffb4 if you are going to add the word `whereas` it needs to be preceded by a comma, not a semicolon. Otherwise ACK.
tcharding: If only I spoke English as a first language, oh wait ... :(

Patch:
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index 4f3cd125..07863dc7 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -15,7 +15,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
 
 [[package]]
-name = "base58check"
+name = "base58ck"
 version = "0.1.0"
 dependencies = [
  "bitcoin-internals",
@@ -49,7 +49,7 @@ dependencies = [
 name = "bitcoin"
 version = "0.31.0"
 dependencies = [
- "base58check",
+ "base58ck",
  "base64",
  "bech32",
  "bincode",
@@ -88,7 +88,7 @@ dependencies = [
 
 [[package]]
 name = "bitcoin-io"
-version = "0.1.2"
+version = "0.1.1"
 
 [[package]]
 name = "bitcoin-units"
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index 5601a833..a9613b2a 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -15,7 +15,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
 
 [[package]]
-name = "base58check"
+name = "base58ck"
 version = "0.1.0"
 dependencies = [
  "bitcoin-internals",
@@ -48,7 +48,7 @@ dependencies = [
 name = "bitcoin"
 version = "0.31.0"
 dependencies = [
- "base58check",
+ "base58ck",
  "base64",
  "bech32",
  "bincode",
@@ -87,7 +87,7 @@ dependencies = [
 
 [[package]]
 name = "bitcoin-io"
-version = "0.1.2"
+version = "0.1.1"
 
 [[package]]
 name = "bitcoin-units"
diff --git a/Cargo.toml b/Cargo.toml
index e076d996..7cf9a0f3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,7 @@
 members = ["bitcoin", "hashes", "internals", "fuzz", "io", "units", "base58"]
 resolver = "2"
 
-[patch.crates-io.base58check]
+[patch.crates-io.base58ck]
 path = "base58"
 
 [patch.crates-io.bitcoin]
diff --git a/SECURITY.md b/SECURITY.md
index c299816e..2e161330 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -10,7 +10,7 @@ As a general rule, an issue is a security vulnerability if it could lead to:
 * Loss of funds
 * Loss of privacy
 * Censorship (including e.g. by attaching an incorrectly low fee to a transaction)
-* Any "ordinary" security problem, such as remote code execution or invalid memory access
+* Any "ordinary" security problem, such as remote code execution or invalid memory acccess
 
 In general, use your best judgement in determining whether an issue is a security issue. If not,
 go ahead and post it to the public issue tracker.
diff --git a/base58/Cargo.toml b/base58/Cargo.toml
index fb312dd3..97618bf1 100644
--- a/base58/Cargo.toml
+++ b/base58/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "base58check"
+name = "base58ck"
 version = "0.1.0"
 authors = ["Andrew Poelstra <apoelstra@wpsoftware.net>"]
 license = "CC0-1.0"
diff --git a/base58/README.md b/base58/README.md
index a8571737..211b8574 100644
--- a/base58/README.md
+++ b/base58/README.md
@@ -4,16 +4,22 @@ Bitcoin base58 encoding
 This crate provides encoding and decoding of base58 strings as defined by the Bitcoin ecosystem
 including the checksum.
 
-There are two other crates on crates.io that implement base58 encoding and decoding. This
-crate differs from them because:
+There are a bunch of crates on crates.io that implement base58 encoding and decoding. The more
+obviously named ones differ from this crate because:
 
 1. [bitcoin-base58](https://crates.io/crates/bitcoin-base58) is transpiled from the C++ code in
-   Bitcoin Core as part of a large long-term transpilation project; this crate is a pure Rust
-   implementation intended to be production-ready and to provide an Rust-idiomatic API.
+   Bitcoin Core as part of a large long-term transpilation project, whereas this crate is a pure
+   Rust implementation intended to be production-ready and to provide an Rust-idiomatic API.
 
-2. [base58](https://crates.io/crates/base58) is incomplete and appears unmaintained as of
-   February 2024. It does not validate checksums and will therefore accept invalid Bitcoin
-   addresses. It may be appropriate in cases where performance is more important than safety.
+2. [base58](https://crates.io/crates/base58) implements parsing but does not validate checksums (see
+   `base58check`). It may be appropriate in cases where performance is more important than safety.
+   Appears unmaintained.
+
+3. [base58check](https://crates.io/crates/base58check) Adds checksum to the `base58` crate and
+   depends on [sha2](https://crates.io/crates/sha2/0.8.2) for hashing. Appears unmaintained.
+
+This crate uses [bitcoin_hashes](https://crates.io/crates/bitcoin_hashes) when hashing to calculate
+the checksum.
 
 
 ## Minimum Supported Rust Version (MSRV)
diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml
index fb287855..4aaed915 100644
--- a/bitcoin/Cargo.toml
+++ b/bitcoin/Cargo.toml
@@ -28,7 +28,7 @@ all-features = true
 rustdoc-args = ["--cfg", "docsrs"]
 
 [dependencies]
-base58 = { package = "base58check", version = "0.1.0", default-features = false }
+base58 = { package = "base58ck", 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"] }
diff --git a/bitcoin/embedded/Cargo.toml b/bitcoin/embedded/Cargo.toml
index ccd22f96..b12f4155 100644
--- a/bitcoin/embedded/Cargo.toml
+++ b/bitcoin/embedded/Cargo.toml
@@ -27,7 +27,7 @@ codegen-units = 1 # better optimizations
 debug = true # symbols are nice and they don't increase the size on Flash
 lto = true # better optimizations
 
-[patch.crates-io.base58check]
+[patch.crates-io.base58ck]
 path = "../../base58"
 
 [patch.crates-io.bitcoin_hashes]
diff --git a/bitcoin/examples/ecdsa-psbt.rs b/bitcoin/examples/ecdsa-psbt.rs
index b5984c8e..fa5b068e 100644
--- a/bitcoin/examples/ecdsa-psbt.rs
+++ b/bitcoin/examples/ecdsa-psbt.rs
@@ -133,7 +133,7 @@ fn new<C: Signing>(secp: &Secp256k1<C>, xpriv: &str) -> Result<ExportData> {
     fn master_fingerprint(&self) -> Fingerprint { self.master_xpub.fingerprint() }
 
     /// Signs `psbt` with this signer.
-    fn sign_psbt<C: Signing + Verification>(&self, secp: &Secp256k1<C>, mut psbt: Psbt) -> Result<Psbt> {
+    fn sign_psbt<C: Signing>(&self, secp: &Secp256k1<C>, mut psbt: Psbt) -> Result<Psbt> {
         match psbt.sign(&self.master_xpriv, secp) {
             Ok(keys) => assert_eq!(keys.len(), 1),
             Err((_, e)) => {
diff --git a/bitcoin/src/blockdata/constants.rs b/bitcoin/src/blockdata/constants.rs
index 1f8de58b..33e5b543 100644
--- a/bitcoin/src/blockdata/constants.rs
+++ b/bitcoin/src/blockdata/constants.rs
@@ -30,7 +30,7 @@
 pub const DIFFCHANGE_TIMESPAN: u32 = 14 * 24 * 3600;
 
 /// The factor that non-witness serialization data is multiplied by during weight calculation.
-pub const WITNESS_SCALE_FACTOR: usize = units::weight::WITNESS_SCALE_FACTOR;
+pub const WITNESS_SCALE_FACTOR: usize = 4;
 /// The maximum allowed number of signature check operations in a block.
 pub const MAX_BLOCK_SIGOPS_COST: i64 = 80_000;
 /// Mainnet (bitcoin) pubkey address prefix.
diff --git a/units/src/fee_rate.rs b/bitcoin/src/blockdata/fee_rate.rs
similarity index 82%
rename from units/src/fee_rate.rs
rename to bitcoin/src/blockdata/fee_rate.rs
index 9dcb88dd..6aaf83f9 100644
--- a/units/src/fee_rate.rs
+++ b/bitcoin/src/blockdata/fee_rate.rs
@@ -5,11 +5,8 @@
 use core::fmt;
 use core::ops::{Div, Mul};
 
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
-
 use crate::amount::Amount;
-use crate::weight::Weight;
+use crate::blockdata::weight::Weight;
 
 /// Represents fee rate.
 ///
@@ -17,6 +14,7 @@
 /// up the types as well as basic formatting features.
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
 #[cfg_attr(feature = "serde", serde(transparent))]
 pub struct FeeRate(u64);
 
@@ -95,6 +93,18 @@ pub fn checked_mul_by_weight(self, rhs: Weight) -> Option<Amount> {
     /// if overflow occurred.
     ///
     /// This is equivalent to `Self::checked_mul_by_weight()`.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use bitcoin::{absolute, transaction, FeeRate, Transaction};
+    /// # // Dummy transaction.
+    /// # let tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, input: vec![], output: vec![] };
+    ///
+    /// let rate = FeeRate::from_sat_per_vb(1).expect("1 sat/vbyte is valid");
+    /// let fee = rate.fee_wu(tx.weight()).unwrap();
+    /// assert_eq!(fee.to_sat(), tx.vsize() as u64);
+    /// ```
     pub fn fee_wu(self, weight: Weight) -> Option<Amount> { self.checked_mul_by_weight(weight) }
 
     /// Calculates fee by multiplying this fee rate by weight, in virtual bytes, returning `None`
@@ -143,10 +153,12 @@ impl Div<Weight> for Amount {
     fn div(self, rhs: Weight) -> Self::Output { FeeRate(self.to_sat() * 1000 / rhs.to_wu()) }
 }
 
-crate::impl_parse_str_from_int_infallible!(FeeRate, u64, from_sat_per_kwu);
+crate::parse::impl_parse_str_from_int_infallible!(FeeRate, u64, from_sat_per_kwu);
 
 #[cfg(test)]
 mod tests {
+    use std::u64;
+
     use super::*;
 
     #[test]
@@ -224,4 +236,21 @@ fn checked_div_test() {
         let fee_rate = FeeRate(10).checked_div(0);
         assert!(fee_rate.is_none());
     }
+
+    #[test]
+    fn fee_convenience_functions_agree() {
+        use hex::test_hex_unwrap as hex;
+
+        use crate::blockdata::transaction::Transaction;
+        use crate::consensus::Decodable;
+
+        const SOME_TX: &str = "0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000";
+
+        let raw_tx = hex!(SOME_TX);
+        let tx: Transaction = Decodable::consensus_decode(&mut raw_tx.as_slice()).unwrap();
+
+        let rate = FeeRate::from_sat_per_vb(1).expect("1 sat/byte is valid");
+
+        assert_eq!(rate.fee_vb(tx.vsize() as u64), rate.fee_wu(tx.weight()));
+    }
 }
diff --git a/bitcoin/src/blockdata/locktime/absolute.rs b/bitcoin/src/blockdata/locktime/absolute.rs
index a8e9a993..6cb52c85 100644
--- a/bitcoin/src/blockdata/locktime/absolute.rs
+++ b/bitcoin/src/blockdata/locktime/absolute.rs
@@ -6,24 +6,33 @@
 //! whether `LockTime < LOCKTIME_THRESHOLD`.
 //!
 
-use core::cmp::Ordering;
+use core::cmp::{Ordering, PartialOrd};
 use core::{fmt, mem};
 
+use internals::write_err;
 use io::{BufRead, Write};
 #[cfg(all(test, mutate))]
 use mutagen::mutate;
-use units::parse;
 
-use crate::consensus::encode::{self, Decodable, Encodable};
-use crate::error::{PrefixedHexError, UnprefixedHexError, ContainsPrefixError, MissingPrefixError};
 #[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::prelude::*;
 
-#[rustfmt::skip]                // Keep public re-exports separate.
-#[doc(inline)]
-pub use units::locktime::absolute::{
-    Height, Time, LOCK_TIME_THRESHOLD, ConversionError, ParseHeightError, ParseTimeError,
-};
+/// The Threshold for deciding whether a lock time value is a height or a time (see [Bitcoin Core]).
+///
+/// `LockTime` values _below_ the threshold are interpreted as block heights, values _above_ (or
+/// equal to) the threshold are interpreted as block times (UNIX timestamp, seconds since epoch).
+///
+/// Bitcoin is able to safely use this value because a block height greater than 500,000,000 would
+/// never occur because it would represent a height in approximately 9500 years. Conversely, block
+/// times under 500,000,000 will never happen because they would represent times before 1986 which
+/// are, for obvious reasons, not useful within the Bitcoin network.
+///
+/// [Bitcoin Core]: https://github.com/bitcoin/bitcoin/blob/9ccaee1d5e2e4b79b0a7c29aadb41b97e4741332/src/script/script.h#L39
+pub const LOCK_TIME_THRESHOLD: u32 = 500_000_000;
 
 /// An absolute lock time value, representing either a block height or a UNIX timestamp (seconds
 /// since epoch).
@@ -129,7 +138,7 @@ pub fn from_unprefixed_hex(s: &str) -> Result<Self, UnprefixedHexError> {
     /// assert_eq!(lock_time.to_consensus_u32(), n_lock_time);
     #[inline]
     pub fn from_consensus(n: u32) -> Self {
-        if units::locktime::absolute::is_block_height(n) {
+        if is_block_height(n) {
             Self::Blocks(Height::from_consensus(n).expect("n is valid"))
         } else {
             Self::Seconds(Time::from_consensus(n).expect("n is valid"))
@@ -284,7 +293,7 @@ pub fn to_consensus_u32(self) -> u32 {
     }
 }
 
-units::impl_parse_str_from_int_infallible!(LockTime, u32, from_consensus);
+impl_parse_str_from_int_infallible!(LockTime, u32, from_consensus);
 
 impl From<Height> for LockTime {
     #[inline]
@@ -406,6 +415,406 @@ fn arbitrary_cmp(&self, other: &Self) -> Ordering {
     }
 }
 
+/// An absolute block height, guaranteed to always contain a valid height value.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
+pub struct Height(u32);
+
+impl Height {
+    /// Absolute block height 0, the genesis block.
+    pub const ZERO: Self = Height(0);
+
+    /// The minimum absolute block height (0), the genesis block.
+    pub const MIN: Self = Self::ZERO;
+
+    /// 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
+    ///
+    /// If `n` does not represent a block height value (see documentation on [`LockTime`]).
+    ///
+    /// # Examples
+    /// ```rust
+    /// use bitcoin::locktime::absolute::Height;
+    ///
+    /// let h: u32 = 741521;
+    /// let height = Height::from_consensus(h).expect("invalid height value");
+    /// assert_eq!(height.to_consensus_u32(), h);
+    /// ```
+    #[inline]
+    pub fn from_consensus(n: u32) -> Result<Height, ConversionError> {
+        if is_block_height(n) {
+            Ok(Self(n))
+        } else {
+            Err(ConversionError::invalid_height(n))
+        }
+    }
+
+    /// Converts this `Height` to its inner `u32` value.
+    ///
+    /// # Examples
+    /// ```rust
+    /// use bitcoin::absolute::LockTime;
+    ///
+    /// let n_lock_time: u32 = 741521;
+    /// let lock_time = LockTime::from_consensus(n_lock_time);
+    /// assert!(lock_time.is_block_height());
+    /// assert_eq!(lock_time.to_consensus_u32(), n_lock_time);
+    #[inline]
+    pub fn to_consensus_u32(self) -> u32 { self.0 }
+}
+
+impl fmt::Display for Height {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
+}
+
+impl_parse_str!(Height, ParseHeightError, parser(Height::from_consensus));
+
+/// Error returned when parsing block height fails.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct ParseHeightError(ParseError);
+
+impl fmt::Display for ParseHeightError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.0.display(f, "block height", 0, LOCK_TIME_THRESHOLD - 1)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for ParseHeightError {
+    // To be consistent with `write_err` we need to **not** return source in case of overflow
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
+}
+
+impl From<ParseError> for ParseHeightError {
+    fn from(value: ParseError) -> Self { Self(value) }
+}
+
+/// A UNIX timestamp, seconds since epoch, guaranteed to always contain a valid time value.
+///
+/// Note that there is no manipulation of the inner value during construction or when using
+/// `to_consensus_u32()`. Said another way, `Time(x)` means 'x seconds since epoch' _not_ '(x -
+/// threshold) seconds since epoch'.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
+pub struct Time(u32);
+
+impl Time {
+    /// The minimum absolute block time (Tue Nov 05 1985 00:53:20 GMT+0000).
+    pub const MIN: Self = Time(LOCK_TIME_THRESHOLD);
+
+    /// 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
+    ///
+    /// If `n` does not encode a UNIX time stamp (see documentation on [`LockTime`]).
+    ///
+    /// # Examples
+    /// ```rust
+    /// use bitcoin::locktime::absolute::Time;
+    ///
+    /// let t: u32 = 1653195600; // May 22nd, 5am UTC.
+    /// let time = Time::from_consensus(t).expect("invalid time value");
+    /// assert_eq!(time.to_consensus_u32(), t);
+    /// ```
+    #[inline]
+    pub fn from_consensus(n: u32) -> Result<Time, ConversionError> {
+        if is_block_time(n) {
+            Ok(Self(n))
+        } else {
+            Err(ConversionError::invalid_time(n))
+        }
+    }
+
+    /// Converts this `Time` to its inner `u32` value.
+    ///
+    /// # Examples
+    /// ```rust
+    /// use bitcoin::absolute::LockTime;
+    ///
+    /// let n_lock_time: u32 = 1653195600; // May 22nd, 5am UTC.
+    /// let lock_time = LockTime::from_consensus(n_lock_time);
+    /// assert_eq!(lock_time.to_consensus_u32(), n_lock_time);
+    /// ```
+    #[inline]
+    pub fn to_consensus_u32(self) -> u32 { self.0 }
+}
+
+impl fmt::Display for Time {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
+}
+
+impl_parse_str!(Time, ParseTimeError, parser(Time::from_consensus));
+
+/// Error returned when parsing block time fails.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct ParseTimeError(ParseError);
+
+impl fmt::Display for ParseTimeError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.0.display(f, "block height", LOCK_TIME_THRESHOLD, u32::MAX)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for ParseTimeError {
+    // To be consistent with `write_err` we need to **not** return source in case of overflow
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
+}
+
+impl From<ParseError> for ParseTimeError {
+    fn from(value: ParseError) -> Self { Self(value) }
+}
+
+fn parser<T, E, S, F>(f: F) -> impl FnOnce(S) -> Result<T, E>
+where
+    E: From<ParseError>,
+    S: AsRef<str> + Into<String>,
+    F: FnOnce(u32) -> Result<T, ConversionError>,
+{
+    move |s| {
+        let n = s.as_ref().parse::<i64>().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)
+    }
+}
+
+fn parse_hex<T, E, S, F>(s: S, f: F) -> Result<T, E>
+where
+    E: From<ParseError>,
+    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 = u32::try_from(n).map_err(|_| ParseError::Conversion(n))?;
+    f(n).map_err(ParseError::from).map_err(Into::into)
+}
+
+/// Returns true if `n` is a block height i.e., less than 500,000,000.
+fn is_block_height(n: u32) -> bool { n < LOCK_TIME_THRESHOLD }
+
+/// Returns true if `n` is a UNIX timestamp i.e., greater than or equal to 500,000,000.
+fn is_block_time(n: u32) -> bool { n >= LOCK_TIME_THRESHOLD }
+
+/// Catchall type for errors that relate to time locks.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum Error {
+    /// An error occurred while converting a `u32` to a lock time variant.
+    Conversion(ConversionError),
+    /// An error occurred while operating on lock times.
+    Operation(OperationError),
+    /// An error occurred while parsing a string into an `u32`.
+    Parse(ParseIntError),
+}
+
+internals::impl_from_infallible!(Error);
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use Error::*;
+
+        match *self {
+            Conversion(ref e) => write_err!(f, "error converting lock time value"; e),
+            Operation(ref e) => write_err!(f, "error during lock time operation"; e),
+            Parse(ref e) => write_err!(f, "failed to parse lock time from string"; e),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for Error {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        use Error::*;
+
+        match *self {
+            Conversion(ref e) => Some(e),
+            Operation(ref e) => Some(e),
+            Parse(ref e) => Some(e),
+        }
+    }
+}
+
+impl From<ConversionError> for Error {
+    #[inline]
+    fn from(e: ConversionError) -> Self { Self::Conversion(e) }
+}
+
+impl From<OperationError> for Error {
+    #[inline]
+    fn from(e: OperationError) -> Self { Self::Operation(e) }
+}
+
+impl From<ParseIntError> for Error {
+    #[inline]
+    fn from(e: ParseIntError) -> Self { Self::Parse(e) }
+}
+
+/// An error that occurs when converting a `u32` to a lock time variant.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct ConversionError {
+    /// The expected timelock unit, height (blocks) or time (seconds).
+    unit: LockTimeUnit,
+    /// The invalid input value.
+    input: u32,
+}
+
+impl ConversionError {
+    /// Constructs a `ConversionError` from an invalid `n` when expecting a height value.
+    fn invalid_height(n: u32) -> Self { Self { unit: LockTimeUnit::Blocks, input: n } }
+
+    /// Constructs a `ConversionError` from an invalid `n` when expecting a time value.
+    fn invalid_time(n: u32) -> Self { Self { unit: LockTimeUnit::Seconds, input: n } }
+}
+
+impl fmt::Display for ConversionError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "invalid lock time value {}, {}", self.input, self.unit)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for ConversionError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
+}
+
+/// Describes the two types of locking, lock-by-blockheight and lock-by-blocktime.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
+enum LockTimeUnit {
+    /// Lock by blockheight.
+    Blocks,
+    /// Lock by blocktime.
+    Seconds,
+}
+
+impl fmt::Display for LockTimeUnit {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use LockTimeUnit::*;
+
+        match *self {
+            Blocks => write!(f, "expected lock-by-blockheight (must be < {})", LOCK_TIME_THRESHOLD),
+            Seconds => write!(f, "expected lock-by-blocktime (must be >= {})", LOCK_TIME_THRESHOLD),
+        }
+    }
+}
+
+/// Errors than occur when operating on lock times.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum OperationError {
+    /// Cannot compare different lock time units (height vs time).
+    InvalidComparison,
+}
+
+internals::impl_from_infallible!(OperationError);
+
+impl fmt::Display for OperationError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use OperationError::*;
+
+        match *self {
+            InvalidComparison =>
+                f.write_str("cannot compare different lock units (height vs time)"),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for OperationError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        use OperationError::*;
+
+        match *self {
+            InvalidComparison => None,
+        }
+    }
+}
+
+/// Internal - common representation for height and time.
+#[derive(Debug, Clone, Eq, PartialEq)]
+enum ParseError {
+    InvalidInteger { source: core::num::ParseIntError, input: String },
+    // unit implied by outer type
+    // we use i64 to have nicer messages for negative values
+    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() }
+    }
+
+    fn display(&self, f: &mut fmt::Formatter<'_>, subject: &str, lower_bound: u32, upper_bound: u32) -> fmt::Result {
+        use core::num::IntErrorKind;
+
+        use ParseError::*;
+
+        match self {
+            InvalidInteger { source, input } if *source.kind() == IntErrorKind::PosOverflow => {
+                write!(f, "{} {} is above limit {}", subject, input, upper_bound)
+            }
+            InvalidInteger { source, input } if *source.kind() == IntErrorKind::NegOverflow => {
+                write!(f, "{} {} is below limit {}", subject, input, lower_bound)
+            }
+            InvalidInteger { source, input } => {
+                write_err!(f, "failed to parse {} as {}", input, subject; source)
+            }
+            Conversion(value) if *value < i64::from(lower_bound) => {
+                write!(f, "{} {} is below limit {}", subject, value, lower_bound)
+            }
+            Conversion(value) => {
+                write!(f, "{} {} is above limit {}", subject, value, upper_bound)
+            }
+        }
+    }
+
+    // To be consistent with `write_err` we need to **not** return source in case of overflow
+    #[cfg(feature = "std")]
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        use core::num::IntErrorKind;
+
+        use ParseError::*;
+
+        match self {
+            InvalidInteger { source, .. } if *source.kind() == IntErrorKind::PosOverflow => None,
+            InvalidInteger { source, .. } if *source.kind() == IntErrorKind::NegOverflow => None,
+            InvalidInteger { source, .. } => Some(source),
+            Conversion(_) => None,
+        }
+    }
+}
+
+impl From<ParseIntError> for ParseError {
+    fn from(value: ParseIntError) -> Self {
+        Self::InvalidInteger { source: value.source, input: value.input }
+    }
+}
+
+impl From<ConversionError> for ParseError {
+    fn from(value: ConversionError) -> Self { Self::Conversion(value.input.into()) }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -451,6 +860,46 @@ fn lock_time_from_invalid_hex_should_err() {
         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();
+        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));
+    }
+
+    #[test]
+    fn time_from_str_hex_invalid_hex_should_err() {
+        let hex = "0xzb93";
+        let result = Time::from_hex(hex);
+        assert!(result.is_err());
+    }
+
+    #[test]
+    fn height_from_str_hex_happy_path() {
+        let actual = Height::from_hex("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();
+        assert_eq!(height, Height(0xBA70D));
+    }
+
+    #[test]
+    fn height_from_str_hex_invalid_hex_should_err() {
+        let hex = "0xzb93";
+        let result = Height::from_hex(hex);
+        assert!(result.is_err());
+    }
+
     #[test]
     fn parses_correctly_to_height_or_time() {
         let lock = LockTime::from_consensus(750_000);
diff --git a/bitcoin/src/blockdata/locktime/relative.rs b/bitcoin/src/blockdata/locktime/relative.rs
index 92e55f17..ad3b140b 100644
--- a/bitcoin/src/blockdata/locktime/relative.rs
+++ b/bitcoin/src/blockdata/locktime/relative.rs
@@ -11,13 +11,10 @@
 #[cfg(all(test, mutate))]
 use mutagen::mutate;
 
+use crate::parse::impl_parse_str_from_int_infallible;
 #[cfg(doc)]
 use crate::relative;
 
-#[rustfmt::skip]                // Keep public re-exports separate.
-#[doc(inline)]
-pub use units::locktime::relative::{Height, Time, TimeOverflowError};
-
 /// A relative lock time value, representing either a block height or time (512 second intervals).
 ///
 /// The `relative::LockTime` type does not have any constructors, this is by design, please use
@@ -191,6 +188,105 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     }
 }
 
+/// A relative lock time lock-by-blockheight value.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
+pub struct Height(u16);
+
+impl Height {
+    /// Relative block height 0, can be included in any block.
+    pub const ZERO: Self = Height(0);
+
+    /// The minimum relative block height (0), can be included in any block.
+    pub const MIN: Self = Self::ZERO;
+
+    /// The maximum relative block height.
+    pub const MAX: Self = Height(u16::max_value());
+
+    /// Returns the inner `u16` value.
+    #[inline]
+    pub fn value(self) -> u16 { self.0 }
+}
+
+impl From<u16> for Height {
+    #[inline]
+    fn from(value: u16) -> Self { Height(value) }
+}
+
+impl_parse_str_from_int_infallible!(Height, u16, from);
+
+impl fmt::Display for Height {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
+}
+
+/// A relative lock time lock-by-blocktime value.
+///
+/// For BIP 68 relative lock-by-blocktime locks, time is measure in 512 second intervals.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
+pub struct Time(u16);
+
+impl Time {
+    /// Relative block time 0, can be included in any block.
+    pub const ZERO: Self = Time(0);
+
+    /// The minimum relative block time (0), can be included in any block.
+    pub const MIN: Self = Time::ZERO;
+
+    /// The maximum relative block time (33,554,432 seconds or approx 388 days).
+    pub const MAX: Self = Time(u16::max_value());
+
+    /// Create a [`Time`] using time intervals where each interval is equivalent to 512 seconds.
+    ///
+    /// Encoding finer granularity of time for relative lock-times is not supported in Bitcoin.
+    #[inline]
+    pub fn from_512_second_intervals(intervals: u16) -> Self { Time(intervals) }
+
+    /// Create a [`Time`] from seconds, converting the seconds into 512 second interval with ceiling
+    /// division.
+    ///
+    /// # Errors
+    ///
+    /// Will return an error if the input cannot be encoded in 16 bits.
+    #[inline]
+    pub fn from_seconds_ceil(seconds: u32) -> Result<Self, TimeOverflowError> {
+        if let Ok(interval) = u16::try_from((seconds + 511) / 512) {
+            Ok(Time::from_512_second_intervals(interval))
+        } else {
+            Err(TimeOverflowError { seconds })
+        }
+    }
+
+    /// Returns the inner `u16` value.
+    #[inline]
+    pub fn value(self) -> u16 { self.0 }
+}
+
+impl_parse_str_from_int_infallible!(Time, u16, from_512_second_intervals);
+
+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.
 #[derive(Debug, Clone, PartialEq, Eq)]
 #[non_exhaustive]
diff --git a/bitcoin/src/blockdata/mod.rs b/bitcoin/src/blockdata/mod.rs
index 1d7ee2bb..943de54a 100644
--- a/bitcoin/src/blockdata/mod.rs
+++ b/bitcoin/src/blockdata/mod.rs
@@ -8,10 +8,12 @@
 
 pub mod block;
 pub mod constants;
+pub mod fee_rate;
 pub mod locktime;
 pub mod opcodes;
 pub mod script;
 pub mod transaction;
+pub mod weight;
 pub mod witness;
 
 #[rustfmt::skip]                // Keep public re-exports separate.
@@ -20,37 +22,3 @@
     fee_rate::FeeRate,
     weight::Weight
 };
-
-/// Implements `FeeRate` and assoctiated features.
-pub mod fee_rate {
-    /// Re-export everything from the [`units::fee_rate`] module.
-    pub use units::fee_rate::*;
-
-    #[cfg(test)]
-    mod tests {
-        use super::*;
-
-        #[test]
-        fn fee_convenience_functions_agree() {
-            use hex::test_hex_unwrap as hex;
-
-            use crate::blockdata::transaction::Transaction;
-            use crate::consensus::Decodable;
-
-            const SOME_TX: &str = "0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000";
-
-            let raw_tx = hex!(SOME_TX);
-            let tx: Transaction = Decodable::consensus_decode(&mut raw_tx.as_slice()).unwrap();
-
-            let rate = FeeRate::from_sat_per_vb(1).expect("1 sat/byte is valid");
-
-            assert_eq!(rate.fee_vb(tx.vsize() as u64), rate.fee_wu(tx.weight()));
-        }
-    }
-}
-
-/// Implements `Weight` and associated features.
-pub mod weight {
-    /// Re-export everything from the [`units::weight`] module.
-    pub use units::weight::*;
-}
diff --git a/bitcoin/src/blockdata/script/witness_version.rs b/bitcoin/src/blockdata/script/witness_version.rs
index a51a01d8..29fa83c4 100644
--- a/bitcoin/src/blockdata/script/witness_version.rs
+++ b/bitcoin/src/blockdata/script/witness_version.rs
@@ -12,11 +12,11 @@
 
 use bech32::Fe32;
 use internals::write_err;
-use units::{parse, ParseIntError};
 
 use crate::blockdata::opcodes::all::*;
 use crate::blockdata::opcodes::Opcode;
 use crate::blockdata::script::Instruction;
+use crate::error::ParseIntError;
 
 /// Version of the segregated witness program.
 ///
@@ -87,7 +87,7 @@ impl FromStr for WitnessVersion {
     type Err = FromStrError;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let version: u8 = parse::int(s)?;
+        let version: u8 = crate::parse::int(s)?;
         Ok(WitnessVersion::try_from(version)?)
     }
 }
diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index 58443b44..50c83054 100644
--- a/bitcoin/src/blockdata/transaction.rs
+++ b/bitcoin/src/blockdata/transaction.rs
@@ -16,7 +16,6 @@
 use hashes::{sha256d, Hash};
 use internals::write_err;
 use io::{BufRead, Write};
-use units::parse;
 
 use super::Weight;
 use crate::blockdata::locktime::absolute::{self, Height, Time};
@@ -27,6 +26,7 @@
 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};
 #[cfg(doc)]
 use crate::sighash::{EcdsaSighashType, TapSighashType};
 use crate::prelude::*;
@@ -169,7 +169,7 @@ fn parse_vout(s: &str) -> Result<u32, ParseOutPointError> {
             return Err(ParseOutPointError::VoutNotCanonical);
         }
     }
-    parse::int(s).map_err(ParseOutPointError::Vout)
+    crate::parse::int(s).map_err(ParseOutPointError::Vout)
 }
 
 impl core::str::FromStr for OutPoint {
@@ -449,7 +449,7 @@ pub fn from_seconds_floor(seconds: u32) -> Result<Self, TimeOverflowError> {
         if let Ok(interval) = u16::try_from(seconds / 512) {
             Ok(Sequence::from_512_second_intervals(interval))
         } else {
-            Err(TimeOverflowError::new(seconds))
+            Err(TimeOverflowError { seconds })
         }
     }
 
@@ -462,7 +462,7 @@ pub fn from_seconds_ceil(seconds: u32) -> Result<Self, TimeOverflowError> {
         if let Ok(interval) = u16::try_from((seconds + 511) / 512) {
             Ok(Sequence::from_512_second_intervals(interval))
         } else {
-            Err(TimeOverflowError::new(seconds))
+            Err(TimeOverflowError { seconds })
         }
     }
 
@@ -526,7 +526,7 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     }
 }
 
-units::impl_parse_str_from_int_infallible!(Sequence, u32, from_consensus);
+impl_parse_str_from_int_infallible!(Sequence, u32, from_consensus);
 
 /// Bitcoin transaction output.
 ///
@@ -1702,7 +1702,7 @@ fn outpoint() {
             OutPoint::from_str(
                 "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:lol"
             ),
-            Err(ParseOutPointError::Vout(parse::int::<u32, _>("lol").unwrap_err()))
+            Err(ParseOutPointError::Vout(crate::parse::int::<u32, _>("lol").unwrap_err()))
         );
 
         assert_eq!(
diff --git a/units/src/weight.rs b/bitcoin/src/blockdata/weight.rs
similarity index 96%
rename from units/src/weight.rs
rename to bitcoin/src/blockdata/weight.rs
index eef336ef..ffd4eb65 100644
--- a/units/src/weight.rs
+++ b/bitcoin/src/blockdata/weight.rs
@@ -5,18 +5,13 @@
 use core::fmt;
 use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
 
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
-
-/// The factor that non-witness serialization data is multiplied by during weight calculation.
-pub const WITNESS_SCALE_FACTOR: usize = 4;
-
 /// Represents block weight - the weight of a transaction or block.
 ///
 /// This is an integer newtype representing weigth in `wu`. It provides protection against mixing
 /// up the types as well as basic formatting features.
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
 #[cfg_attr(feature = "serde", serde(transparent))]
 pub struct Weight(u64);
 
@@ -35,7 +30,7 @@ impl Weight {
     pub const MAX: Weight = Weight(u64::MAX);
 
     /// The factor that non-witness serialization data is multiplied by during weight calculation.
-    pub const WITNESS_SCALE_FACTOR: u64 = WITNESS_SCALE_FACTOR as u64;
+    pub const WITNESS_SCALE_FACTOR: u64 = crate::blockdata::constants::WITNESS_SCALE_FACTOR as u64;
 
     /// The maximum allowed weight for a block, see BIP 141 (network rule).
     pub const MAX_BLOCK: Weight = Weight(4_000_000);
@@ -337,4 +332,4 @@ fn sum<I>(iter: I) -> Self
     }
 }
 
-crate::impl_parse_str_from_int_infallible!(Weight, u64, from_wu);
+crate::parse::impl_parse_str_from_int_infallible!(Weight, u64, from_wu);
diff --git a/bitcoin/src/consensus/encode.rs b/bitcoin/src/consensus/encode.rs
index 1b58001f..fc5626b7 100644
--- a/bitcoin/src/consensus/encode.rs
+++ b/bitcoin/src/consensus/encode.rs
@@ -18,7 +18,6 @@
 use core::{fmt, mem, u32};
 
 use hashes::{sha256, sha256d, Hash};
-use hex::error::{InvalidCharError, OddLengthStringError};
 use internals::write_err;
 use io::{Cursor, BufRead, Read, Write};
 
@@ -26,7 +25,6 @@
 use crate::bip158::{FilterHash, FilterHeader};
 use crate::blockdata::block::{self, BlockHash, TxMerkleNode};
 use crate::blockdata::transaction::{Transaction, TxIn, TxOut};
-use crate::consensus::{DecodeError, IterReader};
 #[cfg(feature = "std")]
 use crate::p2p::{
     address::{AddrV2Message, Address},
@@ -103,44 +101,6 @@ impl From<io::Error> for Error {
     fn from(error: io::Error) -> Self { Error::Io(error) }
 }
 
-/// Hex deserialization error.
-#[derive(Debug)]
-pub enum FromHexError {
-    /// Purported hex string had odd length.
-    OddLengthString(OddLengthStringError),
-    /// Decoding error.
-    Decode(DecodeError<InvalidCharError>)
-}
-
-impl fmt::Display for FromHexError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use FromHexError::*;
-
-        match *self {
-            OddLengthString(ref e) =>
-                write_err!(f, "odd length, failed to create bytes from hex"; e),
-            Decode(ref e) => write_err!(f, "decoding error"; e),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for FromHexError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        use FromHexError::*;
-
-        match *self {
-            OddLengthString(ref e) => Some(e),
-            Decode(ref e) => Some(e),
-        }
-    }
-}
-
-impl From<OddLengthStringError> for FromHexError {
-    #[inline]
-    fn from(e: OddLengthStringError) -> Self { Self::OddLengthString(e) }
-}
-
 /// Encodes an object into a vector.
 pub fn serialize<T: Encodable + ?Sized>(data: &T) -> Vec<u8> {
     let mut encoder = Vec::new();
@@ -167,14 +127,6 @@ pub fn deserialize<T: Decodable>(data: &[u8]) -> Result<T, Error> {
     }
 }
 
-/// Deserialize any decodable type from a hex string, will error if said deserialization
-/// doesn't consume the entire vector.
-pub fn deserialize_hex<T: Decodable>(hex: &str) -> Result<T, FromHexError> {
-    let iter = hex::HexSliceToBytesIter::new(hex)?;
-    let reader = IterReader::new(iter);
-    Ok(reader.decode().map_err(FromHexError::Decode)?)
-}
-
 /// Deserializes an object from a vector, but will not report an error if said deserialization
 /// doesn't consume the entire vector.
 pub fn deserialize_partial<T: Decodable>(data: &[u8]) -> Result<(T, usize), Error> {
@@ -1280,19 +1232,4 @@ fn test_read_bytes_from_finite_reader() {
             );
         }
     }
-
-    #[test]
-    fn deserialize_tx_hex() {
-        let hex = include_str!("../../tests/data/previous_tx_0_hex"); // An arbitrary transaction.
-        assert!(deserialize_hex::<Transaction>(hex).is_ok())
-    }
-
-    #[test]
-    fn deserialize_tx_hex_too_many_bytes() {
-        use crate::consensus::DecodeError;
-
-        let mut hex = include_str!("../../tests/data/previous_tx_0_hex").to_string(); // An arbitrary transaction.
-        hex.push_str("abcdef");
-        assert!(matches!(deserialize_hex::<Transaction>(&hex).unwrap_err(), FromHexError::Decode(DecodeError::TooManyBytes)));
-    }
 }
diff --git a/bitcoin/src/consensus/mod.rs b/bitcoin/src/consensus/mod.rs
index 0505ecd5..1155a612 100644
--- a/bitcoin/src/consensus/mod.rs
+++ b/bitcoin/src/consensus/mod.rs
@@ -13,13 +13,6 @@
 #[cfg(feature = "bitcoinconsensus")]
 pub mod validation;
 
-use core::fmt;
-
-use io::{Read, BufRead};
-use internals::write_err;
-
-use crate::consensus;
-
 #[rustfmt::skip]                // Keep public re-exports separate.
 #[doc(inline)]
 pub use self::{
@@ -32,120 +25,3 @@
 pub use self::validation::{
     verify_script, verify_script_with_flags, verify_transaction, verify_transaction_with_flags,
 };
-
-struct IterReader<E: fmt::Debug, I: Iterator<Item = Result<u8, E>>> {
-    iterator: core::iter::Fuse<I>,
-    buf: Option<u8>,
-    error: Option<E>,
-}
-
-impl<E: fmt::Debug, I: Iterator<Item = Result<u8, E>>> IterReader<E, I> {
-    pub(crate) fn new(iterator: I) -> Self { IterReader { iterator: iterator.fuse(), buf: None, error: None } }
-
-    fn decode<T: Decodable>(mut self) -> Result<T, DecodeError<E>> {
-        let result = T::consensus_decode(&mut self);
-        match (result, self.error) {
-            (Ok(_), None) if self.iterator.next().is_some() => Err(DecodeError::TooManyBytes),
-            (Ok(value), None) => Ok(value),
-            (Ok(_), Some(error)) => panic!("{} silently ate the error: {:?}", core::any::type_name::<T>(), error),
-
-            (Err(consensus::encode::Error::Io(io_error)), Some(de_error)) if io_error.kind() == io::ErrorKind::Other && io_error.get_ref().is_none() => Err(DecodeError::Other(de_error)),
-            (Err(consensus_error), None) => Err(DecodeError::Consensus(consensus_error)),
-            (Err(consensus::encode::Error::Io(io_error)), de_error) => panic!("Unexpected IO error {:?} returned from {}::consensus_decode(), deserialization error: {:?}", io_error, core::any::type_name::<T>(), de_error),
-            (Err(consensus_error), Some(de_error)) => panic!("{} should've returned `Other` IO error because of deserialization error {:?} but it returned consensus error {:?} instead", core::any::type_name::<T>(), de_error, consensus_error),
-        }
-    }
-}
-
-impl<E: fmt::Debug, I: Iterator<Item = Result<u8, E>>> Read for IterReader<E, I> {
-    fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
-        let mut count = 0;
-        if buf.is_empty() {
-            return Ok(0);
-        }
-
-        if let Some(first) = self.buf.take() {
-            buf[0] = first;
-            buf = &mut buf[1..];
-            count += 1;
-        }
-        for (dst, src) in buf.iter_mut().zip(&mut self.iterator) {
-            match src {
-                Ok(byte) => *dst = byte,
-                Err(error) => {
-                    self.error = Some(error);
-                    return Err(io::ErrorKind::Other.into());
-                }
-            }
-            // bounded by the length of buf
-            count += 1;
-        }
-        Ok(count)
-    }
-}
-
-impl<E: fmt::Debug, I: Iterator<Item = Result<u8, E>>> BufRead for IterReader<E, I> {
-    fn fill_buf(&mut self) -> Result<&[u8], io::Error> {
-        // matching on reference rather than using `ref` confuses borrow checker
-        if let Some(ref byte) = self.buf {
-            Ok(core::slice::from_ref(byte))
-        } else {
-            match self.iterator.next() {
-                Some(Ok(byte)) => {
-                    self.buf = Some(byte);
-                    Ok(core::slice::from_ref(self.buf.as_ref().expect("we've just filled it")))
-                },
-                Some(Err(error)) => {
-                    self.error = Some(error);
-                    Err(io::ErrorKind::Other.into())
-                },
-                None => Ok(&[]),
-            }
-        }
-    }
-
-    fn consume(&mut self, len: usize) {
-        debug_assert!(len <= 1);
-        if len > 0 {
-            self.buf = None;
-        }
-    }
-}
-
-/// Error when consensus decoding from an `[IterReader]`.
-#[derive(Debug)]
-pub enum DecodeError<E> {
-    /// Attempted to decode an object from an iterator that yielded too many bytes.
-    TooManyBytes,
-    /// Invalid consensus encoding.
-    Consensus(consensus::encode::Error),
-    /// Other decoding error.
-    Other(E),
-}
-
-internals::impl_from_infallible!(DecodeError<E>);
-
-impl<E: fmt::Debug> fmt::Display for DecodeError<E> {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use DecodeError::*;
-
-        match *self {
-            TooManyBytes => write!(f, "attempted to decode object from an iterator that yielded too many bytes"),
-            Consensus(ref e) => write_err!(f, "invalid consensus encoding"; e),
-            Other(ref other) => write!(f, "other decoding error: {:?}", other),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl<E: fmt::Debug> std::error::Error for DecodeError<E> {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        use DecodeError::*;
-
-        match *self {
-            TooManyBytes => None,
-            Consensus(ref e) => Some(e),
-            Other(_) => None, // TODO: Is this correct?
-        }
-    }
-}
diff --git a/bitcoin/src/consensus/serde.rs b/bitcoin/src/consensus/serde.rs
index cef965a9..5a096e6c 100644
--- a/bitcoin/src/consensus/serde.rs
+++ b/bitcoin/src/consensus/serde.rs
@@ -12,12 +12,11 @@
 use core::fmt;
 use core::marker::PhantomData;
 
-use io::Write;
+use io::{BufRead, Read, Write};
 use serde::de::{SeqAccess, Unexpected, Visitor};
 use serde::ser::SerializeSeq;
 use serde::{Deserializer, Serializer};
 
-use crate::consensus::{DecodeError, IterReader};
 use super::encode::Error as ConsensusError;
 use super::{Decodable, Encodable};
 
@@ -353,6 +352,14 @@ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     }
 }
 
+enum DecodeError<E> {
+    TooManyBytes,
+    Consensus(ConsensusError),
+    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 {
@@ -402,6 +409,86 @@ fn into_de_error<DE: serde::de::Error>(self) -> DE {
     }
 }
 
+struct IterReader<E: fmt::Debug, I: Iterator<Item = Result<u8, E>>> {
+    iterator: core::iter::Fuse<I>,
+    buf: Option<u8>,
+    error: Option<E>,
+}
+
+impl<E: fmt::Debug, I: Iterator<Item = Result<u8, E>>> IterReader<E, I> {
+    fn new(iterator: I) -> Self { IterReader { iterator: iterator.fuse(), buf: None, error: None } }
+
+    fn decode<T: Decodable>(mut self) -> Result<T, DecodeError<E>> {
+        let result = T::consensus_decode(&mut self);
+        match (result, self.error) {
+            (Ok(_), None) if self.iterator.next().is_some() => {
+                Err(DecodeError::TooManyBytes)
+            },
+            (Ok(value), None) => Ok(value),
+            (Ok(_), Some(error)) => panic!("{} silently ate the error: {:?}", core::any::type_name::<T>(), error),
+            (Err(ConsensusError::Io(io_error)), Some(de_error)) if io_error.kind() == io::ErrorKind::Other && io_error.get_ref().is_none() => Err(DecodeError::Other(de_error)),
+            (Err(consensus_error), None) => Err(DecodeError::Consensus(consensus_error)),
+            (Err(ConsensusError::Io(io_error)), de_error) => panic!("Unexpected IO error {:?} returned from {}::consensus_decode(), deserialization error: {:?}", io_error, core::any::type_name::<T>(), de_error),
+            (Err(consensus_error), Some(de_error)) => panic!("{} should've returned `Other` IO error because of deserialization error {:?} but it returned consensus error {:?} instead", core::any::type_name::<T>(), de_error, consensus_error),
+        }
+    }
+}
+
+impl<E: fmt::Debug, I: Iterator<Item = Result<u8, E>>> Read for IterReader<E, I> {
+    fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
+        let mut count = 0;
+        if buf.is_empty() {
+            return Ok(0);
+        }
+
+        if let Some(first) = self.buf.take() {
+            buf[0] = first;
+            buf = &mut buf[1..];
+            count += 1;
+        }
+        for (dst, src) in buf.iter_mut().zip(&mut self.iterator) {
+            match src {
+                Ok(byte) => *dst = byte,
+                Err(error) => {
+                    self.error = Some(error);
+                    return Err(io::ErrorKind::Other.into());
+                }
+            }
+            // bounded by the length of buf
+            count += 1;
+        }
+        Ok(count)
+    }
+}
+
+impl<E: fmt::Debug, I: Iterator<Item = Result<u8, E>>> BufRead for IterReader<E, I> {
+    fn fill_buf(&mut self) -> Result<&[u8], io::Error> {
+        // matching on reference rather than using `ref` confuses borrow checker
+        if let Some(ref byte) = self.buf {
+            Ok(core::slice::from_ref(byte))
+        } else {
+            match self.iterator.next() {
+                Some(Ok(byte)) => {
+                    self.buf = Some(byte);
+                    Ok(core::slice::from_ref(self.buf.as_ref().expect("we've just filled it")))
+                },
+                Some(Err(error)) => {
+                    self.error = Some(error);
+                    Err(io::ErrorKind::Other.into())
+                },
+                None => Ok(&[]),
+            }
+        }
+    }
+
+    fn consume(&mut self, len: usize) {
+        debug_assert!(len <= 1);
+        if len > 0 {
+            self.buf = None;
+        }
+    }
+}
+
 /// Helper for `#[serde(with = "")]`.
 ///
 /// To (de)serialize a field using consensus encoding you can write e.g.:
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index a1f03480..e3babd05 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -241,14 +241,14 @@ fn from_str(s: &str) -> Result<PublicKey, ParsePublicKeyError> {
                     InvalidChar(e) => ParsePublicKeyError::InvalidChar(e.invalid_char()),
                     InvalidLength(_) => unreachable!("length checked already")
                 })?;
-                Ok(PublicKey::from_slice(&bytes)?)
+                Ok(PublicKey::from_slice(&bytes).expect("length checked already"))
             },
             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)?)
+                Ok(PublicKey::from_slice(&bytes).expect("length checked already"))
             }
             len => Err(ParsePublicKeyError::InvalidHexLength(len)),
         }
@@ -1394,34 +1394,6 @@ fn public_key_from_str_wrong_length() {
         let res = PublicKey::from_str(s);
         assert!(res.is_err());
         assert_eq!(res.unwrap_err(), ParsePublicKeyError::InvalidHexLength(8));
-    }
-
-    #[test]
-    fn public_key_from_str_invalid_str() {
-        // Ensuring test cases fail when PublicKey::from_str is used on invalid keys
-        let s = "042e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b142";
-        assert_eq!(s.len(), 130);
-        let res = PublicKey::from_str(s);
-        assert!(res.is_err());
-        assert_eq!(res.unwrap_err(), ParsePublicKeyError::Encoding(FromSliceError::Secp256k1(secp256k1::Error::InvalidPublicKey)));
-
-        let s = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd169";
-        assert_eq!(s.len(), 66);
-        let res = PublicKey::from_str(s);
-        assert!(res.is_err());
-        assert_eq!(res.unwrap_err(), ParsePublicKeyError::Encoding(FromSliceError::Secp256k1(secp256k1::Error::InvalidPublicKey)));
-
-        let s = "062e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b133";
-        assert_eq!(s.len(), 130);
-        let res = PublicKey::from_str(s);
-        assert!(res.is_err());
-        assert_eq!(res.unwrap_err(), ParsePublicKeyError::Encoding(FromSliceError::InvalidKeyPrefix(6)));
-
-        let s = "042e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b13g";
-        assert_eq!(s.len(), 130);
-        let res = PublicKey::from_str(s);
-        assert!(res.is_err());
-        assert_eq!(res.unwrap_err(), ParsePublicKeyError::InvalidChar(103));
 
         let s = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1ag";
         assert_eq!(s.len(), 66);
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index 7ae3eb76..70fc1048 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -89,6 +89,7 @@
 #[macro_use]
 mod test_macros;
 mod internal_macros;
+mod parse;
 #[cfg(feature = "serde")]
 mod serde_utils;
 
@@ -196,9 +197,3 @@ fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Er
         }
     }
 }
-
-/// Unit parsing utilities.
-pub mod parse {
-    /// Re-export everything from the [`units::parse`] module.
-    pub use units::parse::ParseIntError;
-}
diff --git a/bitcoin/src/parse.rs b/bitcoin/src/parse.rs
index e69de29b..7e5b86c6 100644
--- a/bitcoin/src/parse.rs
+++ b/bitcoin/src/parse.rs
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: CC0-1.0
+
+use core::fmt;
+use core::str::FromStr;
+
+use internals::write_err;
+
+use crate::prelude::*;
+
+/// Error with rich context returned when a string can't be parsed as an integer.
+///
+/// This is an extension of [`core::num::ParseIntError`], which carries the input that failed to
+/// parse as well as type information. As a result it provides very informative error messages that
+/// make it easier to understand the problem and correct mistakes.
+///
+/// Note that this is larger than the type from `core` so if it's passed through a deep call stack
+/// in a performance-critical application you may want to box it or throw away the context by
+/// converting to `core` type.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct ParseIntError {
+    pub(crate) input: String,
+    // for displaying - see Display impl with nice error message below
+    bits: u8,
+    // We could represent this as a single bit but it wouldn't actually derease the cost of moving
+    // the struct because String contains pointers so there will be padding of bits at least
+    // pointer_size - 1 bytes: min 1B in practice.
+    is_signed: bool,
+    pub(crate) source: core::num::ParseIntError,
+}
+
+impl ParseIntError {
+    /// Returns the input that was attempted to be parsed.
+    pub fn input(&self) -> &str { &self.input }
+}
+
+impl fmt::Display for ParseIntError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let signed = if self.is_signed { "signed" } else { "unsigned" };
+        let n = if self.bits == 8 { "n" } else { "" };
+        write_err!(f, "failed to parse '{}' as a{} {}-bit {} integer", self.input, n, self.bits, signed; self.source)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for ParseIntError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.source) }
+}
+
+impl From<ParseIntError> for core::num::ParseIntError {
+    fn from(value: ParseIntError) -> Self { value.source }
+}
+
+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
+/// passes non-integer to the `parse()` function.
+pub(crate) trait Integer:
+    FromStr<Err = core::num::ParseIntError> + TryFrom<i8> + Sized
+{
+}
+
+macro_rules! impl_integer {
+    ($($type:ty),* $(,)?) => {
+        $(
+        impl Integer for $type {}
+        )*
+    }
+}
+
+impl_integer!(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128);
+
+/// Parses the input string as an integer returning an error carrying rich context.
+///
+/// If the caller owns `String` or `Box<str>` which is not used later it's better to pass it as
+/// owned since it avoids allocation in error case.
+pub(crate) fn int<T: Integer, S: AsRef<str> + Into<String>>(s: S) -> Result<T, ParseIntError> {
+    s.as_ref().parse().map_err(|error| {
+        ParseIntError {
+            input: s.into(),
+            bits: u8::try_from(core::mem::size_of::<T>() * 8).expect("max is 128 bits for u128"),
+            // We detect if the type is signed by checking if -1 can be represented by it
+            // this way we don't have to implement special traits and optimizer will get rid of the
+            // computation.
+            is_signed: T::try_from(-1i8).is_ok(),
+            source: error,
+        }
+    })
+}
+
+/// 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 {
+        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(),
+        source: error,
+    })
+}
+
+/// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using infallible
+/// conversion function `fn`.
+macro_rules! impl_tryfrom_str_from_int_infallible {
+    ($($from:ty, $to:ident, $inner:ident, $fn:ident);*) => {
+        $(
+        impl core::convert::TryFrom<$from> for $to {
+            type Error = $crate::error::ParseIntError;
+
+            fn try_from(s: $from) -> core::result::Result<Self, Self::Error> {
+                $crate::parse::int::<$inner, $from>(s).map($to::$fn)
+            }
+        }
+        )*
+    }
+}
+pub(crate) use impl_tryfrom_str_from_int_infallible;
+
+/// Implements `FromStr` and `TryFrom<{&str, String, Box<str>}> for $to` using `parse::int`, mapping
+/// the output using infallible conversion function `fn`.
+///
+/// The `Error` type is `ParseIntError`
+macro_rules! impl_parse_str_from_int_infallible {
+    ($to:ident, $inner:ident, $fn:ident) => {
+        $crate::parse::impl_tryfrom_str_from_int_infallible!(&str, $to, $inner, $fn; $crate::prelude::String, $to, $inner, $fn; $crate::prelude::Box<str>, $to, $inner, $fn);
+
+        impl core::str::FromStr for $to {
+            type Err = $crate::error::ParseIntError;
+
+            fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
+                $crate::parse::int::<$inner, &str>(s).map($to::$fn)
+            }
+        }
+
+    }
+}
+pub(crate) use impl_parse_str_from_int_infallible;
+
+macro_rules! impl_tryfrom_str {
+    ($($from:ty, $to:ty, $err:ty, $inner_fn:expr);*) => {
+        $(
+            impl core::convert::TryFrom<$from> for $to {
+                type Error = $err;
+
+                fn try_from(s: $from) -> core::result::Result<Self, Self::Error> {
+                    $inner_fn(s)
+                }
+            }
+        )*
+    }
+}
+pub(crate) use impl_tryfrom_str;
+
+/// Implements standard parsing traits for `$type` by calling into `$inner_fn`.
+macro_rules! impl_parse_str {
+    ($to:ty, $err:ty, $inner_fn:expr) => {
+        $crate::parse::impl_tryfrom_str!(&str, $to, $err, $inner_fn; String, $to, $err, $inner_fn; Box<str>, $to, $err, $inner_fn);
+
+        impl core::str::FromStr for $to {
+            type Err = $err;
+
+            fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
+                $inner_fn(s)
+            }
+        }
+    }
+}
+pub(crate) use impl_parse_str;
diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs
index b65b5976..e07009e8 100644
--- a/bitcoin/src/pow.rs
+++ b/bitcoin/src/pow.rs
@@ -12,14 +12,13 @@
 use io::{BufRead, Write};
 #[cfg(all(test, mutate))]
 use mutagen::mutate;
-use units::parse;
 
 use crate::blockdata::block::BlockHash;
 use crate::consensus::encode::{self, Decodable, Encodable};
 #[cfg(doc)]
 use crate::consensus::Params;
 use crate::error::{PrefixedHexError, UnprefixedHexError, ContainsPrefixError, MissingPrefixError};
-use crate::Network;
+use crate::{parse, Network};
 
 /// Implement traits and methods shared by `Target` and `Work`.
 macro_rules! do_impl {
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index cc0e6cf9..4eac052d 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -20,17 +20,15 @@
 
 use hashes::Hash;
 use internals::write_err;
-use secp256k1::{Keypair, Message, Secp256k1, Signing, Verification};
+use secp256k1::{Message, Secp256k1, Signing};
 
 use crate::bip32::{self, KeySource, Xpriv, Xpub};
 use crate::blockdata::transaction::{self, Transaction, TxOut};
-use crate::crypto::{ecdsa, taproot};
+use crate::crypto::ecdsa;
 use crate::crypto::key::{PrivateKey, PublicKey};
 use crate::prelude::*;
-use crate::sighash::{self, EcdsaSighashType, Prevouts, SighashCache};
-use crate::{Amount, FeeRate, TapSighashType};
-use crate::key::TapTweak;
-use crate::TapLeafHash;
+use crate::sighash::{self, EcdsaSighashType, SighashCache};
+use crate::{Amount, FeeRate};
 
 #[rustfmt::skip]                // Keep public re-exports separate.
 #[doc(inline)]
@@ -305,7 +303,7 @@ pub fn sign<C, K>(
         secp: &Secp256k1<C>,
     ) -> Result<SigningKeys, (SigningKeys, SigningErrors)>
     where
-        C: Signing + Verification,
+        C: Signing,
         K: GetKey,
     {
         let tx = self.unsigned_tx.clone(); // clone because we need to mutably borrow when signing.
@@ -315,31 +313,16 @@ pub fn sign<C, K>(
         let mut errors = BTreeMap::new();
 
         for i in 0..self.inputs.len() {
-            match self.signing_algorithm(i) {
-                Ok(SigningAlgorithm::Ecdsa) => {
-                    match self.bip32_sign_ecdsa(k, i, &mut cache, secp) {
-                        Ok(v) => {
-                            used.insert(i, v);
-                        }
-                        Err(e) => {
-                            errors.insert(i, e);
-                        }
+            if let Ok(SigningAlgorithm::Ecdsa) = self.signing_algorithm(i) {
+                match self.bip32_sign_ecdsa(k, i, &mut cache, secp) {
+                    Ok(v) => {
+                        used.insert(i, v);
                     }
-                }
-                Ok(SigningAlgorithm::Schnorr) => {
-                    match self.bip32_sign_schnorr(k, i, &mut cache, secp) {
-                        Ok(v) => {
-                            used.insert(i, v);
-                        }
-                        Err(e) => {
-                            errors.insert(i, e);
-                        }
+                    Err(e) => {
+                        errors.insert(i, e);
                     }
                 }
-                Err(e) => {
-                    errors.insert(i, e);
-                }
-            }
+            };
         }
         if errors.is_empty() {
             Ok(used)
@@ -402,102 +385,6 @@ fn bip32_sign_ecdsa<C, K, T>(
         Ok(used)
     }
 
-    /// Attempts to create all signatures required by this PSBT's `tap_key_origins` field, adding
-    /// them to `tap_key_sig` or `tap_script_sigs`.
-    ///
-    /// # Returns
-    ///
-    /// - Ok: A list of the public keys used in signing.
-    /// - Err: Error encountered trying to calculate the sighash AND we had the signing key. Also panics
-    /// if input_index is out of bounds.
-    fn bip32_sign_schnorr<C, K, T>(
-        &mut self,
-        k: &K,
-        input_index: usize,
-        cache: &mut SighashCache<T>,
-        secp: &Secp256k1<C>,
-    ) -> Result<Vec<PublicKey>, SignError>
-        where
-            C: Signing + Verification,
-            T: Borrow<Transaction>,
-            K: GetKey,
-    {
-
-        let mut input = self.inputs[input_index].clone();
-
-        let mut used = vec![]; // List of pubkeys used to sign the input.
-
-        for (&xonly, (leaf_hashes, key_source)) in input.tap_key_origins.iter() {
-            let sk = if let Ok(Some(secret_key)) = k.get_key(KeyRequest::Bip32(key_source.clone()), secp) {
-                secret_key
-            } else {
-                continue;
-            };
-
-            // Considering the responsibility of the PSBT's finalizer to extract valid signatures,
-            // the goal of this algorithm is to provide signatures to the best of our ability:
-            // 1) If the conditions for key path spend are met, proceed to provide the signature for key path spend
-            // 2) If the conditions for script path spend are met, proceed to provide the signature for script path spend
-
-            // key path spend
-            if let Some(internal_key) = input.tap_internal_key {
-
-                // BIP 371: The internal key does not have leaf hashes, so can be indicated with a hashes len of 0.
-
-                // Based on input.tap_internal_key.is_some() alone, it is not sufficient to determine whether it is a key path spend.
-                // According to BIP 371, we also need to consider the condition leaf_hashes.is_empty() for a more accurate determination.
-                if internal_key == xonly && leaf_hashes.is_empty() && input.tap_key_sig.is_none() {
-                    let (msg, sighash_type) = self.sighash_taproot(input_index, cache, None)?;
-                    let key_pair = Keypair::from_secret_key(secp, &sk.inner)
-                        .tap_tweak(secp, input.tap_merkle_root)
-                        .to_inner();
-
-                    #[cfg(feature = "rand-std")]
-                    let signature = secp.sign_schnorr(&msg, &key_pair);
-                    #[cfg(not(feature = "rand-std"))]
-                    let signature = secp.sign_schnorr_no_aux_rand(&msg, &key_pair);
-
-                    let signature = taproot::Signature { signature, sighash_type };
-                    input.tap_key_sig = Some(signature);
-
-                    used.push(sk.public_key(secp));
-                }
-            }
-
-            // script path spend
-            if let Some((leaf_hashes, _)) = input.tap_key_origins.get(&xonly) {
-                let leaf_hashes = leaf_hashes
-                    .iter()
-                    .filter(|lh| !input.tap_script_sigs.contains_key(&(xonly, **lh)))
-                    .cloned()
-                    .collect::<Vec<_>>();
-
-                if !leaf_hashes.is_empty() {
-
-                    let key_pair = Keypair::from_secret_key(secp, &sk.inner);
-
-                    for lh in leaf_hashes {
-                        let (msg, sighash_type) = self.sighash_taproot(input_index, cache, Some(lh))?;
-
-                        #[cfg(feature = "rand-std")]
-                        let signature = secp.sign_schnorr(&msg, &key_pair);
-                        #[cfg(not(feature = "rand-std"))]
-                        let signature = secp.sign_schnorr_no_aux_rand(&msg, &key_pair);
-
-                        let signature = taproot::Signature { signature, sighash_type };
-                        input.tap_script_sigs.insert((xonly, lh), signature);
-                    }
-
-                    used.push(sk.public_key(secp));
-                }
-            }
-        }
-
-        self.inputs[input_index] = input;
-
-        Ok(used)
-    }
-
     /// Returns the sighash message to sign an ECDSA input along with the sighash type.
     ///
     /// Uses the [`EcdsaSighashType`] from this input if one is specified. If no sighash type is
@@ -559,64 +446,6 @@ pub fn sighash_ecdsa<T: Borrow<Transaction>>(
         }
     }
 
-    /// Returns the sighash message to sign an SCHNORR input along with the sighash type.
-    ///
-    /// Uses the [`TapSighashType`] from this input if one is specified. If no sighash type is
-    /// specified uses [`TapSighashType::Default`].
-    fn sighash_taproot<T: Borrow<Transaction>>(
-        &self,
-        input_index: usize,
-        cache: &mut SighashCache<T>,
-        leaf_hash: Option<TapLeafHash>
-    ) -> Result<(Message, TapSighashType), SignError> {
-        use OutputType::*;
-
-        if self.signing_algorithm(input_index)? != SigningAlgorithm::Schnorr {
-            return Err(SignError::WrongSigningAlgorithm);
-        }
-
-        let input = self.checked_input(input_index)?;
-
-        match self.output_type(input_index)? {
-            Tr => {
-                let hash_ty = input
-                    .sighash_type
-                    .unwrap_or_else(|| TapSighashType::Default.into())
-                    .taproot_hash_ty()
-                    .map_err(|_| SignError::InvalidSighashType)?;
-
-                let spend_utxos = (0..self.inputs.len())
-                    .map(|i|  self.spend_utxo(i).ok())
-                    .collect::<Vec<_>>();
-                let all_spend_utxos;
-
-                let is_anyone_can_pay = PsbtSighashType::from(hash_ty).to_u32() & 0x80 != 0;
-
-                let prev_outs = if is_anyone_can_pay {
-                    Prevouts::One(
-                        input_index,
-                        spend_utxos[input_index].ok_or(SignError::MissingSpendUtxo)?,
-                    )
-                } else if spend_utxos.iter().all(Option::is_some) {
-                    all_spend_utxos = spend_utxos.iter().filter_map(|x| *x).collect::<Vec<_>>();
-                    Prevouts::All(&all_spend_utxos)
-                } else {
-                    return Err(SignError::MissingSpendUtxo);
-                };
-
-                let sighash = if let Some(leaf_hash) = leaf_hash {
-                    cache.taproot_script_spend_signature_hash(input_index, &prev_outs, leaf_hash, hash_ty)?
-                } else {
-                    cache.taproot_key_spend_signature_hash(input_index, &prev_outs, hash_ty)?
-                };
-                Ok((Message::from(sighash), hash_ty))
-            }
-            _ => {
-                Err(SignError::Unsupported)
-            }
-        }
-    }
-
     /// Returns the spending utxo for this PSBT's input at `input_index`.
     pub fn spend_utxo(&self, input_index: usize) -> Result<&TxOut, SignError> {
         let input = self.checked_input(input_index)?;
@@ -947,8 +776,6 @@ pub enum SignError {
     SegwitV0Sighash(transaction::InputsIndexError),
     /// Sighash computation error (p2wpkh input).
     P2wpkhSighash(sighash::P2wpkhError),
-    /// Sighash computation error (taproot input).
-    TaprootError(sighash::TaprootError),
     /// Unable to determine the output type.
     UnknownOutputType,
     /// Unable to find key.
@@ -977,7 +804,6 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
             NotWpkh => write!(f, "the scriptPubkey is not a P2WPKH script"),
             SegwitV0Sighash(ref e) => write_err!(f, "segwit v0 sighash"; e),
             P2wpkhSighash(ref e) => write_err!(f, "p2wpkh sighash"; e),
-            TaprootError(ref e) => write_err!(f, "taproot sighash"; e),
             UnknownOutputType => write!(f, "unable to determine the output type"),
             KeyNotFound => write!(f, "unable to find key"),
             WrongSigningAlgorithm =>
@@ -995,7 +821,6 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         match *self {
             SegwitV0Sighash(ref e) => Some(e),
             P2wpkhSighash(ref e) => Some(e),
-            TaprootError(ref e) => Some(e),
             IndexOutOfBounds(ref e) => Some(e),
             InvalidSighashType
             | MissingInputUtxo
@@ -1021,10 +846,6 @@ impl From<IndexOutOfBoundsError> for SignError {
     fn from(e: IndexOutOfBoundsError) -> Self { SignError::IndexOutOfBounds(e) }
 }
 
-impl From<sighash::TaprootError> for SignError {
-    fn from(e: sighash::TaprootError) -> Self { SignError::TaprootError(e) }
-}
-
 /// This error is returned when extracting a [`Transaction`] from a [`Psbt`].
 #[derive(Debug, Clone, PartialEq, Eq)]
 #[non_exhaustive]
@@ -2280,9 +2101,9 @@ fn sign_psbt() {
         };
         psbt.inputs[1].witness_utxo = Some(txout_unknown_future);
 
-        let (signing_keys, _) = psbt.sign(&key_map, &secp).unwrap_err();
+        let sigs = psbt.sign(&key_map, &secp).unwrap();
 
-        assert_eq!(signing_keys.len(), 1);
-        assert_eq!(signing_keys[&0], vec![pk]);
+        assert!(sigs.len() == 1);
+        assert!(sigs[&0] == vec![pk]);
     }
 }
diff --git a/bitcoin/tests/bip_174.rs b/bitcoin/tests/psbt.rs
similarity index 100%
rename from bitcoin/tests/bip_174.rs
rename to bitcoin/tests/psbt.rs
diff --git a/contrib/run_task.sh b/contrib/run_task.sh
index 42c83135..f7aa21ac 100755
--- a/contrib/run_task.sh
+++ b/contrib/run_task.sh
@@ -212,12 +212,10 @@ do_asan() {
       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="$ASAN_FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
-    # There is currently a bug in the MemorySanitizer (MSAN) - disable the job for now.
-    #
-    # 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="$ASAN_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="$ASAN_FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
 }
 
 # Bench only works with a non-stable toolchain (nightly, beta).
diff --git a/io/CHANGELOG.md b/io/CHANGELOG.md
index 70d22dee..d22f978a 100644
--- a/io/CHANGELOG.md
+++ b/io/CHANGELOG.md
@@ -1,8 +1,3 @@
-# 0.1.2 - 2024-03-14
-
-* Implement `From<core::convert::Infallible>` for Errors [#2516](https://github.com/rust-bitcoin/rust-bitcoin/pull/2516)
-* Fix new CI build warnings [#2488](https://github.com/rust-bitcoin/rust-bitcoin/pull/2488)
-
 # 0.1.1 - Initial Release - 2024-02-18
 
 Create the `io` crate, add basic I/O traits, types, and implementations.
diff --git a/io/Cargo.toml b/io/Cargo.toml
index ce3a71a4..6fe07dc5 100644
--- a/io/Cargo.toml
+++ b/io/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bitcoin-io"
-version = "0.1.2"
+version = "0.1.1"
 authors = ["Matt Corallo <birchneutea@mattcorallo.com>"]
 license = "CC0-1.0"
 repository = "https://github.com/rust-bitcoin/rust-bitcoin"
diff --git a/units/src/amount.rs b/units/src/amount.rs
index e96238ba..963a248f 100644
--- a/units/src/amount.rs
+++ b/units/src/amount.rs
@@ -1064,15 +1064,8 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 // Just using Bitcoin denominated string.
 impl fmt::Display for Amount {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        let satoshis = self.to_sat();
-        let denomination = Denomination::Bitcoin;
-        let mut format_options = FormatOptions::from_formatter(f);
-
-        if f.precision().is_none() && satoshis.rem_euclid(Amount::ONE_BTC.to_sat()) != 0 {
-            format_options.precision = Some(8);
-        }
-
-        fmt_satoshi_in(satoshis, false, f, denomination, true, format_options)
+        self.fmt_value_in(f, Denomination::Bitcoin)?;
+        write!(f, " {}", Denomination::Bitcoin)
     }
 }
 
@@ -2914,35 +2907,4 @@ fn disallow_unknown_denomination() {
             }
         }
     }
-
-    #[test]
-    #[cfg(feature = "alloc")]
-    fn trailing_zeros_for_amount() {
-        assert_eq!(format!("{}", Amount::ONE_SAT), "0.00000001 BTC");
-        assert_eq!(format!("{}", Amount::ONE_BTC), "1 BTC");
-        assert_eq!(format!("{}", Amount::from_sat(1)), "0.00000001 BTC");
-        assert_eq!(format!("{}", Amount::from_sat(10)), "0.00000010 BTC");
-        assert_eq!(format!("{:.2}", Amount::from_sat(10)), "0.0000001 BTC");
-        assert_eq!(format!("{:.2}", Amount::from_sat(100)), "0.000001 BTC");
-        assert_eq!(format!("{:.2}", Amount::from_sat(1000)), "0.00001 BTC");
-        assert_eq!(format!("{:.2}", Amount::from_sat(10_000)), "0.0001 BTC");
-        assert_eq!(format!("{:.2}", Amount::from_sat(100_000)), "0.001 BTC");
-        assert_eq!(format!("{:.2}", Amount::from_sat(1_000_000)), "0.01 BTC");
-        assert_eq!(format!("{:.2}", Amount::from_sat(10_000_000)), "0.10 BTC");
-        assert_eq!(format!("{:.2}", Amount::from_sat(100_000_000)), "1.00 BTC");
-        assert_eq!(format!("{}", Amount::from_sat(100_000_000)), "1 BTC");
-        assert_eq!(format!("{}", Amount::from_sat(40_000_000_000)), "400 BTC");
-        assert_eq!(
-            format!("{:.10}", Amount::from_sat(100_000_000)),
-            "1.0000000000 BTC"
-        );
-        assert_eq!(
-            format!("{}", Amount::from_sat(400_000_000_000_010)),
-            "4000000.00000010 BTC"
-        );
-        assert_eq!(
-            format!("{}", Amount::from_sat(400_000_000_000_000)),
-            "4000000 BTC"
-        );
-    }
 }
diff --git a/units/src/lib.rs b/units/src/lib.rs
index 3b42f1c2..a79f496c 100644
--- a/units/src/lib.rs
+++ b/units/src/lib.rs
@@ -34,28 +34,6 @@
 pub extern crate serde;
 
 pub mod amount;
-#[cfg(feature = "alloc")]
-pub mod locktime;
-#[cfg(feature = "alloc")]
-pub mod fee_rate;
-#[cfg(feature = "alloc")]
-pub mod parse;
-#[cfg(feature = "alloc")]
-pub mod weight;
 
 #[doc(inline)]
-pub use self::{
-    amount::{Amount, ParseAmountError, SignedAmount},
-};
-#[cfg(feature = "alloc")]
-pub use self::parse::ParseIntError;
-
-#[rustfmt::skip]
-#[allow(unused_imports)]
-mod prelude {
-    #[cfg(all(feature = "alloc", not(feature = "std")))]
-    pub use alloc::{string::{String, ToString}, vec::Vec, boxed::Box, borrow::{Borrow, BorrowMut, Cow, ToOwned}, slice, rc};
-
-    #[cfg(feature = "std")]
-    pub use std::{string::{String, ToString}, vec::Vec, boxed::Box, borrow::{Borrow, BorrowMut, Cow, ToOwned}, rc};
-}
+pub use self::amount::{Amount, ParseAmountError, SignedAmount};
diff --git a/units/src/locktime/absolute.rs b/units/src/locktime/absolute.rs
deleted file mode 100644
index 0930f3cd..00000000
--- a/units/src/locktime/absolute.rs
+++ /dev/null
@@ -1,367 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Provides type `Height` and `Time` types used by the `rust-bitcoin` `absolute::LockTime` type.
-
-use core::fmt;
-
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
-
-use internals::write_err;
-
-#[cfg(feature = "alloc")]
-use crate::prelude::*;
-use crate::parse::{self, ParseIntError};
-
-/// The Threshold for deciding whether a lock time value is a height or a time (see [Bitcoin Core]).
-///
-/// `LockTime` values _below_ the threshold are interpreted as block heights, values _above_ (or
-/// equal to) the threshold are interpreted as block times (UNIX timestamp, seconds since epoch).
-///
-/// Bitcoin is able to safely use this value because a block height greater than 500,000,000 would
-/// never occur because it would represent a height in approximately 9500 years. Conversely, block
-/// times under 500,000,000 will never happen because they would represent times before 1986 which
-/// are, for obvious reasons, not useful within the Bitcoin network.
-///
-/// [Bitcoin Core]: https://github.com/bitcoin/bitcoin/blob/9ccaee1d5e2e4b79b0a7c29aadb41b97e4741332/src/script/script.h#L39
-pub const LOCK_TIME_THRESHOLD: u32 = 500_000_000;
-
-/// An absolute block height, guaranteed to always contain a valid height value.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Height(u32);
-
-impl Height {
-    /// Absolute block height 0, the genesis block.
-    pub const ZERO: Self = Height(0);
-
-    /// The minimum absolute block height (0), the genesis block.
-    pub const MIN: Self = Self::ZERO;
-
-    /// 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
-    ///
-    /// If `n` does not represent a valid block height value.
-    ///
-    /// # Examples
-    /// ```rust
-    /// use bitcoin_units::locktime::absolute::Height;
-    ///
-    /// let h: u32 = 741521;
-    /// let height = Height::from_consensus(h).expect("invalid height value");
-    /// assert_eq!(height.to_consensus_u32(), h);
-    /// ```
-    #[inline]
-    pub fn from_consensus(n: u32) -> Result<Height, ConversionError> {
-        if is_block_height(n) {
-            Ok(Self(n))
-        } else {
-            Err(ConversionError::invalid_height(n))
-        }
-    }
-
-    /// Converts this `Height` to its inner `u32` value.
-    #[inline]
-    pub fn to_consensus_u32(self) -> u32 { self.0 }
-}
-
-impl fmt::Display for Height {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
-}
-
-crate::impl_parse_str!(Height, ParseHeightError, parser(Height::from_consensus));
-
-/// Error returned when parsing block height fails.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct ParseHeightError(ParseError);
-
-impl fmt::Display for ParseHeightError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        self.0.display(f, "block height", 0, LOCK_TIME_THRESHOLD - 1)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for ParseHeightError {
-    // To be consistent with `write_err` we need to **not** return source in case of overflow
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
-}
-
-impl From<ParseError> for ParseHeightError {
-    fn from(value: ParseError) -> Self { Self(value) }
-}
-
-/// A UNIX timestamp, seconds since epoch, guaranteed to always contain a valid time value.
-///
-/// Note that there is no manipulation of the inner value during construction or when using
-/// `to_consensus_u32()`. Said another way, `Time(x)` means 'x seconds since epoch' _not_ '(x -
-/// threshold) seconds since epoch'.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Time(u32);
-
-impl Time {
-    /// The minimum absolute block time (Tue Nov 05 1985 00:53:20 GMT+0000).
-    pub const MIN: Self = Time(LOCK_TIME_THRESHOLD);
-
-    /// 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
-    ///
-    /// If `n` does not encode a valid UNIX time stamp.
-    ///
-    /// # Examples
-    /// ```rust
-    /// use bitcoin_units::locktime::absolute::Time;
-    ///
-    /// let t: u32 = 1653195600; // May 22nd, 5am UTC.
-    /// let time = Time::from_consensus(t).expect("invalid time value");
-    /// assert_eq!(time.to_consensus_u32(), t);
-    /// ```
-    #[inline]
-    pub fn from_consensus(n: u32) -> Result<Time, ConversionError> {
-        if is_block_time(n) {
-            Ok(Self(n))
-        } else {
-            Err(ConversionError::invalid_time(n))
-        }
-    }
-
-    /// Converts this `Time` to its inner `u32` value.
-    #[inline]
-    pub fn to_consensus_u32(self) -> u32 { self.0 }
-}
-
-impl fmt::Display for Time {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
-}
-
-crate::impl_parse_str!(Time, ParseTimeError, parser(Time::from_consensus));
-
-/// Error returned when parsing block time fails.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct ParseTimeError(ParseError);
-
-impl fmt::Display for ParseTimeError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        self.0.display(f, "block height", LOCK_TIME_THRESHOLD, u32::MAX)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for ParseTimeError {
-    // To be consistent with `write_err` we need to **not** return source in case of overflow
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
-}
-
-impl From<ParseError> for ParseTimeError {
-    fn from(value: ParseError) -> Self { Self(value) }
-}
-
-fn parser<T, E, S, F>(f: F) -> impl FnOnce(S) -> Result<T, E>
-where
-    E: From<ParseError>,
-    S: AsRef<str> + Into<String>,
-    F: FnOnce(u32) -> Result<T, ConversionError>,
-{
-    move |s| {
-        let n = s.as_ref().parse::<i64>().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)
-    }
-}
-
-fn parse_hex<T, E, S, F>(s: S, f: F) -> Result<T, E>
-where
-    E: From<ParseError>,
-    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 = u32::try_from(n).map_err(|_| ParseError::Conversion(n))?;
-    f(n).map_err(ParseError::from).map_err(Into::into)
-}
-
-/// Returns true if `n` is a block height i.e., less than 500,000,000.
-pub fn is_block_height(n: u32) -> bool { n < LOCK_TIME_THRESHOLD }
-
-/// Returns true if `n` is a UNIX timestamp i.e., greater than or equal to 500,000,000.
-pub fn is_block_time(n: u32) -> bool { n >= LOCK_TIME_THRESHOLD }
-
-/// An error that occurs when converting a `u32` to a lock time variant.
-#[derive(Debug, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub struct ConversionError {
-    /// The expected timelock unit, height (blocks) or time (seconds).
-    unit: LockTimeUnit,
-    /// The invalid input value.
-    input: u32,
-}
-
-impl ConversionError {
-    /// Constructs a `ConversionError` from an invalid `n` when expecting a height value.
-    fn invalid_height(n: u32) -> Self { Self { unit: LockTimeUnit::Blocks, input: n } }
-
-    /// Constructs a `ConversionError` from an invalid `n` when expecting a time value.
-    fn invalid_time(n: u32) -> Self { Self { unit: LockTimeUnit::Seconds, input: n } }
-}
-
-impl fmt::Display for ConversionError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "invalid lock time value {}, {}", self.input, self.unit)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for ConversionError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
-}
-
-/// Describes the two types of locking, lock-by-blockheight and lock-by-blocktime.
-#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
-enum LockTimeUnit {
-    /// Lock by blockheight.
-    Blocks,
-    /// Lock by blocktime.
-    Seconds,
-}
-
-impl fmt::Display for LockTimeUnit {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use LockTimeUnit::*;
-
-        match *self {
-            Blocks => write!(f, "expected lock-by-blockheight (must be < {})", LOCK_TIME_THRESHOLD),
-            Seconds => write!(f, "expected lock-by-blocktime (must be >= {})", LOCK_TIME_THRESHOLD),
-        }
-    }
-}
-
-/// Internal - common representation for height and time.
-#[derive(Debug, Clone, Eq, PartialEq)]
-enum ParseError {
-    InvalidInteger { source: core::num::ParseIntError, input: String },
-    // unit implied by outer type
-    // we use i64 to have nicer messages for negative values
-    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() }
-    }
-
-    fn display(&self, f: &mut fmt::Formatter<'_>, subject: &str, lower_bound: u32, upper_bound: u32) -> fmt::Result {
-        use core::num::IntErrorKind;
-
-        use ParseError::*;
-
-        match self {
-            InvalidInteger { source, input } if *source.kind() == IntErrorKind::PosOverflow => {
-                write!(f, "{} {} is above limit {}", subject, input, upper_bound)
-            }
-            InvalidInteger { source, input } if *source.kind() == IntErrorKind::NegOverflow => {
-                write!(f, "{} {} is below limit {}", subject, input, lower_bound)
-            }
-            InvalidInteger { source, input } => {
-                write_err!(f, "failed to parse {} as {}", input, subject; source)
-            }
-            Conversion(value) if *value < i64::from(lower_bound) => {
-                write!(f, "{} {} is below limit {}", subject, value, lower_bound)
-            }
-            Conversion(value) => {
-                write!(f, "{} {} is above limit {}", subject, value, upper_bound)
-            }
-        }
-    }
-
-    // To be consistent with `write_err` we need to **not** return source in case of overflow
-    #[cfg(feature = "std")]
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        use core::num::IntErrorKind;
-
-        use ParseError::*;
-
-        match self {
-            InvalidInteger { source, .. } if *source.kind() == IntErrorKind::PosOverflow => None,
-            InvalidInteger { source, .. } if *source.kind() == IntErrorKind::NegOverflow => None,
-            InvalidInteger { source, .. } => Some(source),
-            Conversion(_) => None,
-        }
-    }
-}
-
-impl From<ParseIntError> for ParseError {
-    fn from(value: ParseIntError) -> Self {
-        Self::InvalidInteger { source: value.source, input: value.input }
-    }
-}
-
-impl From<ConversionError> for ParseError {
-    fn from(value: ConversionError) -> Self { Self::Conversion(value.input.into()) }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn time_from_str_hex_happy_path() {
-        let actual = Time::from_hex("0x6289C350").unwrap();
-        let expected = Time::from_consensus(0x6289C350).unwrap();
-        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));
-    }
-
-    #[test]
-    fn time_from_str_hex_invalid_hex_should_err() {
-        let hex = "0xzb93";
-        let result = Time::from_hex(hex);
-        assert!(result.is_err());
-    }
-
-    #[test]
-    fn height_from_str_hex_happy_path() {
-        let actual = Height::from_hex("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();
-        assert_eq!(height, Height(0xBA70D));
-    }
-
-    #[test]
-    fn height_from_str_hex_invalid_hex_should_err() {
-        let hex = "0xzb93";
-        let result = Height::from_hex(hex);
-        assert!(result.is_err());
-    }
-
-}
diff --git a/units/src/locktime/mod.rs b/units/src/locktime/mod.rs
deleted file mode 100644
index 4cf62ad1..00000000
--- a/units/src/locktime/mod.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Provides absolute and relative locktimes.
-
-pub mod absolute;
-pub mod relative;
diff --git a/units/src/locktime/relative.rs b/units/src/locktime/relative.rs
deleted file mode 100644
index a44a7486..00000000
--- a/units/src/locktime/relative.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Provides type `Height` and `Time` types used by the `rust-bitcoin` `relative::LockTime` type.
-
-use core::fmt;
-
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
-
-/// A relative lock time lock-by-blockheight value.
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Height(u16);
-
-impl Height {
-    /// Relative block height 0, can be included in any block.
-    pub const ZERO: Self = Height(0);
-
-    /// The minimum relative block height (0), can be included in any block.
-    pub const MIN: Self = Self::ZERO;
-
-    /// The maximum relative block height.
-    pub const MAX: Self = Height(u16::max_value());
-
-    /// Returns the inner `u16` value.
-    #[inline]
-    pub fn value(self) -> u16 { self.0 }
-}
-
-impl From<u16> for Height {
-    #[inline]
-    fn from(value: u16) -> Self { Height(value) }
-}
-
-crate::impl_parse_str_from_int_infallible!(Height, u16, from);
-
-impl fmt::Display for Height {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
-}
-
-/// A relative lock time lock-by-blocktime value.
-///
-/// For BIP 68 relative lock-by-blocktime locks, time is measure in 512 second intervals.
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Time(u16);
-
-impl Time {
-    /// Relative block time 0, can be included in any block.
-    pub const ZERO: Self = Time(0);
-
-    /// The minimum relative block time (0), can be included in any block.
-    pub const MIN: Self = Time::ZERO;
-
-    /// The maximum relative block time (33,554,432 seconds or approx 388 days).
-    pub const MAX: Self = Time(u16::max_value());
-
-    /// Create a [`Time`] using time intervals where each interval is equivalent to 512 seconds.
-    ///
-    /// Encoding finer granularity of time for relative lock-times is not supported in Bitcoin.
-    #[inline]
-    pub fn from_512_second_intervals(intervals: u16) -> Self { Time(intervals) }
-
-    /// Create a [`Time`] from seconds, converting the seconds into 512 second interval with ceiling
-    /// division.
-    ///
-    /// # Errors
-    ///
-    /// Will return an error if the input cannot be encoded in 16 bits.
-    #[inline]
-    pub fn from_seconds_ceil(seconds: u32) -> Result<Self, TimeOverflowError> {
-        if let Ok(interval) = u16::try_from((seconds + 511) / 512) {
-            Ok(Time::from_512_second_intervals(interval))
-        } else {
-            Err(TimeOverflowError { seconds })
-        }
-    }
-
-    /// Returns the inner `u16` value.
-    #[inline]
-    pub fn value(self) -> u16 { self.0 }
-}
-
-crate::impl_parse_str_from_int_infallible!(Time, u16, from_512_second_intervals);
-
-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 TimeOverflowError {
-    /// Creates a new `TimeOverflowError` using `seconds`.
-    ///
-    /// # Panics
-    ///
-    /// If `seconds` would not actually overflow a `u16`.
-    pub fn new(seconds: u32) -> Self {
-        assert!(u16::try_from((seconds + 511) / 512).is_err());
-        Self { seconds }
-    }
-}
-
-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 {}
diff --git a/units/src/parse.rs b/units/src/parse.rs
deleted file mode 100644
index ba0b4b2c..00000000
--- a/units/src/parse.rs
+++ /dev/null
@@ -1,187 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Parsing utilities.
-
-use core::fmt;
-use core::str::FromStr;
-use alloc::string::String;
-
-use internals::write_err;
-
-/// Error with rich context returned when a string can't be parsed as an integer.
-///
-/// This is an extension of [`core::num::ParseIntError`], which carries the input that failed to
-/// parse as well as type information. As a result it provides very informative error messages that
-/// make it easier to understand the problem and correct mistakes.
-///
-/// Note that this is larger than the type from `core` so if it's passed through a deep call stack
-/// in a performance-critical application you may want to box it or throw away the context by
-/// converting to `core` type.
-#[derive(Debug, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub struct ParseIntError {
-    pub(crate) input: String,
-    // for displaying - see Display impl with nice error message below
-    bits: u8,
-    // We could represent this as a single bit but it wouldn't actually derease the cost of moving
-    // the struct because String contains pointers so there will be padding of bits at least
-    // pointer_size - 1 bytes: min 1B in practice.
-    is_signed: bool,
-    pub(crate) source: core::num::ParseIntError,
-}
-
-impl ParseIntError {
-    /// Returns the input that was attempted to be parsed.
-    pub fn input(&self) -> &str { &self.input }
-}
-
-impl fmt::Display for ParseIntError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        let signed = if self.is_signed { "signed" } else { "unsigned" };
-        let n = if self.bits == 8 { "n" } else { "" };
-        write_err!(f, "failed to parse '{}' as a{} {}-bit {} integer", self.input, n, self.bits, signed; self.source)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for ParseIntError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.source) }
-}
-
-impl From<ParseIntError> for core::num::ParseIntError {
-    fn from(value: ParseIntError) -> Self { value.source }
-}
-
-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
-/// passes non-integer to the `parse()` function.
-pub trait Integer:
-    FromStr<Err = core::num::ParseIntError> + TryFrom<i8> + Sized
-{
-}
-
-macro_rules! impl_integer {
-    ($($type:ty),* $(,)?) => {
-        $(
-        impl Integer for $type {}
-        )*
-    }
-}
-
-impl_integer!(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128);
-
-/// Parses the input string as an integer returning an error carrying rich context.
-///
-/// If the caller owns `String` or `Box<str>` which is not used later it's better to pass it as
-/// owned since it avoids allocation in error case.
-pub fn int<T: Integer, S: AsRef<str> + Into<String>>(s: S) -> Result<T, ParseIntError> {
-    s.as_ref().parse().map_err(|error| {
-        ParseIntError {
-            input: s.into(),
-            bits: u8::try_from(core::mem::size_of::<T>() * 8).expect("max is 128 bits for u128"),
-            // We detect if the type is signed by checking if -1 can be represented by it
-            // this way we don't have to implement special traits and optimizer will get rid of the
-            // computation.
-            is_signed: T::try_from(-1i8).is_ok(),
-            source: error,
-        }
-    })
-}
-
-/// 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
-    }
-}
-
-/// Parses a u32 from a hex string.
-///
-/// Input string may or may not contain a `0x` prefix.
-pub 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 {
-        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(),
-        source: error,
-    })
-}
-
-/// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using infallible
-/// conversion function `fn`.
-#[macro_export]
-macro_rules! impl_tryfrom_str_from_int_infallible {
-    ($($from:ty, $to:ident, $inner:ident, $fn:ident);*) => {
-        $(
-        impl core::convert::TryFrom<$from> for $to {
-            type Error = $crate::parse::ParseIntError;
-
-            fn try_from(s: $from) -> core::result::Result<Self, Self::Error> {
-                $crate::parse::int::<$inner, $from>(s).map($to::$fn)
-            }
-        }
-        )*
-    }
-}
-
-/// Implements `FromStr` and `TryFrom<{&str, String, Box<str>}> for $to` using `parse::int`, mapping
-/// the output using infallible conversion function `fn`.
-///
-/// The `Error` type is `ParseIntError`
-#[macro_export]
-macro_rules! impl_parse_str_from_int_infallible {
-    ($to:ident, $inner:ident, $fn:ident) => {
-        #[cfg(all(feature = "alloc", not(feature = "std")))]
-        $crate::impl_tryfrom_str_from_int_infallible!(&str, $to, $inner, $fn; alloc::string::String, $to, $inner, $fn; alloc::boxed::Box<str>, $to, $inner, $fn);
-        #[cfg(feature = "std")]
-        $crate::impl_tryfrom_str_from_int_infallible!(&str, $to, $inner, $fn; std::string::String, $to, $inner, $fn; std::boxed::Box<str>, $to, $inner, $fn);
-
-        impl core::str::FromStr for $to {
-            type Err = $crate::parse::ParseIntError;
-
-            fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
-                $crate::parse::int::<$inner, &str>(s).map($to::$fn)
-            }
-        }
-
-    }
-}
-
-/// Implements `TryFrom<$from> for $to`.
-#[macro_export]
-macro_rules! impl_tryfrom_str {
-    ($($from:ty, $to:ty, $err:ty, $inner_fn:expr);*) => {
-        $(
-            impl core::convert::TryFrom<$from> for $to {
-                type Error = $err;
-
-                fn try_from(s: $from) -> core::result::Result<Self, Self::Error> {
-                    $inner_fn(s)
-                }
-            }
-        )*
-    }
-}
-
-/// Implements standard parsing traits for `$type` by calling into `$inner_fn`.
-#[macro_export]
-macro_rules! impl_parse_str {
-    ($to:ty, $err:ty, $inner_fn:expr) => {
-        $crate::impl_tryfrom_str!(&str, $to, $err, $inner_fn; String, $to, $err, $inner_fn; Box<str>, $to, $err, $inner_fn);
-
-        impl core::str::FromStr for $to {
-            type Err = $err;
-
-            fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
-                $inner_fn(s)
-            }
-        }
-    }
-}


Test patch:
diff --git a/bitcoin/tests/psbt-sign-taproot.rs b/bitcoin/tests/psbt-sign-taproot.rs
deleted file mode 100644
index e1aad558..00000000
--- a/bitcoin/tests/psbt-sign-taproot.rs
+++ /dev/null
@@ -1,316 +0,0 @@
-#![cfg(not(feature = "rand-std"))]
-
-use std::collections::BTreeMap;
-use std::str::FromStr;
-use secp256k1::{Keypair, Signing, Secp256k1, XOnlyPublicKey};
-use bitcoin::{absolute, Address, Network, OutPoint, PrivateKey, Psbt, script, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness};
-use bitcoin::bip32::{DerivationPath, Fingerprint};
-use bitcoin::consensus::encode::serialize_hex;
-use bitcoin::opcodes::all::OP_CHECKSIG;
-use bitcoin::psbt::{GetKey, Input, KeyRequest, PsbtSighashType, SignError};
-use bitcoin::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo};
-use bitcoin::transaction::Version;
-use units::Amount;
-
-
-#[test]
-fn psbt_sign_taproot() {
-
-    struct Keystore {
-        sk: PrivateKey,
-        mfp: Fingerprint,
-    }
-
-    impl GetKey for Keystore {
-        type Error = SignError;
-        fn get_key<C: Signing>(&self, key_request: KeyRequest, _secp: &Secp256k1<C>) -> Result<Option<PrivateKey>, Self::Error> {
-            match key_request {
-                KeyRequest::Bip32((mfp, _)) => {
-                    if mfp == self.mfp {
-                        Ok(Some(self.sk))
-                    } else {
-                        Err(SignError::KeyNotFound)
-                    }
-                }
-                _ => Err(SignError::KeyNotFound)
-            }
-        }
-    }
-
-    let secp = &Secp256k1::new();
-
-    let sk_path = [
-        ("dff1c8c2c016a572914b4c5adb8791d62b4768ae9d0a61be8ab94cf5038d7d90", "86'/1'/0'/0/0"),
-        ("1ede31b0e7e47c2afc65ffd158b1b1b9d3b752bba8fd117dc8b9e944a390e8d9", "86'/1'/0'/0/1"),
-        ("1fb777f1a6fb9b76724551f8bc8ad91b77f33b8c456d65d746035391d724922a", "86'/1'/0'/0/2"),
-    ];
-    let mfp = "73c5da0a";
-
-    //
-    // Step 0: Create P2TR address.
-    //
-
-    // Create three basic scripts to test script path spend.
-    let script1 = create_basic_single_sig_script(secp, sk_path[0].0); // m/86'/1'/0'/0/0
-    let script2 = create_basic_single_sig_script(secp, sk_path[1].0); // m/86'/1'/0'/0/1
-    let script3 = create_basic_single_sig_script(secp, sk_path[2].0); // m/86'/1'/0'/0/2
-
-    // Just use one of the secret keys for the key path spend.
-    let kp = Keypair::from_seckey_str(secp, &sk_path[2].0).expect("failed to create keypair");
-
-    let internal_key = kp.x_only_public_key().0; // Ignore the parity.
-
-    let tree = create_taproot_tree(secp, script1.clone(), script2.clone(), script3.clone(), internal_key);
-
-    let address = create_p2tr_address(tree.clone());
-    assert_eq!("tb1pytee2mxz0f4fkrsqqws2lsgnkp8nrw2atjkjy2n9gahggsphr0gszaxxmv", address.to_string());
-
-    // m/86'/1'/0'/0/7
-    let to_address = "tb1pyfv094rr0vk28lf8v9yx3veaacdzg26ztqk4ga84zucqqhafnn5q9my9rz";
-    let to_address = Address::from_str(to_address).unwrap().assume_checked();
-
-    // key path spend
-    {
-        //
-        // Step 1: create psbt for key path spend.
-        //
-        let mut psbt_key_path_spend = create_psbt_for_taproot_key_path_spend(address.clone(), to_address.clone(), tree.clone());
-
-        //
-        // Step 2: sign psbt.
-        //
-        let keystore = Keystore {
-            mfp: Fingerprint::from_str(mfp).unwrap(),
-            sk: PrivateKey::new(kp.secret_key(), Network::Testnet),
-        };
-        let _ = psbt_key_path_spend.sign(&keystore, secp);
-
-        let sig = "92864dc9e56b6260ecbd54ec16b94bb597a2e6be7cca0de89d75e17921e0e1528cba32dd04217175c237e1835b5db1c8b384401718514f9443dce933c6ba9c87";
-        assert_eq!(sig, psbt_key_path_spend.inputs[0].tap_key_sig.unwrap().signature.to_string());
-
-        //
-        // Step 3: finalize psbt.
-        //
-        let final_psbt = finalize_psbt_for_key_path_spend(psbt_key_path_spend);
-        let tx = final_psbt.extract_tx().unwrap();
-
-        let tx_id = "5306516f2032d9f34c9f2f6d2b1b8ad2486ef1ba196d8d8d780e59773e48ad6d";
-        assert_eq!(tx_id, tx.compute_txid().to_string());
-
-        let tx_bytes = "020000000001013aee4d6b51da574900e56d173041115bd1e1d01d4697a845784cf716a10c98060000000000ffffffff0100190000000000002251202258f2d4637b2ca3fd27614868b33dee1a242b42582d5474f51730005fa99ce8014092864dc9e56b6260ecbd54ec16b94bb597a2e6be7cca0de89d75e17921e0e1528cba32dd04217175c237e1835b5db1c8b384401718514f9443dce933c6ba9c8700000000";
-        let tx_hex = serialize_hex(&tx);
-        assert_eq!(tx_bytes, tx_hex);
-    }
-
-    // script path spend
-    {
-        // use private key of path "m/86'/1'/0'/0/1" as signing key
-        let kp = Keypair::from_seckey_str(secp, &sk_path[1].0).expect("failed to create keypair");
-        let x_only_pubkey = kp.x_only_public_key().0;
-        let signing_key_path = sk_path[1].1;
-
-        let keystore = Keystore {
-            mfp: Fingerprint::from_str(mfp).unwrap(),
-            sk: PrivateKey::new(kp.secret_key(), Network::Testnet),
-        };
-
-        //
-        // Step 1: create psbt for script path spend.
-        //
-        let mut psbt_script_path_spend = create_psbt_for_taproot_script_path_spend(address.clone(), to_address.clone(), tree.clone(), x_only_pubkey, signing_key_path, script2.clone());
-
-        //
-        // Step 2: sign psbt.
-        //
-        let _ = psbt_script_path_spend.sign(&keystore, secp);
-
-        let sig = "9c1466e1631a58c55fcb8642ce5f7896314f4b565d92c5c80b17aa9abf56d22e0b5e5dcbcfe836bbd7d409491f58aa9e1f68a491ef8f05eef62fb50ffac85727";
-        assert_eq!(sig, psbt_script_path_spend.inputs[0].tap_script_sigs.get(&(x_only_pubkey, script2.clone().tapscript_leaf_hash())).unwrap().signature.to_string());
-
-        //
-        // Step 3: finalize psbt.
-        //
-        let final_psbt = finalize_psbt_for_script_path_spend(psbt_script_path_spend);
-        let tx = final_psbt.extract_tx().unwrap();
-
-        let tx_id = "a51f723beffc810248809355ba9c9e4b39c6e55c08429f0aeaa79b73f18bc2a6";
-        assert_eq!(tx_id, tx.compute_txid().to_string());
-
-        let tx_hex = serialize_hex(&tx);
-        let tx_bytes = "0200000000010176a3c94a6b21d742e8ca192130ad10fdfc4c83510cb6baba8572a5fc70677c9d0000000000ffffffff0170170000000000002251202258f2d4637b2ca3fd27614868b33dee1a242b42582d5474f51730005fa99ce803419c1466e1631a58c55fcb8642ce5f7896314f4b565d92c5c80b17aa9abf56d22e0b5e5dcbcfe836bbd7d409491f58aa9e1f68a491ef8f05eef62fb50ffac857270122203058679f6d60b87ef921d98a2a9a1f1e0779dae27bedbd1cdb2f147a07835ac9ac61c1b68df382cad577d8304d5a8e640c3cb42d77c10016ab754caa4d6e68b6cb296d9b9d92a717ebeba858f75182936f0da5a7aecc434b0eebb2dc8a6af5409422ccf87f124e735a592a8ff390a68f6f05469ba8422e246dc78b0b57cd1576ffa98c00000000";
-        assert_eq!(tx_bytes, tx_hex);
-    }
-}
-
-fn create_basic_single_sig_script(secp: &Secp256k1::<secp256k1::All>, sk: &str) -> ScriptBuf {
-    let kp = Keypair::from_seckey_str(secp, sk).expect("failed to create keypair");
-    let x_only_pubkey = kp.x_only_public_key().0;
-    script::Builder::new()
-        .push_slice(x_only_pubkey.serialize())
-        .push_opcode(OP_CHECKSIG)
-        .into_script()
-}
-
-fn create_taproot_tree(secp: &Secp256k1::<secp256k1::All>, script1: ScriptBuf, script2: ScriptBuf, script3: ScriptBuf, internal_key: XOnlyPublicKey) -> TaprootSpendInfo {
-    let builder = TaprootBuilder::new();
-    let builder = builder.add_leaf(2, script1).unwrap();
-    let builder = builder.add_leaf(2, script2).unwrap();
-    let builder = builder.add_leaf(1, script3).unwrap();
-    builder.finalize(secp, internal_key).unwrap()
-}
-
-fn create_p2tr_address(tree: TaprootSpendInfo) -> Address {
-    let output_key = tree.output_key();
-    Address::p2tr_tweaked(output_key, Network::Testnet)
-}
-
-fn create_psbt_for_taproot_key_path_spend(from_address: Address, to_address: Address, tree: TaprootSpendInfo) -> Psbt {
-
-    let send_value = 6400;
-    let out_puts = vec![
-        TxOut { value: Amount::from_sat(send_value), script_pubkey: to_address.script_pubkey() },
-    ];
-    let prev_tx_id = "06980ca116f74c7845a897461dd0e1d15b114130176de5004957da516b4dee3a";
-
-    let transaction = Transaction {
-        version: Version(2),
-        lock_time: absolute::LockTime::ZERO,
-        input: vec![TxIn {
-            previous_output: OutPoint { txid: prev_tx_id.parse().unwrap(), vout: 0 },
-            script_sig: ScriptBuf::new(),
-            sequence: Sequence(0xFFFFFFFF), // Ignore nSequence.
-            witness: Witness::default(),
-        }],
-        output: out_puts,
-    };
-
-    let mut psbt = Psbt::from_unsigned_tx(transaction).unwrap();
-
-
-    let mfp = "73c5da0a";
-    let internal_key_path = "86'/1'/0'/0/2";
-
-    let mut origins = BTreeMap::new();
-    origins.insert(
-        tree.internal_key(),
-        (
-            vec![],
-            (
-                Fingerprint::from_str(mfp).unwrap(),
-                DerivationPath::from_str(internal_key_path).unwrap(),
-            ),
-        ),
-    );
-
-    let utxo_value = 6588;
-    let mut input = Input {
-        witness_utxo: {
-            let script_pubkey = from_address.script_pubkey();
-            Some(TxOut { value: Amount::from_sat(utxo_value), script_pubkey })
-        },
-        tap_key_origins: origins,
-        ..Default::default()
-    };
-    let ty = PsbtSighashType::from_str("SIGHASH_DEFAULT").unwrap();
-    input.sighash_type = Some(ty);
-    input.tap_internal_key = Some(tree.internal_key());
-    input.tap_merkle_root = tree.merkle_root();
-    psbt.inputs = vec![input];
-    psbt
-}
-
-fn finalize_psbt_for_key_path_spend(mut psbt: Psbt) -> Psbt {
-    psbt.inputs.iter_mut().for_each(|input| {
-        let mut script_witness: Witness = Witness::new();
-        script_witness.push(input.tap_key_sig.unwrap().to_vec());
-        input.final_script_witness = Some(script_witness);
-        input.partial_sigs = BTreeMap::new();
-        input.sighash_type = None;
-        input.redeem_script = None;
-        input.witness_script = None;
-        input.bip32_derivation = BTreeMap::new();
-    });
-    psbt
-}
-
-fn create_psbt_for_taproot_script_path_spend(from_address: Address, to_address: Address, tree: TaprootSpendInfo, x_only_pubkey_of_signing_key: XOnlyPublicKey, signing_key_path: &str, use_script: ScriptBuf) -> Psbt {
-    let utxo_value = 6280;
-    let send_value = 6000;
-    let mfp = "73c5da0a";
-
-    let out_puts = vec![
-        TxOut { value: Amount::from_sat(send_value), script_pubkey: to_address.script_pubkey() },
-    ];
-    let prev_tx_id = "9d7c6770fca57285babab60c51834cfcfd10ad302119cae842d7216b4ac9a376";
-    let transaction = Transaction {
-        version: Version(2),
-        lock_time: absolute::LockTime::ZERO,
-        input: vec![TxIn {
-            previous_output: OutPoint { txid: prev_tx_id.parse().unwrap(), vout: 0 },
-            script_sig: ScriptBuf::new(),
-            sequence: Sequence(0xFFFFFFFF), // Ignore nSequence.
-            witness: Witness::default(),
-        }],
-        output: out_puts,
-    };
-
-    let mut psbt = Psbt::from_unsigned_tx(transaction).unwrap();
-
-    let mut origins = BTreeMap::new();
-    origins.insert(
-        x_only_pubkey_of_signing_key,
-        (
-            vec![use_script.tapscript_leaf_hash()],
-            (
-                Fingerprint::from_str(mfp).unwrap(),
-                DerivationPath::from_str(signing_key_path).unwrap(),
-            ),
-        ),
-    );
-
-    let mut tap_scripts = BTreeMap::new();
-    tap_scripts.insert(
-        tree.control_block(&(use_script.clone(), LeafVersion::TapScript)).unwrap(),
-        (use_script.clone(), LeafVersion::TapScript),
-    );
-
-    let mut input = Input {
-        witness_utxo: {
-            let script_pubkey= from_address.script_pubkey();
-            Some(TxOut { value: Amount::from_sat(utxo_value), script_pubkey })
-        },
-        tap_key_origins: origins,
-        tap_scripts,
-        ..Default::default()
-    };
-    let ty = PsbtSighashType::from_str("SIGHASH_ALL").unwrap();
-    input.sighash_type = Some(ty);
-    input.tap_internal_key = Some(tree.internal_key());
-    input.tap_merkle_root = tree.merkle_root();
-    psbt.inputs = vec![input];
-    psbt
-}
-
-
-fn finalize_psbt_for_script_path_spend(mut psbt: Psbt) -> Psbt {
-    psbt.inputs.iter_mut().for_each(|input| {
-        let mut script_witness: Witness = Witness::new();
-        for (_, signature) in input.tap_script_sigs.iter() {
-            script_witness.push(signature.to_vec());
-        }
-        for (control_block, (script, _)) in input.tap_scripts.iter() {
-            script_witness.push(script.to_bytes());
-            script_witness.push(control_block.serialize());
-        }
-        input.final_script_witness = Some(script_witness);
-        input.partial_sigs = BTreeMap::new();
-        input.sighash_type = None;
-        input.redeem_script = None;
-        input.witness_script = None;
-        input.bip32_derivation = BTreeMap::new();
-        input.tap_script_sigs = BTreeMap::new();
-        input.tap_scripts = BTreeMap::new();
-        input.tap_key_sig = None;
-    });
-    psbt
-}