Split PushBytes into an extension trait
What changed, and why it matters
This commit is a routine internal code reorganization. It moves two existing helper methods (read_scriptint and read_cltv_scriptint) out of the PushBytes type into a Rust 'extension trait', and turns their shared internal helper into a plain private function. No behavior changes, bug fixes, or security-sensitive logic changes are visible in the diff.
No security action required. Treat as normal refactoring; standard code review and CI pass are sufficient.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change refactors PushBytes in rust-bitcoin. It introduces a PushBytesExt extension trait (via define_extension_trait!) that holds read_scriptint and read_cltv_scriptint, re-exports it, and converts read_scriptint_internal from an inherent method to a private free function taking &PushBytes. The actual parsing logic, overflow checks, non-minimal encoding checks, and calls to script::scriptint_parse are unchanged. Call sites are updated to import PushBytesExt as _ so the methods remain available through the trait.
Changed components
bitcoin/src/blockdata/script/push_bytes.rsbitcoin/src/blockdata/script/mod.rsbitcoin/src/blockdata/script/instruction.rsbitcoin/src/blockdata/block.rsbitcoin/src/lib.rsInspect captured patch +80 / −70
diff --git a/bitcoin/src/blockdata/block.rs b/bitcoin/src/blockdata/block.rs
index 0fe468d2..996f3620 100644
--- a/bitcoin/src/blockdata/block.rs
+++ b/bitcoin/src/blockdata/block.rs
@@ -16,7 +16,7 @@ use crate::merkle_tree::{TxMerkleNode, WitnessMerkleNode};
use crate::network::Params;
use crate::pow::TargetExt as _;
use crate::prelude::Vec;
-use crate::script::ScriptExt as _;
+use crate::script::{PushBytesExt as _, ScriptExt as _};
use crate::transaction::{Coinbase, Transaction, TransactionExt as _};
use crate::{internal_macros, BlockTime, Target, Weight, Work};
diff --git a/bitcoin/src/blockdata/script/instruction.rs b/bitcoin/src/blockdata/script/instruction.rs
index e7037591..74481aba 100644
--- a/bitcoin/src/blockdata/script/instruction.rs
+++ b/bitcoin/src/blockdata/script/instruction.rs
@@ -2,7 +2,7 @@
use internals::script::{self, PushDataLenLen};
-use super::{Error, PushBytes, Script, ScriptBufExtPriv as _};
+use super::{Error, PushBytes, PushBytesExt as _, Script, ScriptBufExtPriv as _};
use crate::opcodes::{self, Opcode};
/// A "parsed opcode" which allows iterating over a [`Script`] in a more sensible way.
diff --git a/bitcoin/src/blockdata/script/mod.rs b/bitcoin/src/blockdata/script/mod.rs
index cea6378c..d7a2d951 100644
--- a/bitcoin/src/blockdata/script/mod.rs
+++ b/bitcoin/src/blockdata/script/mod.rs
@@ -72,7 +72,7 @@ pub use self::{
builder::Builder,
instruction::{Instruction, Instructions, InstructionIndices},
owned::{ScriptBufExt, ScriptPubKeyBufExt, ScriptSigBufExt},
- push_bytes::{PushBytes, PushBytesBuf, PushBytesErrorReport},
+ push_bytes::{PushBytes, PushBytesBuf, PushBytesExt, PushBytesErrorReport},
};
#[doc(no_inline)]
pub use primitives::script::ScriptBufDecoderError;
diff --git a/bitcoin/src/blockdata/script/push_bytes.rs b/bitcoin/src/blockdata/script/push_bytes.rs
index 43265c11..9e0fbe79 100644
--- a/bitcoin/src/blockdata/script/push_bytes.rs
+++ b/bitcoin/src/blockdata/script/push_bytes.rs
@@ -294,78 +294,88 @@ impl PushBytes {
/// Returns true if the buffer contains zero bytes.
pub fn is_empty(&self) -> bool { self.as_bytes().is_empty() }
+}
- /// Decodes an integer in script(minimal CScriptNum) format.
- ///
- /// This code is based on the
- /// [`CScriptNum` constructor in Bitcoin Core](https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/script.h#L245)
- ///
- /// # Errors
- ///
- /// * [`ScriptIntError::NumericOverflow`] if result is not in range [-2^39 +1...2^39 -1].
- /// * [`ScriptIntError::NonMinimal`] if encoding is non-minimal.
- pub fn read_scriptint(&self) -> Result<i32, ScriptIntError> {
- // Cast is safe, since the function already checks for byte length > 4
- let ret = self.read_scriptint_internal(4)?;
- Ok(i32::try_from(ret).expect("4 bytes or less fits in an i32"))
- }
+crate::internal_macros::define_extension_trait! {
+ /// Extension functionality for the [`PushBytes`] type.
+ pub trait PushBytesExt impl for PushBytes {
+ /// Decodes an integer in script(minimal CScriptNum) format.
+ ///
+ /// This code is based on the
+ /// [`CScriptNum` constructor in Bitcoin Core](https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/script.h#L245)
+ ///
+ /// # Errors
+ ///
+ /// * [`ScriptIntError::NumericOverflow`] if result is not in range [-2^39 +1...2^39 -1].
+ /// * [`ScriptIntError::NonMinimal`] if encoding is non-minimal.
+ fn read_scriptint(&self) -> Result<i32, ScriptIntError> {
+ // Cast is safe, since the function already checks for byte length > 4
+ let ret = read_scriptint_internal(self, 4)?;
+ Ok(i32::try_from(ret).expect("4 bytes or less fits in an i32"))
+ }
- /// Decodes an integer in script(minimal CScriptNum) format.
- ///
- /// This is suitable to read input values for CHECKLOCKTIMEVERIFY instructions.
- ///
- /// Notice that this fails on overflow: the result is the same as in bitcoind, that only 4-byte
- /// signed-magnitude values may be read as numbers. They can be added or subtracted (and a long
- /// time ago, multiplied and divided), and this may result in numbers which can't be written out
- /// in 4 bytes or less. This is ok! The number just can't be read as a number again. This is a
- /// bit crazy and subtle, but it makes sense: you can load 32-bit numbers and do anything with
- /// them, which back when mult/div was allowed, could result in up to a 64-bit number. We don't
- /// want overflow since that's surprising --- and we don't want numbers that don't fit in 64
- /// bits (for efficiency on modern processors). This function will return any value up to 40
- /// bits in length. This is basically a ranged type implementation.
- ///
- /// This code is based on the
- /// [`CScriptNum` constructor in Bitcoin Core](https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/script.h#L245)
- ///
- /// # Errors
- ///
- /// * [`ScriptIntError::NumericOverflow`] if result is not in range [-2^39 +1...2^39 -1].
- /// * [`ScriptIntError::NonMinimal`] if encoding is non-minimal.
- pub fn read_cltv_scriptint(&self) -> Result<i64, ScriptIntError> {
- self.read_scriptint_internal(5)
+ /// Decodes an integer in script(minimal CScriptNum) format.
+ ///
+ /// This is suitable to read input values for CHECKLOCKTIMEVERIFY instructions.
+ ///
+ /// Notice that this fails on overflow: the result is the same as in bitcoind, that only 4-byte
+ /// signed-magnitude values may be read as numbers. They can be added or subtracted (and a long
+ /// time ago, multiplied and divided), and this may result in numbers which can't be written out
+ /// in 4 bytes or less. This is ok! The number just can't be read as a number again. This is a
+ /// bit crazy and subtle, but it makes sense: you can load 32-bit numbers and do anything with
+ /// them, which back when mult/div was allowed, could result in up to a 64-bit number. We don't
+ /// want overflow since that's surprising --- and we don't want numbers that don't fit in 64
+ /// bits (for efficiency on modern processors). This function will return any value up to 40
+ /// bits in length. This is basically a ranged type implementation.
+ ///
+ /// This code is based on the
+ /// [`CScriptNum` constructor in Bitcoin Core](https://github.com/bitcoin/bitcoin/blob/99a4ddf5ab1b3e514d08b90ad8565827fda7b63b/src/script/script.h#L245)
+ ///
+ /// # Errors
+ ///
+ /// * [`ScriptIntError::NumericOverflow`] if result is not in range [-2^39 +1...2^39 -1].
+ /// * [`ScriptIntError::NonMinimal`] if encoding is non-minimal.
+ fn read_cltv_scriptint(&self) -> Result<i64, ScriptIntError> {
+ read_scriptint_internal(self, 5)
+ }
}
+}
- /// The internal implementation for reading a script integer.
- ///
- /// As with `read_cltv_scriptint`, this returns an i64, since that is the maximum size we might
- /// need to return data. In practice, if the max_size parameter is 4 or less, this function
- /// will always return a value that can fit into an i32, and can thus be safely cast.
- fn read_scriptint_internal(&self, max_size: usize) -> Result<i64, ScriptIntError> {
- let last = match self.as_bytes().last() {
- Some(last) => last,
- None => return Ok(0),
- };
- if self.len() > max_size {
- return Err(ScriptIntError::NumericOverflow);
- }
- // Comment and code copied from Bitcoin Core:
- // https://github.com/bitcoin/bitcoin/blob/447f50e4aed9a8b1d80e1891cda85801aeb80b4e/src/script/script.h#L247-L262
- // If the most-significant-byte - excluding the sign bit - is zero
- // then we're not minimal. Note how this test also rejects the
- // negative-zero encoding, 0x80.
- if (*last & 0x7f) == 0 {
- // One exception: if there's more than one byte and the most
- // significant bit of the second-most-significant-byte is set
- // it would conflict with the sign bit. An example of this case
- // is +-255, which encode to 0xff00 and 0xff80 respectively.
- // (big-endian).
- if self.len() <= 1 || (self[self.len() - 2] & 0x80) == 0 {
- return Err(ScriptIntError::NonMinimal);
- }
+/// The internal implementation for reading a script integer.
+///
+/// As with `read_cltv_scriptint`, this returns an i64, since that is the maximum size we might
+/// need to return data. In practice, if the max_size parameter is 4 or less, this function
+/// will always return a value that can fit into an i32, and can thus be safely cast.
+fn read_scriptint_internal(bytes: &PushBytes, max_size: usize) -> Result<i64, ScriptIntError> {
+ let last = match bytes.as_bytes().last() {
+ Some(last) => last,
+ None => return Ok(0),
+ };
+ if bytes.len() > max_size {
+ return Err(ScriptIntError::NumericOverflow);
+ }
+ // Comment and code copied from Bitcoin Core:
+ // https://github.com/bitcoin/bitcoin/blob/447f50e4aed9a8b1d80e1891cda85801aeb80b4e/src/script/script.h#L247-L262
+ // If the most-significant-byte - excluding the sign bit - is zero
+ // then we're not minimal. Note how this test also rejects the
+ // negative-zero encoding, 0x80.
+ if (*last & 0x7f) == 0 {
+ // One exception: if there's more than one byte and the most
+ // significant bit of the second-most-significant-byte is set
+ // it would conflict with the sign bit. An example of this case
+ // is +-255, which encode to 0xff00 and 0xff80 respectively.
+ // (big-endian).
+ if bytes.len() <= 1 || (bytes[bytes.len() - 2] & 0x80) == 0 {
+ return Err(ScriptIntError::NonMinimal);
}
-
- Ok(script::scriptint_parse(self.as_bytes()))
}
+
+ Ok(script::scriptint_parse(bytes.as_bytes()))
+}
+
+mod sealed {
+ pub trait Sealed {}
+ impl Sealed for super::PushBytes {}
}
impl PushBytesBuf {
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index a80d0cd3..fc1a53e0 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -104,7 +104,7 @@ pub mod ext {
key::{FullPublicKeyExt as _, LegacyPublicKeyExt as _},
network::NetworkExt as _,
pow::{CompactTargetExt as _, TargetExt as _, WorkExt as _},
- script::{ScriptExt as _, ScriptBufExt as _, TapScriptExt as _, ScriptPubKeyExt as _, ScriptPubKeyBufExt as _, WitnessScriptExt as _, ScriptSigExt as _},
+ script::{PushBytesExt as _, ScriptExt as _, ScriptBufExt as _, TapScriptExt as _, ScriptPubKeyExt as _, ScriptPubKeyBufExt as _, WitnessScriptExt as _, ScriptSigExt as _},
taproot::{TapLeafHashExt as _, TapNodeHashExt as _},
transaction::{TxidExt as _, WtxidExt as _, OutPointExt as _, TxInExt as _, TxOutExt as _, TransactionExt as _},
witness::WitnessExt as _,
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.