Fix script::Builder::push_verify() following a push_int()
What changed, and why it matters
This commit fixes a bug in the Rust Bitcoin library's script builder. When building a Bitcoin script, after pushing a number onto the stack, the builder would incorrectly remember the last opcode it had seen. This caused a later call to `push_verify()` to try to combine with that remembered opcode, producing an invalid or unexpected script. The fix clears that remembered state whenever an integer is pushed, and adds a test to prevent regression.
Review any code using `Builder::push_int` followed by `push_verify` to confirm the generated scripts now match intent. Update to a release containing this fix. Consider auditing downstream consumers that build scripts dynamically.
Security signals we found
Script construction correctness bug
Builder state not invalidated after push_int
push_verify optimization could mispair with stale opcode
Potential for producing non-standard or invalid Bitcoin scripts
No explicit security disclosure in commit or references
Evidence from the diff
In script::Builder, the second tuple field (self.1) tracks the last pushed opcode so that push_verify() can optimize sequences like OP_EQUAL OP_VERIFY into OP_EQUALVERIFY. The bug: push_int(), push_int_unchecked(), and push_int_non_minimal() did not reset self.1, so a subsequent push_verify() would treat the previous opcode as combinable. For example, Builder::new().push_opcode(OP_EQUAL).push_int(5).push_verify() previously emitted 875569 only after the fix (per the new test). The patch resets self.1 = None in all three integer push methods.
Changed components
bitcoin/src/blockdata/script/builder.rsscript::Builder::push_intscript::Builder::push_int_uncheckedscript::Builder::push_int_non_minimalscript::Builder::push_verifyInspect captured patch +11 / −1
diff --git a/bitcoin/src/blockdata/script/builder.rs b/bitcoin/src/blockdata/script/builder.rs
index 90f21836..a00f76f4 100644
--- a/bitcoin/src/blockdata/script/builder.rs
+++ b/bitcoin/src/blockdata/script/builder.rs
@@ -43,7 +43,11 @@ impl<T> Builder<T> {
/// # Errors
///
/// Only errors if `data == i32::MIN` (CScriptNum cannot have value -2^31).
- pub fn push_int(mut self, n: i32) -> Result<Self, Error> { self.0.push_int(n).map(|_| self) }
+ pub fn push_int(mut self, n: i32) -> Result<Self, Error> {
+ self.0.push_int(n)?;
+ self.1 = None;
+ Ok(self)
+ }
/// Adds instructions to push an unchecked integer onto the stack.
///
@@ -62,6 +66,7 @@ impl<T> Builder<T> {
/// Does not check whether `n` is in the range of [-2^31 +1...2^31 -1].
pub fn push_int_unchecked(mut self, n: i64) -> Self {
self.0.push_int_unchecked(n);
+ self.1 = None;
self
}
@@ -70,6 +75,7 @@ impl<T> Builder<T> {
/// This uses the explicit encoding regardless of the availability of dedicated opcodes.
pub(in crate::blockdata) fn push_int_non_minimal(mut self, data: i64) -> Self {
self.0.push_int_non_minimal(data);
+ self.1 = None;
self
}
diff --git a/bitcoin/src/blockdata/script/tests.rs b/bitcoin/src/blockdata/script/tests.rs
index 276c8630..bb5221ca 100644
--- a/bitcoin/src/blockdata/script/tests.rs
+++ b/bitcoin/src/blockdata/script/tests.rs
@@ -352,6 +352,10 @@ fn script_builder_verify() {
assert_eq!(trick_slice.to_hex_string_no_length_prefix(), "01ae69");
let trick_slice2 = Builder::from(vec![0x01, 0xae]).push_verify().into_script();
assert_eq!(trick_slice2.to_hex_string_no_length_prefix(), "01ae69");
+
+ let pushint_then_verify =
+ Builder::new().push_opcode(OP_EQUAL).push_int(5).unwrap().push_verify().into_script();
+ assert_eq!(pushint_then_verify.to_hex_string_no_length_prefix(), "875569");
}
#[test]
Why this scored 47/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.