What changed, and why it matters
This commit only adds and moves automated tests for Bitcoin script building code. It does not change any production logic, fix bugs, or alter behavior. There is no security issue here.
No action needed. This is a benign test-only change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit is a test-only refactor in rust-bitcoin. It moves existing tests from bitcoin/src/blockdata/script/tests.rs into primitives/src/script/tests.rs and primitives/src/script/builder.rs, adds new unit tests for ScriptBuf and Builder push encoding, and updates .cargo/mutants.toml to exclude a mutant that cannot be tested due to u8 truncation. No production code paths are modified.
Changed components
primitives/src/script/builder.rsprimitives/src/script/owned.rsprimitives/src/script/tests.rsbitcoin/src/blockdata/script/tests.rs.cargo/mutants.tomlInspect captured patch +214 / −67
diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml
index 7006c602..29ac6b3b 100644
--- a/.cargo/mutants.toml
+++ b/.cargo/mutants.toml
@@ -70,6 +70,7 @@ exclude_re = [
"primitives/.* calculate_root_batched", # Behind cfg(target_arch = "aarch64"), not compiled on x86_64 CI runner
"primitives/.* <impl MerkleNode for TxMerkleNode>::calculate_root", # Behind cfg(target_arch = "aarch64"), not compiled on x86_64 CI runner
"primitives/.* <impl MerkleNode for WitnessMerkleNode>::calculate_root", # Behind cfg(target_arch = "aarch64"), not compiled on x86_64 CI runner
+ "primitives/.* replace \\% with \\+ in ScriptBuf<T>::push_slice_no_opt", # as u8 truncation makes >= 0x100 impossible to test
# consensus_encoding - most of these are for mutations in the logic used to determine when to stop encoding or decoding.
"consensus_encoding/.* <impl Decoder for ArrayDecoder<N>>::push_bytes", # Mutations cause an infinite loop
diff --git a/bitcoin/src/blockdata/script/tests.rs b/bitcoin/src/blockdata/script/tests.rs
index abbe0671..02748176 100644
--- a/bitcoin/src/blockdata/script/tests.rs
+++ b/bitcoin/src/blockdata/script/tests.rs
@@ -1,7 +1,5 @@
// SPDX-License-Identifier: CC0-1.0
-#[cfg(feature = "serde")]
-use alloc::borrow::ToOwned;
use alloc::string::ToString;
use hex::hex;
@@ -267,22 +265,6 @@ fn script_x_only_key() {
assert_eq!(script.into_bytes(), &hex!(KEYSTR) as &[u8]);
}
-#[test]
-fn script_builder() {
- // from txid 3bb5e6434c11fb93f64574af5d116736510717f2c595eb45b52c28e31622dfff which was in my mempool when I wrote the test
- let script = ScriptPubKey::builder()
- .push_opcode(OP_DUP)
- .push_opcode(OP_HASH160)
- .push_slice(hex!("16e1ae70ff0fa102905d4af297f6912bda6cce19"))
- .push_opcode(OP_EQUALVERIFY)
- .push_opcode(OP_CHECKSIG)
- .into_script();
- assert_eq!(
- script.to_hex_string_no_length_prefix(),
- "76a91416e1ae70ff0fa102905d4af297f6912bda6cce1988ac"
- );
-}
-
#[test]
fn script_builder_with_capacity() {
let script = Builder::<Tag>::with_capacity(42);
@@ -587,55 +569,6 @@ fn multisig() {
.is_multisig());
}
-#[test]
-#[cfg(feature = "serde")]
-fn script_json_serialize() {
- use serde_json;
-
- let original = ScriptBuf::from_hex_no_length_prefix("827651a0698faaa9a8a7a687").unwrap();
- let json = serde_json::to_value(&original).unwrap();
- assert_eq!(json, serde_json::Value::String("827651a0698faaa9a8a7a687".to_owned()));
- let des = serde_json::from_value::<ScriptBuf>(json).unwrap();
- assert_eq!(original, des);
-}
-
-#[test]
-fn script_asm() {
- assert_eq!(
- ScriptBuf::from_hex_no_length_prefix("6363636363686868686800").unwrap().to_string(),
- "OP_IF OP_IF OP_IF OP_IF OP_IF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_0"
- );
- assert_eq!(ScriptBuf::from_hex_no_length_prefix("2102715e91d37d239dea832f1460e91e368115d8ca6cc23a7da966795abad9e3b699ac").unwrap().to_string(),
- "OP_PUSHBYTES_33 02715e91d37d239dea832f1460e91e368115d8ca6cc23a7da966795abad9e3b699 OP_CHECKSIG");
- // Elements Alpha peg-out transaction with some signatures removed for brevity. Mainly to test PUSHDATA1
- assert_eq!(ScriptBuf::from_hex_no_length_prefix("0047304402202457e78cc1b7f50d0543863c27de75d07982bde8359b9e3316adec0aec165f2f02200203fd331c4e4a4a02f48cf1c291e2c0d6b2f7078a784b5b3649fca41f8794d401004cf1552103244e602b46755f24327142a0517288cebd159eccb6ccf41ea6edf1f601e9af952103bbbacc302d19d29dbfa62d23f37944ae19853cf260c745c2bea739c95328fcb721039227e83246bd51140fe93538b2301c9048be82ef2fb3c7fc5d78426ed6f609ad210229bf310c379b90033e2ecb07f77ecf9b8d59acb623ab7be25a0caed539e2e6472103703e2ed676936f10b3ce9149fa2d4a32060fb86fa9a70a4efe3f21d7ab90611921031e9b7c6022400a6bb0424bbcde14cff6c016b91ee3803926f3440abf5c146d05210334667f975f55a8455d515a2ef1c94fdfa3315f12319a14515d2a13d82831f62f57ae").unwrap().to_string(),
- "OP_0 OP_PUSHBYTES_71 304402202457e78cc1b7f50d0543863c27de75d07982bde8359b9e3316adec0aec165f2f02200203fd331c4e4a4a02f48cf1c291e2c0d6b2f7078a784b5b3649fca41f8794d401 OP_0 OP_PUSHDATA1 552103244e602b46755f24327142a0517288cebd159eccb6ccf41ea6edf1f601e9af952103bbbacc302d19d29dbfa62d23f37944ae19853cf260c745c2bea739c95328fcb721039227e83246bd51140fe93538b2301c9048be82ef2fb3c7fc5d78426ed6f609ad210229bf310c379b90033e2ecb07f77ecf9b8d59acb623ab7be25a0caed539e2e6472103703e2ed676936f10b3ce9149fa2d4a32060fb86fa9a70a4efe3f21d7ab90611921031e9b7c6022400a6bb0424bbcde14cff6c016b91ee3803926f3440abf5c146d05210334667f975f55a8455d515a2ef1c94fdfa3315f12319a14515d2a13d82831f62f57ae");
- // Various weird scripts found in transaction 6d7ed9914625c73c0288694a6819196a27ef6c08f98e1270d975a8e65a3dc09a
- // which triggered overflow bugs on 32-bit machines in script formatting in the past.
- assert_eq!(
- ScriptBuf::from_hex_no_length_prefix("01").unwrap().to_string(),
- "OP_PUSHBYTES_1 <push past end>"
- );
- assert_eq!(
- ScriptBuf::from_hex_no_length_prefix("0201").unwrap().to_string(),
- "OP_PUSHBYTES_2 <push past end>"
- );
- assert_eq!(ScriptBuf::from_hex_no_length_prefix("4c").unwrap().to_string(), "<unexpected end>");
- assert_eq!(
- ScriptBuf::from_hex_no_length_prefix("4c0201").unwrap().to_string(),
- "OP_PUSHDATA1 <push past end>"
- );
- assert_eq!(ScriptBuf::from_hex_no_length_prefix("4d").unwrap().to_string(), "<unexpected end>");
- assert_eq!(
- ScriptBuf::from_hex_no_length_prefix("4dffff01").unwrap().to_string(),
- "OP_PUSHDATA2 <push past end>"
- );
- assert_eq!(
- ScriptBuf::from_hex_no_length_prefix("4effffffff01").unwrap().to_string(),
- "OP_PUSHDATA4 <push past end>"
- );
-}
-
#[test]
fn script_buf_collect() {
assert_eq!(&core::iter::empty::<Instruction<'_>>().collect::<ScriptBuf>(), Script::new());
diff --git a/primitives/src/script/builder.rs b/primitives/src/script/builder.rs
index f903a2e5..b0cc69f3 100644
--- a/primitives/src/script/builder.rs
+++ b/primitives/src/script/builder.rs
@@ -81,3 +81,113 @@ impl<T> fmt::Display for Builder<T> {
impl<T> fmt::Debug for Builder<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(self, f) }
}
+
+#[cfg(test)]
+mod tests {
+ use alloc::{format, vec};
+
+ use super::Builder;
+ use crate::script::{PushBytes, ScriptSigTag as Tag};
+
+ #[test]
+ fn push_slice_minimal() {
+ let script = Builder::<Tag>::new().push_slice([0x81]).into_script();
+ assert_eq!(script.as_bytes(), &[0x4f]);
+
+ for n in 1u8..=16 {
+ let script = Builder::<Tag>::new().push_slice([n]).into_script();
+ assert_eq!(script.as_bytes(), &[0x50 + n]);
+ }
+
+ let script = Builder::<Tag>::new().push_slice([0u8]).into_script();
+ assert_eq!(script.as_bytes(), &[1, 0]);
+ let script = Builder::<Tag>::new().push_slice([17u8]).into_script();
+ assert_eq!(script.as_bytes(), &[1, 17]);
+ let script = Builder::<Tag>::new().push_slice(b"NRA4VR").into_script();
+ assert_eq!(script.as_bytes(), &[6, b'N', b'R', b'A', b'4', b'V', b'R']);
+ }
+
+ #[test]
+ fn push_slice_non_minimal() {
+ let script = Builder::<Tag>::new().push_slice_non_minimal([0x81]).into_script();
+ assert_eq!(script.as_bytes(), &[1, 0x81]);
+
+ let script = Builder::<Tag>::new().push_slice_non_minimal([1u8]).into_script();
+ assert_eq!(script.as_bytes(), &[1, 1]);
+ }
+
+ #[test]
+ fn push_slice_pushdata1_and_pushdata2() {
+ let script = Builder::<Tag>::new()
+ .push_slice(<&PushBytes>::try_from([0xab; 0x4b].as_slice()).unwrap())
+ .into_script();
+ assert_eq!(script.as_bytes()[0], 0x4b);
+ assert_eq!(script.len(), 1 + 0x4b);
+
+ let script = Builder::<Tag>::new()
+ .push_slice(<&PushBytes>::try_from([0xab; 0x4c].as_slice()).unwrap())
+ .into_script();
+ assert_eq!(&script.as_bytes()[..2], &[0x4c, 0x4c]);
+ assert_eq!(script.len(), 2 + 0x4c);
+
+ let script = Builder::<Tag>::new()
+ .push_slice(<&PushBytes>::try_from([0xab; 0xff].as_slice()).unwrap())
+ .into_script();
+ assert_eq!(&script.as_bytes()[..2], &[0x4c, 0xff]);
+ assert_eq!(script.len(), 2 + 0xff);
+
+ let script = Builder::<Tag>::new()
+ .push_slice(<&PushBytes>::try_from([0xab; 0x100].as_slice()).unwrap())
+ .into_script();
+ assert_eq!(&script.as_bytes()[..3], &[0x4d, 0x00, 0x01]);
+ assert_eq!(script.len(), 3 + 0x100);
+
+ let script = Builder::<Tag>::new()
+ .push_slice(<&PushBytes>::try_from([0xab; 0x102].as_slice()).unwrap())
+ .into_script();
+ assert_eq!(&script.as_bytes()[..3], &[0x4d, 0x02, 0x01]);
+ assert_eq!(script.len(), 3 + 0x102);
+
+ let script = Builder::<Tag>::new()
+ .push_slice(<&PushBytes>::try_from(vec![0xab; 0xffff].as_slice()).unwrap())
+ .into_script();
+ assert_eq!(&script.as_bytes()[..3], &[0x4d, 0xff, 0xff]);
+ assert_eq!(script.len(), 3 + 0xffff);
+ }
+
+ #[test]
+ #[cfg_attr(miri, ignore)]
+ fn push_slice_pushdata4_boundary() {
+ let script = Builder::<Tag>::new()
+ .push_slice(<&PushBytes>::try_from(vec![0u8; 0x10000].as_slice()).unwrap())
+ .into_script();
+ assert_eq!(&script.as_bytes()[..5], &[0x4e, 0x00, 0x00, 0x01, 0x00]);
+ assert_eq!(script.len(), 5 + 0x10000);
+ }
+
+ #[test]
+ #[cfg(target_pointer_width = "64")]
+ #[cfg_attr(miri, ignore)]
+ fn push_slice_pushdata4_length_bytes() {
+ let len = 0x0102_0304;
+ let script = Builder::<Tag>::new()
+ .push_slice(<&PushBytes>::try_from(vec![0u8; len].as_slice()).unwrap())
+ .into_script();
+ assert_eq!(&script.as_bytes()[..5], &[0x4e, 0x04, 0x03, 0x02, 0x01]);
+ assert_eq!(script.len(), 5 + len);
+ }
+
+ #[test]
+ fn from_vec() {
+ let script = Builder::<Tag>::from(vec![0xac, 0x51]).into_script();
+ assert_eq!(script.as_bytes(), &[0xac, 0x51]);
+ }
+
+ #[test]
+ fn display_delegates_to_script() {
+ let builder = Builder::<Tag>::from(vec![0x51, 0x52]);
+ let displayed = format!("{}", builder);
+ assert!(!displayed.is_empty());
+ assert_eq!(displayed, format!("{}", builder.as_script()));
+ }
+}
diff --git a/primitives/src/script/owned.rs b/primitives/src/script/owned.rs
index 81e51883..562a77d6 100644
--- a/primitives/src/script/owned.rs
+++ b/primitives/src/script/owned.rs
@@ -342,3 +342,34 @@ impl<'a, T> Arbitrary<'a> for ScriptBuf<T> {
Ok(Self::from_bytes(v))
}
}
+
+#[cfg(test)]
+mod tests {
+ use alloc::vec;
+
+ use super::ScriptBuf;
+ use crate::script::ScriptSigTag as Tag;
+
+ #[test]
+ fn reserved_len_for_slice() {
+ // Length plus the size of the push opcode that prefixes it.
+ assert_eq!(ScriptBuf::<Tag>::reserved_len_for_slice(0), 1);
+ assert_eq!(ScriptBuf::<Tag>::reserved_len_for_slice(0x4b), 0x4b + 1);
+ assert_eq!(ScriptBuf::<Tag>::reserved_len_for_slice(0x4c), 0x4c + 2);
+ assert_eq!(ScriptBuf::<Tag>::reserved_len_for_slice(0xff), 0xff + 2);
+ assert_eq!(ScriptBuf::<Tag>::reserved_len_for_slice(0x100), 0x100 + 3);
+ assert_eq!(ScriptBuf::<Tag>::reserved_len_for_slice(0xffff), 0xffff + 3);
+ assert_eq!(ScriptBuf::<Tag>::reserved_len_for_slice(0x10000), 0x10000 + 5);
+ }
+
+ #[test]
+ fn as_byte_vec_deref_restores() {
+ let mut script = ScriptBuf::<Tag>::from_bytes(vec![1, 2, 3]);
+ {
+ let vec = script.as_byte_vec();
+ assert_eq!(vec.len(), 3);
+ assert_eq!(vec.as_slice(), &[1, 2, 3]);
+ }
+ assert_eq!(script.as_bytes(), &[1, 2, 3]);
+ }
+}
diff --git a/primitives/src/script/tests.rs b/primitives/src/script/tests.rs
index f5b4286d..bfd74296 100644
--- a/primitives/src/script/tests.rs
+++ b/primitives/src/script/tests.rs
@@ -186,6 +186,78 @@ fn script_is_empty() {
assert!(!script.is_empty());
}
+#[test]
+#[cfg(feature = "hex")]
+fn script_builder() {
+ use hex::hex;
+
+ use crate::opcodes::all::{OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160};
+
+ // from txid 3bb5e6434c11fb93f64574af5d116736510717f2c595eb45b52c28e31622dfff which was in my mempool when I wrote the test
+ let script = Builder::<ScriptPubKeyTag>::new()
+ .push_opcode(OP_DUP)
+ .push_opcode(OP_HASH160)
+ .push_slice(hex!("16e1ae70ff0fa102905d4af297f6912bda6cce19"))
+ .push_opcode(OP_EQUALVERIFY)
+ .push_opcode(OP_CHECKSIG)
+ .into_script();
+ assert_eq!(
+ script.to_hex_string_no_length_prefix(),
+ "76a91416e1ae70ff0fa102905d4af297f6912bda6cce1988ac"
+ );
+}
+
+#[test]
+#[cfg(feature = "hex")]
+#[cfg(feature = "serde")]
+fn script_json_serialize() {
+ use serde_json;
+
+ let original = ScriptBuf::from_hex_no_length_prefix("827651a0698faaa9a8a7a687").unwrap();
+ let json = serde_json::to_value(&original).unwrap();
+ assert_eq!(json, serde_json::Value::String("827651a0698faaa9a8a7a687".to_owned()));
+ let des = serde_json::from_value::<ScriptBuf>(json).unwrap();
+ assert_eq!(original, des);
+}
+
+#[test]
+#[cfg(feature = "hex")]
+fn script_asm() {
+ assert_eq!(
+ ScriptBuf::from_hex_no_length_prefix("6363636363686868686800").unwrap().to_string(),
+ "OP_IF OP_IF OP_IF OP_IF OP_IF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_ENDIF OP_0"
+ );
+ assert_eq!(ScriptBuf::from_hex_no_length_prefix("2102715e91d37d239dea832f1460e91e368115d8ca6cc23a7da966795abad9e3b699ac").unwrap().to_string(),
+ "OP_PUSHBYTES_33 02715e91d37d239dea832f1460e91e368115d8ca6cc23a7da966795abad9e3b699 OP_CHECKSIG");
+ // Elements Alpha peg-out transaction with some signatures removed for brevity. Mainly to test PUSHDATA1
+ assert_eq!(ScriptBuf::from_hex_no_length_prefix("0047304402202457e78cc1b7f50d0543863c27de75d07982bde8359b9e3316adec0aec165f2f02200203fd331c4e4a4a02f48cf1c291e2c0d6b2f7078a784b5b3649fca41f8794d401004cf1552103244e602b46755f24327142a0517288cebd159eccb6ccf41ea6edf1f601e9af952103bbbacc302d19d29dbfa62d23f37944ae19853cf260c745c2bea739c95328fcb721039227e83246bd51140fe93538b2301c9048be82ef2fb3c7fc5d78426ed6f609ad210229bf310c379b90033e2ecb07f77ecf9b8d59acb623ab7be25a0caed539e2e6472103703e2ed676936f10b3ce9149fa2d4a32060fb86fa9a70a4efe3f21d7ab90611921031e9b7c6022400a6bb0424bbcde14cff6c016b91ee3803926f3440abf5c146d05210334667f975f55a8455d515a2ef1c94fdfa3315f12319a14515d2a13d82831f62f57ae").unwrap().to_string(),
+ "OP_0 OP_PUSHBYTES_71 304402202457e78cc1b7f50d0543863c27de75d07982bde8359b9e3316adec0aec165f2f02200203fd331c4e4a4a02f48cf1c291e2c0d6b2f7078a784b5b3649fca41f8794d401 OP_0 OP_PUSHDATA1 552103244e602b46755f24327142a0517288cebd159eccb6ccf41ea6edf1f601e9af952103bbbacc302d19d29dbfa62d23f37944ae19853cf260c745c2bea739c95328fcb721039227e83246bd51140fe93538b2301c9048be82ef2fb3c7fc5d78426ed6f609ad210229bf310c379b90033e2ecb07f77ecf9b8d59acb623ab7be25a0caed539e2e6472103703e2ed676936f10b3ce9149fa2d4a32060fb86fa9a70a4efe3f21d7ab90611921031e9b7c6022400a6bb0424bbcde14cff6c016b91ee3803926f3440abf5c146d05210334667f975f55a8455d515a2ef1c94fdfa3315f12319a14515d2a13d82831f62f57ae");
+ // Various weird scripts found in transaction 6d7ed9914625c73c0288694a6819196a27ef6c08f98e1270d975a8e65a3dc09a
+ // which triggered overflow bugs on 32-bit machines in script formatting in the past.
+ assert_eq!(
+ ScriptBuf::from_hex_no_length_prefix("01").unwrap().to_string(),
+ "OP_PUSHBYTES_1 <push past end>"
+ );
+ assert_eq!(
+ ScriptBuf::from_hex_no_length_prefix("0201").unwrap().to_string(),
+ "OP_PUSHBYTES_2 <push past end>"
+ );
+ assert_eq!(ScriptBuf::from_hex_no_length_prefix("4c").unwrap().to_string(), "<unexpected end>");
+ assert_eq!(
+ ScriptBuf::from_hex_no_length_prefix("4c0201").unwrap().to_string(),
+ "OP_PUSHDATA1 <push past end>"
+ );
+ assert_eq!(ScriptBuf::from_hex_no_length_prefix("4d").unwrap().to_string(), "<unexpected end>");
+ assert_eq!(
+ ScriptBuf::from_hex_no_length_prefix("4dffff01").unwrap().to_string(),
+ "OP_PUSHDATA2 <push past end>"
+ );
+ assert_eq!(
+ ScriptBuf::from_hex_no_length_prefix("4effffffff01").unwrap().to_string(),
+ "OP_PUSHDATA4 <push past end>"
+ );
+}
+
#[test]
fn test_index() {
let script = Script::from_bytes(&[1, 2, 3, 4, 5]);
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.