primitives: Make hex optional

Make the `hex` dependency optional. This means not implementing
`Display` for some types if `hex` is not enabled and only implementing
`Debug`.

Also without `hex` enabled:

- We loose the ability to parse an `OutPoint` from string because we
  can no longer parse `Txid`.

- We loose the hex formatting of witness elements.

Note also that `primitives` builds with the `serde` feature even if
`hex?/serde` is excluded from the `serde` feature. I found this
surprising.
This commit is contained in:
Tobin C. Harding 2025-03-20 15:55:08 +11:00
parent b4326f0806
commit a5f904559d
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
9 changed files with 67 additions and 17 deletions

View File

@ -213,6 +213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4afe881d0527571892c4034822e59bb10c6c991cce6abe8199b6f5cf10766f55"
dependencies = [
"arrayvec",
"serde",
]
[[package]]

View File

@ -215,6 +215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4afe881d0527571892c4034822e59bb10c6c991cce6abe8199b6f5cf10766f55"
dependencies = [
"arrayvec",
"serde",
]
[[package]]

View File

@ -31,7 +31,7 @@ hashes = { package = "bitcoin_hashes", path = "../hashes", default-features = fa
hex = { package = "hex-conservative", version = "0.3.0", default-features = false, features = ["alloc"] }
internals = { package = "bitcoin-internals", path = "../internals", features = ["alloc", "hex"] }
io = { package = "bitcoin-io", path = "../io", default-features = false, features = ["alloc", "hashes"] }
primitives = { package = "bitcoin-primitives", path = "../primitives", default-features = false, features = ["alloc"] }
primitives = { package = "bitcoin-primitives", path = "../primitives", default-features = false, features = ["alloc", "hex"] }
secp256k1 = { version = "0.30.0", default-features = false, features = ["hashes", "alloc", "rand"] }
units = { package = "bitcoin-units", path = "../units", default-features = false, features = ["alloc"] }

View File

@ -15,19 +15,20 @@ rust-version = "1.63.0"
exclude = ["tests", "contrib"]
[features]
default = ["std"]
std = ["alloc", "hashes/std", "hex/std", "internals/std", "units/std"]
alloc = ["hashes/alloc", "hex/alloc", "internals/alloc", "units/alloc"]
serde = ["dep:serde", "hashes/serde", "internals/serde", "units/serde", "alloc"]
default = ["std", "hex"]
std = ["alloc", "hashes/std", "hex?/std", "internals/std", "units/std"]
alloc = ["hashes/alloc", "hex?/alloc", "internals/alloc", "units/alloc"]
serde = ["dep:serde", "hashes/serde", "hex?/serde", "internals/serde", "units/serde", "alloc", "hex"]
arbitrary = ["dep:arbitrary", "units/arbitrary"]
hex = ["dep:hex", "hashes/hex", "internals/hex"]
[dependencies]
hashes = { package = "bitcoin_hashes", path = "../hashes", default-features = false, features = ["hex"] }
hex = { package = "hex-conservative", version = "0.3.0", default-features = false }
internals = { package = "bitcoin-internals", path = "../internals", features = ["hex"] }
hashes = { package = "bitcoin_hashes", path = "../hashes", default-features = false }
internals = { package = "bitcoin-internals", path = "../internals" }
units = { package = "bitcoin-units", path = "../units", default-features = false }
arbitrary = { version = "1.4", optional = true }
hex = { package = "hex-conservative", version = "0.3.0", default-features = false, optional = true }
serde = { version = "1.0.103", default-features = false, features = ["derive", "alloc"], optional = true }
[dev-dependencies]

View File

@ -310,7 +310,10 @@ hashes::hash_newtype! {
pub struct WitnessCommitment(sha256d::Hash);
}
#[cfg(feature = "hex")]
hashes::impl_hex_for_newtype!(BlockHash, WitnessCommitment);
#[cfg(not(feature = "hex"))]
hashes::impl_debug_only_for_newtype!(BlockHash, WitnessCommitment);
#[cfg(feature = "serde")]
hashes::impl_serde_for_newtype!(BlockHash, WitnessCommitment);

View File

@ -11,6 +11,9 @@ hashes::hash_newtype! {
pub struct WitnessMerkleNode(sha256d::Hash);
}
#[cfg(feature = "hex")]
hashes::impl_hex_for_newtype!(TxMerkleNode, WitnessMerkleNode);
#[cfg(not(feature = "hex"))]
hashes::impl_debug_only_for_newtype!(TxMerkleNode, WitnessMerkleNode);
#[cfg(feature = "serde")]
hashes::impl_serde_for_newtype!(TxMerkleNode, WitnessMerkleNode);

View File

@ -10,6 +10,7 @@ use core::convert::Infallible;
use core::fmt;
use hashes::{hash160, sha256};
#[cfg(feature = "hex")]
use hex::DisplayHex;
use internals::script::{self, PushDataLenLen};
@ -49,7 +50,10 @@ hashes::hash_newtype! {
pub struct WScriptHash(sha256::Hash);
}
#[cfg(feature = "hex")]
hashes::impl_hex_for_newtype!(ScriptHash, WScriptHash);
#[cfg(not(feature = "hex"))]
hashes::impl_debug_only_for_newtype!(ScriptHash, WScriptHash);
#[cfg(feature = "serde")]
hashes::impl_serde_for_newtype!(ScriptHash, WScriptHash);
@ -425,6 +429,7 @@ impl fmt::Display for ScriptBuf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(self.as_script(), f) }
}
#[cfg(feature = "hex")]
impl fmt::LowerHex for Script {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@ -432,15 +437,19 @@ impl fmt::LowerHex for Script {
}
}
#[cfg(feature = "alloc")]
#[cfg(feature = "hex")]
internals::impl_to_hex_from_lower_hex!(Script, |script: &Self| script.len() * 2);
#[cfg(feature = "hex")]
impl fmt::LowerHex for ScriptBuf {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(self.as_script(), f) }
}
#[cfg(feature = "alloc")]
#[cfg(feature = "hex")]
internals::impl_to_hex_from_lower_hex!(ScriptBuf, |script_buf: &Self| script_buf.len() * 2);
#[cfg(feature = "hex")]
impl fmt::UpperHex for Script {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@ -448,6 +457,7 @@ impl fmt::UpperHex for Script {
}
}
#[cfg(feature = "hex")]
impl fmt::UpperHex for ScriptBuf {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::UpperHex::fmt(self.as_script(), f) }
@ -720,16 +730,24 @@ mod tests {
fn script_display() {
let script = Script::from_bytes(&[0xa1, 0xb2, 0xc3]);
assert_eq!(format!("{}", script), "OP_LESSTHANOREQUAL OP_CSV OP_RETURN_195");
assert_eq!(format!("{:x}", script), "a1b2c3");
assert_eq!(format!("{:X}", script), "A1B2C3");
#[cfg(feature = "hex")]
{
assert_eq!(format!("{:x}", script), "a1b2c3");
assert_eq!(format!("{:X}", script), "A1B2C3");
}
}
#[test]
fn scriptbuf_display() {
let script_buf = ScriptBuf::from(vec![0xa1, 0xb2, 0xc3]);
assert_eq!(format!("{}", script_buf), "OP_LESSTHANOREQUAL OP_CSV OP_RETURN_195");
assert_eq!(format!("{:x}", script_buf), "a1b2c3");
assert_eq!(format!("{:X}", script_buf), "A1B2C3");
#[cfg(feature = "hex")]
{
assert_eq!(format!("{:x}", script_buf), "a1b2c3");
assert_eq!(format!("{:X}", script_buf), "A1B2C3");
}
}
#[test]

View File

@ -13,6 +13,7 @@
#[cfg(feature = "alloc")]
use core::cmp;
#[cfg(feature = "alloc")]
#[cfg(feature = "hex")]
use core::convert::Infallible;
use core::fmt;
@ -20,9 +21,13 @@ use core::fmt;
use arbitrary::{Arbitrary, Unstructured};
use hashes::sha256d;
#[cfg(feature = "alloc")]
use internals::{compact_size, write_err};
use internals::compact_size;
#[cfg(feature = "hex")]
use internals::write_err;
#[cfg(feature = "hex")]
use units::parse;
#[cfg(feature = "alloc")]
use units::{parse, Amount, Weight};
use units::{Amount, Weight};
#[cfg(feature = "alloc")]
use crate::locktime::absolute;
@ -389,6 +394,7 @@ impl OutPoint {
pub const COINBASE_PREVOUT: Self = Self { txid: Txid::COINBASE_PREVOUT, vout: u32::MAX };
}
#[cfg(feature = "hex")]
impl fmt::Display for OutPoint {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@ -397,6 +403,7 @@ impl fmt::Display for OutPoint {
}
#[cfg(feature = "alloc")]
#[cfg(feature = "hex")]
impl core::str::FromStr for OutPoint {
type Err = ParseOutPointError;
@ -424,6 +431,7 @@ impl core::str::FromStr for OutPoint {
///
/// Does not permit leading zeroes or non-digit characters.
#[cfg(feature = "alloc")]
#[cfg(feature = "hex")]
fn parse_vout(s: &str) -> Result<u32, ParseOutPointError> {
if s.len() > 1 {
let first = s.chars().next().unwrap();
@ -438,6 +446,7 @@ fn parse_vout(s: &str) -> Result<u32, ParseOutPointError> {
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
#[cfg(feature = "alloc")]
#[cfg(feature = "hex")]
pub enum ParseOutPointError {
/// Error in TXID part.
Txid(hex::HexToArrayError),
@ -452,12 +461,14 @@ pub enum ParseOutPointError {
}
#[cfg(feature = "alloc")]
#[cfg(feature = "hex")]
impl From<Infallible> for ParseOutPointError {
#[inline]
fn from(never: Infallible) -> Self { match never {} }
}
#[cfg(feature = "alloc")]
#[cfg(feature = "hex")]
impl fmt::Display for ParseOutPointError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ParseOutPointError as E;
@ -473,6 +484,7 @@ impl fmt::Display for ParseOutPointError {
}
#[cfg(feature = "std")]
#[cfg(feature = "hex")]
impl std::error::Error for ParseOutPointError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use ParseOutPointError as E;
@ -500,7 +512,10 @@ hashes::hash_newtype! {
pub struct Wtxid(sha256d::Hash);
}
#[cfg(feature = "hex")]
hashes::impl_hex_for_newtype!(Txid, Wtxid);
#[cfg(not(feature = "hex"))]
hashes::impl_debug_only_for_newtype!(Txid, Wtxid);
#[cfg(feature = "serde")]
hashes::impl_serde_for_newtype!(Txid, Wtxid);
@ -699,6 +714,7 @@ mod tests {
}
#[test]
#[cfg(feature = "hex")]
fn outpoint_from_str() {
// Check format errors
let mut outpoint_str = "0".repeat(64); // No ":"
@ -725,6 +741,7 @@ mod tests {
}
#[test]
#[cfg(feature = "hex")]
fn canonical_vout() {
assert_eq!(parse_vout("0").unwrap(), 0);
assert_eq!(parse_vout("1").unwrap(), 1);

View File

@ -9,7 +9,6 @@ use core::ops::Index;
#[cfg(feature = "arbitrary")]
use arbitrary::{Arbitrary, Unstructured};
use hex::DisplayHex;
use internals::compact_size;
use internals::slice::SliceExt;
use internals::wrap_debug::WrapDebug;
@ -314,7 +313,7 @@ impl<T: core::borrow::Borrow<[u8]>> PartialEq<Witness> for alloc::sync::Arc<[T]>
/// Debug implementation that displays the witness as a structured output containing:
/// - Number of witness elements
/// - Total bytes across all elements
/// - List of hex-encoded witness elements
/// - List of hex-encoded witness elements if `hex` features is enabled.
#[allow(clippy::missing_fields_in_debug)] // We don't want to show `indices_start`.
impl fmt::Debug for Witness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -326,7 +325,14 @@ impl fmt::Debug for Witness {
.field(
"elements",
&WrapDebug(|f| {
f.debug_list().entries(self.iter().map(DisplayHex::as_hex)).finish()
#[cfg(feature = "hex")]
{
f.debug_list().entries(self.iter().map(hex::DisplayHex::as_hex)).finish()
}
#[cfg(not(feature = "hex"))]
{
f.debug_list().entries(self.iter()).finish()
}
}),
)
.finish()