Make Lower/Upper hex print scripts with len prefix

Add the length prefix when formatting hex strings by way of `LowerHex`
and `UpperHex`.

This looses formatting options because I can't remember right now how
not to - again.
This commit is contained in:
Tobin C. Harding 2025-04-16 18:10:15 +10:00
parent c27b95fb0d
commit 2b72f1f30b
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
2 changed files with 32 additions and 19 deletions

View File

@ -1,5 +1,12 @@
// SPDX-License-Identifier: CC0-1.0 // SPDX-License-Identifier: CC0-1.0
//! Demonstrates the API for parsing and formatting Bitcoin scripts.
//!
//! Bitcoin script is conceptually a vector of bytes. As such it is consensus encoded with a compact
//! size encoded length prefix. See [CompactSize].
//!
//! [`CompactSize`]: <https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer>
use bitcoin::consensus::encode; use bitcoin::consensus::encode;
use bitcoin::key::WPubkeyHash; use bitcoin::key::WPubkeyHash;
use bitcoin::script::{self, ScriptExt, ScriptBufExt}; use bitcoin::script::{self, ScriptExt, ScriptBufExt};
@ -8,6 +15,7 @@ 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`.
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();
let decoded = ScriptBuf::from_hex(&hex).unwrap(); let decoded = ScriptBuf::from_hex(&hex).unwrap();
@ -19,16 +27,19 @@ 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 not equivalent to consensus encoding i.e., does not include the length prefix. // This is equivalent to consensus encoding i.e., includes the length prefix.
let hex_lower_hex_trait = format!("{:x}", script_code); let hex_lower_hex_trait = format!("{:x}", script_code);
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!(encode::deserialize_hex::<ScriptBuf>(&hex_lower_hex_trait).is_err()); assert_eq!(encode::deserialize_hex::<ScriptBuf>(&hex_lower_hex_trait).unwrap(), script_code);
// And so does `from_hex`. // And so does `from_hex`.
assert!(ScriptBuf::from_hex(&hex_lower_hex_trait).is_err()); assert_eq!(ScriptBuf::from_hex(&hex_lower_hex_trait).unwrap(), script_code);
// But we provide an explicit constructor that does not.
assert_eq!(ScriptBuf::from_hex_no_length_prefix(&hex_lower_hex_trait).unwrap(), script_code); // And we also provide an explicit constructor that does not use the length prefix.
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(); // Defined in `ScriptExt`.
@ -47,12 +58,7 @@ 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);
let decoded = ScriptBuf::from_hex(&encoded).unwrap();
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 encoded = encode::serialize_hex(&script_code);
let decoded = ScriptBuf::from_hex(&encoded).unwrap(); let decoded = ScriptBuf::from_hex(&encoded).unwrap();
assert_eq!(decoded, script_code); assert_eq!(decoded, script_code);

View File

@ -431,9 +431,12 @@ 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 {
fmt::LowerHex::fmt(&self.as_bytes().as_hex(), f) let compact = internals::compact_size::encode(self.as_bytes().len());
write!(f, "{:x}", compact.as_slice().as_hex())?;
write!(f, "{:x}", self.as_bytes().as_hex())
} }
} }
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
@ -451,9 +454,12 @@ 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 {
fmt::UpperHex::fmt(&self.as_bytes().as_hex(), f) let compact = internals::compact_size::encode(self.as_bytes().len());
write!(f, "{:X}", compact.as_slice().as_hex())?;
write!(f, "{:X}", self.as_bytes().as_hex())
} }
} }
@ -505,7 +511,8 @@ impl serde::Serialize for Script {
S: serde::Serializer, S: serde::Serializer,
{ {
if serializer.is_human_readable() { if serializer.is_human_readable() {
serializer.collect_str(&format_args!("{:x}", self)) // Do not call LowerHex because we don't want to add the len prefix.
serializer.collect_str(&format_args!("{}", self.as_bytes().as_hex()))
} else { } else {
serializer.serialize_bytes(self.as_bytes()) serializer.serialize_bytes(self.as_bytes())
} }
@ -796,8 +803,8 @@ mod tests {
#[cfg(feature = "hex")] #[cfg(feature = "hex")]
{ {
assert_eq!(format!("{:x}", script), "00a1b2"); assert_eq!(format!("{:x}", script), "0300a1b2");
assert_eq!(format!("{:X}", script), "00A1B2"); assert_eq!(format!("{:X}", script), "0300A1B2");
} }
assert!(!format!("{:?}", script).is_empty()); assert!(!format!("{:?}", script).is_empty());
} }
@ -809,8 +816,8 @@ mod tests {
#[cfg(feature = "hex")] #[cfg(feature = "hex")]
{ {
assert_eq!(format!("{:x}", script_buf), "00a1b2"); assert_eq!(format!("{:x}", script_buf), "0300a1b2");
assert_eq!(format!("{:X}", script_buf), "00A1B2"); assert_eq!(format!("{:X}", script_buf), "0300A1B2");
} }
assert!(!format!("{:?}", script_buf).is_empty()); assert!(!format!("{:?}", script_buf).is_empty());
} }
@ -928,7 +935,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, "a1b2c3"); assert_eq!(hex, "03a1b2c3");
} }
#[test] #[test]
@ -937,6 +944,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, "a1b2c3"); assert_eq!(hex, "03a1b2c3");
} }
} }