Move ScriptBuf extension trait functions to primitives
What changed, and why it matters
This commit is a routine internal code reorganization. It moves some helper methods for building Bitcoin scripts from one internal module to another, without changing what the code actually does. There is no security-relevant change visible in the diff.
No security action required. Review as normal refactoring if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change relocates push_opcode, push_slice, push_slice_non_minimal, and supporting private helpers (reserved_len_for_slice, as_byte_vec, push_slice_no_opt, ScriptBufAsVec) from the bitcoin crate’s ScriptBufExt extension trait into inherent impl methods on ScriptBuf<T> in the primitives crate. The implementation logic is copied verbatim, including the numeric-to-opcode optimization and push-data encoding. No behavior, API surface, or trust boundary changes are introduced.
Changed components
bitcoin/src/blockdata/script/owned.rsprimitives/src/script/owned.rsInspect captured patch +120 / −38
diff --git a/bitcoin/src/blockdata/script/owned.rs b/bitcoin/src/blockdata/script/owned.rs
index ce9b4545..8e14d3da 100644
--- a/bitcoin/src/blockdata/script/owned.rs
+++ b/bitcoin/src/blockdata/script/owned.rs
@@ -68,44 +68,6 @@ internal_macros::define_extension_trait! {
}
}
- /// Adds a single opcode to the script.
- fn push_opcode(&mut self, data: Opcode) { self.as_byte_vec().push(data.to_u8()); }
-
- /// Adds instructions to push some arbitrary data onto the stack.
- ///
- /// If the data can be exactly produced by a numeric opcode, that opcode
- /// will be used, since its behavior is equivalent but will not violate minimality
- /// rules. To avoid this, use [`ScriptBuf::push_slice_non_minimal`] which will always
- /// use a push opcode.
- ///
- /// However, this method does *not* enforce any numeric minimality rules.
- /// If your pushes should be interpreted as numbers, ensure your input does
- /// not have any leading zeros. In particular, the number 0 should be encoded
- /// as an empty string rather than as a single 0 byte.
- fn push_slice<D: AsRef<PushBytes>>(&mut self, data: D) {
- let bytes = data.as_ref().as_bytes();
- if bytes.len() == 1 {
- match bytes[0] {
- 0x81 => { self.push_opcode(OP_1NEGATE); },
- 1..=16 => { self.push_opcode(Opcode::from(bytes[0] + (OP_1.to_u8() - 1))); },
- _ => { self.push_slice_non_minimal(data); },
- }
- } else {
- self.push_slice_non_minimal(data);
- }
- }
-
- /// Adds instructions to push some arbitrary data onto the stack without minimality.
- ///
- /// Standardness rules require push minimality according to [CheckMinimalPush] of core.
- ///
- /// [CheckMinimalPush]: <https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/script.cpp#L366>
- fn push_slice_non_minimal<D: AsRef<PushBytes>>(&mut self, data: D) {
- let data = data.as_ref();
- self.reserve(Self::reserved_len_for_slice(data.len()));
- self.push_slice_no_opt(data);
- }
-
/// Add a single instruction to the script.
///
/// # Panics
diff --git a/primitives/src/script/owned.rs b/primitives/src/script/owned.rs
index b054fd64..4236323c 100644
--- a/primitives/src/script/owned.rs
+++ b/primitives/src/script/owned.rs
@@ -8,7 +8,10 @@ use arbitrary::{Arbitrary, Unstructured};
use encoding::{ByteVecDecoder, DecoderStatus};
use super::{Script, ScriptBufDecoderError};
+use crate::opcodes::all::{OP_1, OP_1NEGATE};
+use crate::opcodes::{self, Opcode};
use crate::prelude::{Box, Vec};
+use crate::script::PushBytes;
/// An owned, growable script.
///
@@ -160,6 +163,99 @@ impl<T> ScriptBuf<T> {
#[inline]
#[deprecated(since = "1.0.0-rc.0", note = "use `format!(\"{var:x}\")` instead")]
pub fn to_hex(&self) -> alloc::string::String { alloc::format!("{:x}", self) }
+
+ /// Adds a single opcode to the script.
+ pub fn push_opcode(&mut self, data: Opcode) { self.as_byte_vec().push(data.to_u8()); }
+
+ /// Adds instructions to push some arbitrary data onto the stack.
+ ///
+ /// If the data can be exactly produced by a numeric opcode, that opcode
+ /// will be used, since its behavior is equivalent but will not violate minimality
+ /// rules. To avoid this, use [`ScriptBuf::push_slice_non_minimal`] which will always
+ /// use a push opcode.
+ ///
+ /// However, this method does *not* enforce any numeric minimality rules.
+ /// If your pushes should be interpreted as numbers, ensure your input does
+ /// not have any leading zeros. In particular, the number 0 should be encoded
+ /// as an empty string rather than as a single 0 byte.
+ pub fn push_slice<D: AsRef<PushBytes>>(&mut self, data: D) {
+ let bytes = data.as_ref().as_bytes();
+ if bytes.len() == 1 {
+ match bytes[0] {
+ 0x81 => {
+ self.push_opcode(OP_1NEGATE);
+ }
+ 1..=16 => {
+ self.push_opcode(Opcode::from(bytes[0] + (OP_1.to_u8() - 1)));
+ }
+ _ => {
+ self.push_slice_non_minimal(data);
+ }
+ }
+ } else {
+ self.push_slice_non_minimal(data);
+ }
+ }
+
+ /// Adds instructions to push some arbitrary data onto the stack without minimality.
+ ///
+ /// Standardness rules require push minimality according to [CheckMinimalPush] of core.
+ ///
+ /// [CheckMinimalPush]: <https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/script.cpp#L366>
+ pub fn push_slice_non_minimal<D: AsRef<PushBytes>>(&mut self, data: D) {
+ let data = data.as_ref();
+ self.reserve(Self::reserved_len_for_slice(data.len()));
+ self.push_slice_no_opt(data);
+ }
+
+ /// Computes the sum of `len` and the length of an appropriate push opcode.
+ fn reserved_len_for_slice(len: usize) -> usize {
+ len + match len {
+ 0..=0x4b => 1,
+ 0x4c..=0xff => 2,
+ 0x100..=0xffff => 3,
+ // we don't care about oversized, the other fn will panic anyway
+ _ => 5,
+ }
+ }
+
+ /// Pretends to convert `&mut ScriptBuf` to `&mut Vec<u8>` so that it can be modified.
+ ///
+ /// Note: if the returned value leaks the original `ScriptBuf` will become empty.
+ fn as_byte_vec(&mut self) -> ScriptBufAsVec<'_, T> {
+ let vec = core::mem::take(self).into_bytes();
+ ScriptBufAsVec(self, vec)
+ }
+
+ /// Pushes the slice without reserving
+ fn push_slice_no_opt(&mut self, data: &PushBytes) {
+ let mut this = self.as_byte_vec();
+ // Start with a PUSH opcode
+ match data.len() as u64 {
+ n if n < opcodes::OP_PUSHDATA1.into() => {
+ this.push(n as u8);
+ }
+ n if n < 0x100 => {
+ this.push(opcodes::OP_PUSHDATA1);
+ this.push(n as u8);
+ }
+ n if n < 0x10000 => {
+ this.push(opcodes::OP_PUSHDATA2);
+ this.push((n % 0x100) as u8);
+ this.push((n / 0x100) as u8);
+ }
+ // `PushBytes` enforces len < 0x100000000
+ n => {
+ this.push(opcodes::OP_PUSHDATA4);
+ this.push((n % 0x100) as u8);
+ this.push(((n / 0x100) % 0x100) as u8);
+ this.push(((n / 0x10000) % 0x100) as u8);
+ this.push((n / 0x1000000) as u8);
+ }
+ }
+ // Then push the raw bytes
+ this.extend_from_slice(data.as_bytes());
+ }
}
// Cannot derive due to generics.
@@ -214,6 +310,30 @@ impl<T> encoding::Decoder for ScriptBufDecoder<T> {
fn read_limit(&self) -> usize { self.0.read_limit() }
}
+/// Pretends that this is a mutable reference to [`ScriptBuf`]'s internal buffer.
+///
+/// In reality the backing `Vec<u8>` is swapped with an empty one and this is holding both the
+/// reference and the vec. The vec is put back when this drops so it also covers panics. (But not
+/// leaks, which is OK since we never leak.)
+pub(crate) struct ScriptBufAsVec<'a, T>(&'a mut ScriptBuf<T>, Vec<u8>);
+
+impl<T> core::ops::Deref for ScriptBufAsVec<'_, T> {
+ type Target = Vec<u8>;
+
+ fn deref(&self) -> &Self::Target { &self.1 }
+}
+
+impl<T> core::ops::DerefMut for ScriptBufAsVec<'_, T> {
+ fn deref_mut(&mut self) -> &mut Self::Target { &mut self.1 }
+}
+
+impl<T> Drop for ScriptBufAsVec<'_, T> {
+ fn drop(&mut self) {
+ let vec = core::mem::take(&mut self.1);
+ *(self.0) = ScriptBuf::from_bytes(vec);
+ }
+}
+
#[cfg(feature = "arbitrary")]
impl<'a, T> Arbitrary<'a> for ScriptBuf<T> {
#[inline]
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.