Merge rust-bitcoin/rust-bitcoin#4469: Iterate on the script hex APIs

3b8164139f primitives: Add docs section for script hex API (Tobin C. Harding)
6b90e42e78 Finalize the script hex APIs (Tobin C. Harding)

Pull request description:

  In #4316 we made some 'improvements' to what script functions and trait implementations do and do not include the length prefix. Iterate again on it as described here: https://github.com/rust-bitcoin/rust-bitcoin/pull/4316#issuecomment-2847710436

  - Patch 1 does the changes
  - Patch 2 adds some more docs, requires a grammarian to check my Aussie lingua

ACKs for top commit:
  apoelstra:
    ACK 3b8164139f6ecebc97b66a299b4a87c2288d8a76; successfully ran local tests

Tree-SHA512: 481db88ae1b6f5751e81e4cd126f545cfc34bef6dcfcf857f1c7464aeb41f5de95fc4582c015abde04372fe025efa13cdf2906e75517f62cff3ddec05c4d9711
This commit is contained in:
merge-script 2025-05-12 11:13:50 +00:00
commit 9578ad3e50
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
6 changed files with 68 additions and 38 deletions

View File

@ -15,10 +15,16 @@ use bitcoin::ScriptBuf;
fn main() { fn main() {
let pk = "b472a266d0bd89c13706a4132ccfb16f7c3b9fcb".parse::<WPubkeyHash>().unwrap(); let pk = "b472a266d0bd89c13706a4132ccfb16f7c3b9fcb".parse::<WPubkeyHash>().unwrap();
// TL;DR Use `to_hex_string` and `from_hex`. // TL;DR Use `to_hex_string_prefixed` and `from_hex_prefixed`.
let script_code = script::p2wpkh_script_code(pk); let script_code = script::p2wpkh_script_code(pk);
let hex = script_code.to_hex_string(); let hex = script_code.to_hex_string_prefixed();
let decoded = ScriptBuf::from_hex(&hex).unwrap(); let decoded = ScriptBuf::from_hex_prefixed(&hex).unwrap();
assert_eq!(decoded, script_code);
// Or if you prefer: `to_hex_string_no_length_prefix` and `from_hex_no_length_prefix`.
let script_code = script::p2wpkh_script_code(pk);
let hex = script_code.to_hex_string_no_length_prefix();
let decoded = ScriptBuf::from_hex_no_length_prefix(&hex).unwrap();
assert_eq!(decoded, script_code); assert_eq!(decoded, script_code);
// Writes the script as human-readable eg, OP_DUP OP_HASH160 OP_PUSHBYTES_20 ... // Writes the script as human-readable eg, OP_DUP OP_HASH160 OP_PUSHBYTES_20 ...
@ -27,28 +33,25 @@ fn main() {
// We do not implement parsing scripts from human-readable format. // We do not implement parsing scripts from human-readable format.
// let decoded = s.parse::<ScriptBuf>().unwrap(); // let decoded = s.parse::<ScriptBuf>().unwrap();
// This is equivalent to consensus encoding i.e., includes the length prefix. // This is not equivalent to consensus encoding i.e., does not include the length prefix.
let hex_lower_hex_trait = format!("{script_code:x}"); let hex_lower_hex_trait = format!("{script_code:x}");
println!("hex created using `LowerHex`: {hex_lower_hex_trait}"); println!("hex created using `LowerHex`: {hex_lower_hex_trait}");
// The `deserialize_hex` function requires the length prefix. // The `deserialize_hex` function requires the length prefix.
assert_eq!(encode::deserialize_hex::<ScriptBuf>(&hex_lower_hex_trait).unwrap(), script_code); assert!(encode::deserialize_hex::<ScriptBuf>(&hex_lower_hex_trait).is_err());
// And so does `from_hex`. // And so does `from_hex_prefixed`.
assert_eq!(ScriptBuf::from_hex(&hex_lower_hex_trait).unwrap(), script_code); assert!(ScriptBuf::from_hex_prefixed(&hex_lower_hex_trait).is_err());
// But we provide an explicit constructor that does not.
// And we also provide an explicit constructor that does not use the length prefix. assert_eq!(ScriptBuf::from_hex_no_length_prefix(&hex_lower_hex_trait).unwrap(), script_code);
let other = ScriptBuf::from_hex_no_length_prefix(&hex_lower_hex_trait).unwrap();
// Without a prefix the script parses but its not the one we meant.
assert_ne!(other, script_code);
// This is consensus encoding i.e., includes the length prefix. // This is consensus encoding i.e., includes the length prefix.
let hex_inherent = script_code.to_hex_string(); // Defined in `ScriptExt`. let hex_inherent = script_code.to_hex_string_prefixed(); // Defined in `ScriptExt`.
println!("hex created using inherent `to_hex_string`: {hex_inherent}"); println!("hex created using inherent `to_hex_string_prefixed`: {hex_inherent}");
// The inverse of `to_hex_string` is `from_hex`. // The inverse of `to_hex_string_prefixed` is `from_hex_string_prefixed`.
let decoded = ScriptBuf::from_hex(&hex_inherent).unwrap(); // Defined in `ScriptBufExt`. let decoded = ScriptBuf::from_hex_prefixed(&hex_inherent).unwrap(); // Defined in `ScriptBufExt`.
assert_eq!(decoded, script_code); assert_eq!(decoded, script_code);
// We can also parse the output of `to_hex_string` using `deserialize_hex`. // We can also parse the output of `to_hex_string_prefixed` using `deserialize_hex`.
let decoded = encode::deserialize_hex::<ScriptBuf>(&hex_inherent).unwrap(); let decoded = encode::deserialize_hex::<ScriptBuf>(&hex_inherent).unwrap();
assert_eq!(decoded, script_code); assert_eq!(decoded, script_code);
@ -58,8 +61,10 @@ fn main() {
let decoded: ScriptBuf = encode::deserialize_hex(&encoded).unwrap(); let decoded: ScriptBuf = encode::deserialize_hex(&encoded).unwrap();
assert_eq!(decoded, script_code); assert_eq!(decoded, script_code);
// And we can mix these to calls because both include the length prefix. // And we can mix these to calls because both include the length prefix.
let decoded = ScriptBuf::from_hex(&encoded).unwrap(); let encoded = encode::serialize_hex(&script_code);
let decoded = ScriptBuf::from_hex_prefixed(&encoded).unwrap();
assert_eq!(decoded, script_code); assert_eq!(decoded, script_code);
// Encode/decode using a byte vector. // Encode/decode using a byte vector.

View File

@ -376,12 +376,19 @@ crate::internal_macros::define_extension_trait! {
fn to_asm_string(&self) -> String { self.to_string() } fn to_asm_string(&self) -> String { self.to_string() }
/// Consensus encodes the script as lower-case hex. /// Consensus encodes the script as lower-case hex.
fn to_hex_string(&self) -> String { consensus::encode::serialize_hex(self) } #[deprecated(since = "TBD", note = "use `to_hex_string_prefixed()` instead")]
fn to_hex_string(&self) -> String { self.to_hex_string_prefixed() }
/// Consensus encodes the script as lower-case hex.
fn to_hex_string_prefixed(&self) -> String { consensus::encode::serialize_hex(self) }
/// Consensus encodes the script as lower-case hex. /// Consensus encodes the script as lower-case hex.
/// ///
/// This is **not** consensus encoding, you likely want to use `to_hex_string`. The returned /// This is **not** consensus encoding, you likely want to use `to_hex_string_prefixed`.
/// hex string will not include the length prefix. ///
/// # Returns
///
/// The returned hex string will not include the length prefix.
fn to_hex_string_no_length_prefix(&self) -> String { fn to_hex_string_no_length_prefix(&self) -> String {
self.as_bytes().to_lower_hex_string() self.as_bytes().to_lower_hex_string()
} }

View File

@ -30,10 +30,18 @@ crate::internal_macros::define_extension_trait! {
/// Constructs a new [`ScriptBuf`] from a hex string. /// Constructs a new [`ScriptBuf`] from a hex string.
/// ///
/// The input string is expected to be consensus encoded i.e., includes the length prefix. /// The input string is expected to be consensus encoded i.e., includes the length prefix.
fn from_hex(s: &str) -> Result<ScriptBuf, consensus::FromHexError> { fn from_hex_prefixed(s: &str) -> Result<ScriptBuf, consensus::FromHexError> {
consensus::encode::deserialize_hex(s) consensus::encode::deserialize_hex(s)
} }
/// Constructs a new [`ScriptBuf`] from a hex string.
///
/// The input string is expected to be consensus encoded i.e., includes the length prefix.
#[deprecated(since = "TBD", note = "use `from_hex_string_prefixed()` instead")]
fn from_hex(s: &str) -> Result<ScriptBuf, consensus::FromHexError> {
Self::from_hex_prefixed(s)
}
/// Constructs a new [`ScriptBuf`] from a hex string. /// Constructs a new [`ScriptBuf`] from a hex string.
/// ///
/// This is **not** consensus encoding. If your hex string is a consensus encode script then /// This is **not** consensus encoding. If your hex string is a consensus encode script then

View File

@ -49,6 +49,14 @@ internals::transparent_newtype! {
/// The type is `#[repr(transparent)]` for internal purposes only! /// The type is `#[repr(transparent)]` for internal purposes only!
/// No consumer crate may rely on the representation of the struct! /// No consumer crate may rely on the representation of the struct!
/// ///
/// # Hexadecimal strings
///
/// Scripts are consensus encoded with a length prefix and as a result of this in some places in
/// the eccosystem one will encounter hex strings that include the prefix while in other places
/// the prefix is excluded. To support parsing and formatting scripts as hex we provide a bunch
/// of different APIs and trait implementations. Please see [`examples/script.rs`] for a
/// thorough example of all the APIs.
///
/// # Bitcoin Core References /// # Bitcoin Core References
/// ///
/// * [CScript definition](https://github.com/bitcoin/bitcoin/blob/d492dc1cdaabdc52b0766bf4cba4bd73178325d0/src/script/script.h#L410) /// * [CScript definition](https://github.com/bitcoin/bitcoin/blob/d492dc1cdaabdc52b0766bf4cba4bd73178325d0/src/script/script.h#L410)

View File

@ -431,12 +431,9 @@ impl fmt::Display for ScriptBuf {
#[cfg(feature = "hex")] #[cfg(feature = "hex")]
impl fmt::LowerHex for Script { impl fmt::LowerHex for Script {
// Currently we drop all formatter options.
#[inline] #[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let compact = internals::compact_size::encode(self.as_bytes().len()); fmt::LowerHex::fmt(&self.as_bytes().as_hex(), f)
write!(f, "{:x}", compact.as_slice().as_hex())?;
write!(f, "{:x}", self.as_bytes().as_hex())
} }
} }
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
@ -454,12 +451,9 @@ internals::impl_to_hex_from_lower_hex!(ScriptBuf, |script_buf: &Self| script_buf
#[cfg(feature = "hex")] #[cfg(feature = "hex")]
impl fmt::UpperHex for Script { impl fmt::UpperHex for Script {
// Currently we drop all formatter options.
#[inline] #[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let compact = internals::compact_size::encode(self.as_bytes().len()); fmt::UpperHex::fmt(&self.as_bytes().as_hex(), f)
write!(f, "{:X}", compact.as_slice().as_hex())?;
write!(f, "{:X}", self.as_bytes().as_hex())
} }
} }
@ -511,8 +505,7 @@ impl serde::Serialize for Script {
S: serde::Serializer, S: serde::Serializer,
{ {
if serializer.is_human_readable() { if serializer.is_human_readable() {
// Do not call LowerHex because we don't want to add the len prefix. serializer.collect_str(&format_args!("{:x}", self))
serializer.collect_str(&format_args!("{}", self.as_bytes().as_hex()))
} else { } else {
serializer.serialize_bytes(self.as_bytes()) serializer.serialize_bytes(self.as_bytes())
} }
@ -803,8 +796,8 @@ mod tests {
#[cfg(feature = "hex")] #[cfg(feature = "hex")]
{ {
assert_eq!(format!("{:x}", script), "0300a1b2"); assert_eq!(format!("{:x}", script), "00a1b2");
assert_eq!(format!("{:X}", script), "0300A1B2"); assert_eq!(format!("{:X}", script), "00A1B2");
} }
assert!(!format!("{:?}", script).is_empty()); assert!(!format!("{:?}", script).is_empty());
} }
@ -816,8 +809,8 @@ mod tests {
#[cfg(feature = "hex")] #[cfg(feature = "hex")]
{ {
assert_eq!(format!("{:x}", script_buf), "0300a1b2"); assert_eq!(format!("{:x}", script_buf), "00a1b2");
assert_eq!(format!("{:X}", script_buf), "0300A1B2"); assert_eq!(format!("{:X}", script_buf), "00A1B2");
} }
assert!(!format!("{:?}", script_buf).is_empty()); assert!(!format!("{:?}", script_buf).is_empty());
} }
@ -935,7 +928,7 @@ mod tests {
fn script_to_hex() { fn script_to_hex() {
let script = Script::from_bytes(&[0xa1, 0xb2, 0xc3]); let script = Script::from_bytes(&[0xa1, 0xb2, 0xc3]);
let hex = script.to_hex(); let hex = script.to_hex();
assert_eq!(hex, "03a1b2c3"); assert_eq!(hex, "a1b2c3");
} }
#[test] #[test]
@ -944,6 +937,6 @@ mod tests {
fn scriptbuf_to_hex() { fn scriptbuf_to_hex() {
let script = ScriptBuf::from_bytes(vec![0xa1, 0xb2, 0xc3]); let script = ScriptBuf::from_bytes(vec![0xa1, 0xb2, 0xc3]);
let hex = script.to_hex(); let hex = script.to_hex();
assert_eq!(hex, "03a1b2c3"); assert_eq!(hex, "a1b2c3");
} }
} }

View File

@ -16,6 +16,15 @@ use crate::prelude::{Box, Vec};
/// Just as other similar types, this implements [`Deref`], so [deref coercions] apply. Also note /// Just as other similar types, this implements [`Deref`], so [deref coercions] apply. Also note
/// that all the safety/validity restrictions that apply to [`Script`] apply to `ScriptBuf` as well. /// that all the safety/validity restrictions that apply to [`Script`] apply to `ScriptBuf` as well.
/// ///
/// # Hexadecimal strings
///
/// Scripts are consensus encoded with a length prefix and as a result of this in some places in the
/// eccosystem one will encounter hex strings that include the prefix while in other places the
/// prefix is excluded. To support parsing and formatting scripts as hex we provide a bunch of
/// different APIs and trait implementations. Please see [`examples/script.rs`] for a thorough
/// example of all the APIs.
///
/// [`examples/script.rs`]: <https://github.com/rust-bitcoin/rust-bitcoin/blob/master/bitcoin/examples/script.rs>
/// [deref coercions]: https://doc.rust-lang.org/std/ops/trait.Deref.html#more-on-deref-coercion /// [deref coercions]: https://doc.rust-lang.org/std/ops/trait.Deref.html#more-on-deref-coercion
#[derive(Default, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] #[derive(Default, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct ScriptBuf(Vec<u8>); pub struct ScriptBuf(Vec<u8>);