From 2b72f1f30bdafa234cfc6525d6e7724598d40507 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Wed, 16 Apr 2025 18:10:15 +1000 Subject: [PATCH] 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. --- bitcoin/examples/script.rs | 26 ++++++++++++++++---------- primitives/src/script/mod.rs | 25 ++++++++++++++++--------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/bitcoin/examples/script.rs b/bitcoin/examples/script.rs index a7af632dc..e7195ecae 100644 --- a/bitcoin/examples/script.rs +++ b/bitcoin/examples/script.rs @@ -1,5 +1,12 @@ // 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`]: + use bitcoin::consensus::encode; use bitcoin::key::WPubkeyHash; use bitcoin::script::{self, ScriptExt, ScriptBufExt}; @@ -8,6 +15,7 @@ use bitcoin::ScriptBuf; fn main() { let pk = "b472a266d0bd89c13706a4132ccfb16f7c3b9fcb".parse::().unwrap(); + // TL;DR Use `to_hex_string` and `from_hex`. let script_code = script::p2wpkh_script_code(pk); let hex = script_code.to_hex_string(); let decoded = ScriptBuf::from_hex(&hex).unwrap(); @@ -19,16 +27,19 @@ fn main() { // We do not implement parsing scripts from human-readable format. // let decoded = s.parse::().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); println!("hex created using `LowerHex`: {}", hex_lower_hex_trait); // The `deserialize_hex` function requires the length prefix. - assert!(encode::deserialize_hex::(&hex_lower_hex_trait).is_err()); + assert_eq!(encode::deserialize_hex::(&hex_lower_hex_trait).unwrap(), script_code); // And so does `from_hex`. - assert!(ScriptBuf::from_hex(&hex_lower_hex_trait).is_err()); - // But we provide an explicit constructor that does not. - assert_eq!(ScriptBuf::from_hex_no_length_prefix(&hex_lower_hex_trait).unwrap(), script_code); + assert_eq!(ScriptBuf::from_hex(&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. 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(); 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. - let encoded = encode::serialize_hex(&script_code); let decoded = ScriptBuf::from_hex(&encoded).unwrap(); assert_eq!(decoded, script_code); diff --git a/primitives/src/script/mod.rs b/primitives/src/script/mod.rs index 012b4f444..473b85cb1 100644 --- a/primitives/src/script/mod.rs +++ b/primitives/src/script/mod.rs @@ -431,9 +431,12 @@ impl fmt::Display for ScriptBuf { #[cfg(feature = "hex")] impl fmt::LowerHex for Script { + // Currently we drop all formatter options. #[inline] 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")] @@ -451,9 +454,12 @@ internals::impl_to_hex_from_lower_hex!(ScriptBuf, |script_buf: &Self| script_buf #[cfg(feature = "hex")] impl fmt::UpperHex for Script { + // Currently we drop all formatter options. #[inline] 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, { 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 { serializer.serialize_bytes(self.as_bytes()) } @@ -796,8 +803,8 @@ mod tests { #[cfg(feature = "hex")] { - assert_eq!(format!("{:x}", script), "00a1b2"); - assert_eq!(format!("{:X}", script), "00A1B2"); + assert_eq!(format!("{:x}", script), "0300a1b2"); + assert_eq!(format!("{:X}", script), "0300A1B2"); } assert!(!format!("{:?}", script).is_empty()); } @@ -809,8 +816,8 @@ mod tests { #[cfg(feature = "hex")] { - assert_eq!(format!("{:x}", script_buf), "00a1b2"); - assert_eq!(format!("{:X}", script_buf), "00A1B2"); + assert_eq!(format!("{:x}", script_buf), "0300a1b2"); + assert_eq!(format!("{:X}", script_buf), "0300A1B2"); } assert!(!format!("{:?}", script_buf).is_empty()); } @@ -928,7 +935,7 @@ mod tests { fn script_to_hex() { let script = Script::from_bytes(&[0xa1, 0xb2, 0xc3]); let hex = script.to_hex(); - assert_eq!(hex, "a1b2c3"); + assert_eq!(hex, "03a1b2c3"); } #[test] @@ -937,6 +944,6 @@ mod tests { fn scriptbuf_to_hex() { let script = ScriptBuf::from_bytes(vec![0xa1, 0xb2, 0xc3]); let hex = script.to_hex(); - assert_eq!(hex, "a1b2c3"); + assert_eq!(hex, "03a1b2c3"); } }