bitcoin: add `push_int` and friends to `ScriptBuf`
What changed, and why it matters
This commit is a straightforward code reorganization. It moves functions that add integer-pushing instructions to Bitcoin scripts from one internal component (`script::Builder`) to another (`ScriptBuf`), then makes `Builder` call the new location. No behavior changes are visible to users, and no security bug is being fixed.
No security action needed; review as normal refactoring.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch relocates push_int, push_int_non_minimal, and push_int_unchecked implementations from script::Builder to an extension trait on ScriptBuf. Builder methods are rewritten as thin wrappers delegating to ScriptBuf. The logic, error handling (rejecting i32::MIN), opcode selection, and non-minimal encoding via write_scriptint are preserved exactly. New unit tests verify the moved functions produce the same byte sequences.
Changed components
bitcoin/src/blockdata/script/builder.rsbitcoin/src/blockdata/script/owned.rsbitcoin/src/blockdata/script/tests.rsInspect captured patch +95 / −22
diff --git a/bitcoin/src/blockdata/script/builder.rs b/bitcoin/src/blockdata/script/builder.rs
index 1a3109f6..2a479d6a 100644
--- a/bitcoin/src/blockdata/script/builder.rs
+++ b/bitcoin/src/blockdata/script/builder.rs
@@ -2,7 +2,7 @@
use core::fmt;
-use super::{opcode_to_verify, write_scriptint, Error, PushBytes, Script, ScriptBuf};
+use super::{opcode_to_verify, Error, PushBytes, Script, ScriptBuf};
use crate::key::{PublicKey, XOnlyPublicKey};
use crate::locktime::absolute;
use crate::opcodes::all::*;
@@ -38,14 +38,7 @@ impl<T> Builder<T> {
/// # Errors
///
/// Only errors if `data == i32::MIN` (CScriptNum cannot have value -2^31).
- pub fn push_int(self, n: i32) -> Result<Self, Error> {
- if n == i32::MIN {
- // ref: https://github.com/bitcoin/bitcoin/blob/cac846c2fbf6fc69bfc288fd387aa3f68d84d584/src/script/script.h#L230
- Err(Error::NumericOverflow)
- } else {
- Ok(self.push_int_unchecked(n.into()))
- }
- }
+ pub fn push_int(mut self, n: i32) -> Result<Self, Error> { self.0.push_int(n).map(|_| self) }
/// Adds instructions to push an unchecked integer onto the stack.
///
@@ -62,22 +55,17 @@ impl<T> Builder<T> {
/// > throwing an exception if arithmetic is done or the result is interpreted as an integer.
///
/// Does not check whether `n` is in the range of [-2^31 +1...2^31 -1].
- pub fn push_int_unchecked(self, n: i64) -> Self {
- match n {
- -1 => self.push_opcode(OP_PUSHNUM_NEG1),
- 0 => self.push_opcode(OP_PUSHBYTES_0),
- 1..=16 => self.push_opcode(Opcode::from(n as u8 + (OP_PUSHNUM_1.to_u8() - 1))),
- _ => self.push_int_non_minimal(n),
- }
+ pub fn push_int_unchecked(mut self, n: i64) -> Self {
+ self.0.push_int_unchecked(n);
+ self
}
/// Adds instructions to push an integer onto the stack without optimization.
///
/// This uses the explicit encoding regardless of the availability of dedicated opcodes.
- pub(in crate::blockdata) fn push_int_non_minimal(self, data: i64) -> Self {
- let mut buf = [0u8; 8];
- let len = write_scriptint(&mut buf, data);
- self.push_slice_non_minimal(&<&PushBytes>::from(&buf)[..len])
+ pub(in crate::blockdata) fn push_int_non_minimal(mut self, data: i64) -> Self {
+ self.0.push_int_non_minimal(data);
+ self
}
/// Adds instructions to push some arbitrary data onto the stack.
diff --git a/bitcoin/src/blockdata/script/owned.rs b/bitcoin/src/blockdata/script/owned.rs
index b359182b..ef8fade5 100644
--- a/bitcoin/src/blockdata/script/owned.rs
+++ b/bitcoin/src/blockdata/script/owned.rs
@@ -8,8 +8,8 @@ use internals::ToU64 as _;
use secp256k1::{Secp256k1, Verification};
use super::{
- opcode_to_verify, Builder, Instruction, PushBytes, ScriptBuf, ScriptExtPriv as _,
- ScriptPubKeyBuf,
+ opcode_to_verify, write_scriptint, Builder, Error, Instruction, PushBytes, ScriptBuf,
+ ScriptExtPriv as _, ScriptPubKeyBuf,
};
use crate::key::{
PubkeyHash, PublicKey, TapTweak, TweakedPublicKey, UntweakedPublicKey, WPubkeyHash,
@@ -29,6 +29,48 @@ internal_macros::define_extension_trait! {
/// Constructs a new script builder
fn builder() -> Builder<T> { Builder::new() }
+ /// Adds instructions to push an integer onto the stack.
+ ///
+ /// Integers are encoded as little-endian signed-magnitude numbers, but there are dedicated
+ /// opcodes to push some small integers.
+ ///
+ /// # Errors
+ ///
+ /// Only errors if `data == i32::MIN` (CScriptNum cannot have value -2^31).
+ fn push_int(&mut self, n: i32) -> Result<(), Error> {
+ if n == i32::MIN {
+ // ref: https://github.com/bitcoin/bitcoin/blob/cac846c2fbf6fc69bfc288fd387aa3f68d84d584/src/script/script.h#L230
+ Err(Error::NumericOverflow)
+ } else {
+ self.push_int_unchecked(n.into());
+ Ok(())
+ }
+ }
+
+ /// Adds instructions to push an unchecked integer onto the stack.
+ ///
+ /// Integers are encoded as little-endian signed-magnitude numbers, but there are dedicated
+ /// opcodes to push some small integers.
+ ///
+ /// This function implements `CScript::push_int64` from Core `script.h`.
+ ///
+ /// > Numeric opcodes (OP_1ADD, etc) are restricted to operating on 4-byte integers.
+ /// > The semantics are subtle, though: operands must be in the range [-2^31 +1...2^31 -1],
+ /// > but results may overflow (and are valid as long as they are not used in a subsequent
+ /// > numeric operation). CScriptNum enforces those semantics by storing results as
+ /// > an int64 and allowing out-of-range values to be returned as a vector of bytes but
+ /// > throwing an exception if arithmetic is done or the result is interpreted as an integer.
+ ///
+ /// Does not check whether `n` is in the range of [-2^31 +1...2^31 -1].
+ fn push_int_unchecked(&mut self, n: i64) {
+ match n {
+ -1 => self.push_opcode(OP_PUSHNUM_NEG1),
+ 0 => self.push_opcode(OP_PUSHBYTES_0),
+ 1..=16 => self.push_opcode(Opcode::from(n as u8 + (OP_PUSHNUM_1.to_u8() - 1))),
+ _ => self.push_int_non_minimal(n),
+ }
+ }
+
/// Adds a single opcode to the script.
fn push_opcode(&mut self, data: Opcode) { self.as_byte_vec().push(data.to_u8()); }
@@ -275,6 +317,16 @@ internal_macros::define_extension_trait! {
None => self.push_opcode(OP_VERIFY),
}
}
+
+ /// Adds instructions to push an integer onto the stack without optimization.
+ ///
+ /// This uses the explicit encoding regardless of the availability of dedicated opcodes.
+ fn push_int_non_minimal(&mut self, data: i64) {
+ let mut buf = [0u8; 8];
+ let len = write_scriptint(&mut buf, data);
+ self.reserve(Self::reserved_len_for_slice(len));
+ self.push_slice_no_opt(&<&PushBytes>::from(&buf)[..len]);
+ }
}
}
diff --git a/bitcoin/src/blockdata/script/tests.rs b/bitcoin/src/blockdata/script/tests.rs
index f4c8fa92..f621603e 100644
--- a/bitcoin/src/blockdata/script/tests.rs
+++ b/bitcoin/src/blockdata/script/tests.rs
@@ -70,6 +70,39 @@ fn script() {
script = script.push_opcode(OP_CHECKSIG); comp.push(0xACu8); assert_eq!(script.as_bytes(), &comp[..]);
}
+#[test]
+#[rustfmt::skip]
+fn script_buf_push_int() {
+ let mut comp = vec![];
+ let mut script = ScriptBuf::new();
+ assert_eq!(script.as_bytes(), &comp[..]);
+
+ // small ints
+ script.push_int_unchecked(1); comp.push(81u8); assert_eq!(script.as_bytes(), &comp[..]);
+ script.push_int_unchecked(0); comp.push(0u8); assert_eq!(script.as_bytes(), &comp[..]);
+ script.push_int_unchecked(4); comp.push(84u8); assert_eq!(script.as_bytes(), &comp[..]);
+ script.push_int_unchecked(-1); comp.push(79u8); assert_eq!(script.as_bytes(), &comp[..]);
+ // forced scriptint
+ script.push_int_non_minimal(4); comp.extend([1u8, 4].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+ // big ints
+ script.push_int_unchecked(17); comp.extend([1u8, 17].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+ script.push_int_unchecked(10000); comp.extend([2u8, 16, 39].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+ // notice the sign bit set here, hence the extra zero/128 at the end
+ script.push_int_unchecked(10000000); comp.extend([4u8, 128, 150, 152, 0].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+ script.push_int_unchecked(-10000000); comp.extend([4u8, 128, 150, 152, 128].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+
+ script.push_int(0).unwrap(); comp.extend([0u8].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+ script.push_int_non_minimal(0); comp.extend([0u8].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+ // OP_1..16
+ for n in 1..=16 {
+ script.push_int(n.into()).unwrap(); comp.extend([0x50 + n].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+ script.push_int_non_minimal(n.into()); comp.extend([1, n].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+ }
+
+ script.push_int(-1).unwrap(); comp.extend([0x4f].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+ script.push_int_non_minimal(-1); comp.extend([1, 0x81].iter().cloned()); assert_eq!(script.as_bytes(), &comp[..]);
+}
+
#[test]
fn p2pk_pubkey_bytes_valid_key_and_valid_script_returns_expected_key() {
let key_str = "0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3";
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.