Move compact_size decoding into primitives witness
What changed, and why it matters
This commit is a simple internal code reorganization: it moves a helper function that decodes Bitcoin's compact-size integers from a shared 'internals' module into the only place that actually uses it, the witness handling code. The function's behavior, including its safety checks and panic conditions, is copied unchanged. There is no indication this fixes or introduces a security bug.
No security action required. Treat as ordinary refactoring. If reviewing, verify that no other crates depended on the removed public `bitcoin_internals::compact_size` module; the API snapshot deletions suggest it was public but the commit message states it was only used by witnesses.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change relocates decode_unchecked from bitcoin_internals::compact_size into primitives/src/witness.rs as a private helper, removes the now-unused internals/src/compact_size.rs module, and updates public API snapshots. The implementation is byte-for-byte identical, including assertions for non-empty input, length checks, and debug-only minimal-encoding assertions. Two call sites in Witness::nth and Iter::next are updated from compact_size::decode_unchecked to the local decode_unchecked. The test suite is also moved unchanged.
Changed components
internals/src/compact_size.rs (removed)internals/src/lib.rsprimitives/src/witness.rsInspect captured patch +125 / −145
diff --git a/api/internals/all-features.txt b/api/internals/all-features.txt
index 2684d1e7..65f9b11d 100644
--- a/api/internals/all-features.txt
+++ b/api/internals/all-features.txt
@@ -135,7 +135,6 @@ pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::fmt(&self, f: &mut core::
pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::hash<H: core::hash::Hasher>(&self, state: &mut H)
pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::pop(&mut self) -> core::option::Option<T>
pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::push(&mut self, element: T)
-pub fn bitcoin_internals::compact_size::decode_unchecked(slice: &mut &[u8]) -> u64
pub fn bitcoin_internals::error::input_string::CannotParse<'_, T>::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub fn bitcoin_internals::error::input_string::InputString::clone(&self) -> bitcoin_internals::error::input_string::InputString
pub fn bitcoin_internals::error::input_string::InputString::cmp(&self, other: &bitcoin_internals::error::input_string::InputString) -> core::cmp::Ordering
@@ -197,7 +196,6 @@ pub macro bitcoin_internals::write_err!
pub mod bitcoin_internals
pub mod bitcoin_internals::array
pub mod bitcoin_internals::array_vec
-pub mod bitcoin_internals::compact_size
pub mod bitcoin_internals::const_casts
pub mod bitcoin_internals::const_tools
pub mod bitcoin_internals::error
diff --git a/api/internals/alloc-only.txt b/api/internals/alloc-only.txt
index 9c9155bf..526d702b 100644
--- a/api/internals/alloc-only.txt
+++ b/api/internals/alloc-only.txt
@@ -124,7 +124,6 @@ pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::fmt(&self, f: &mut core::
pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::hash<H: core::hash::Hasher>(&self, state: &mut H)
pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::pop(&mut self) -> core::option::Option<T>
pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::push(&mut self, element: T)
-pub fn bitcoin_internals::compact_size::decode_unchecked(slice: &mut &[u8]) -> u64
pub fn bitcoin_internals::error::input_string::CannotParse<'_, T>::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub fn bitcoin_internals::error::input_string::InputString::clone(&self) -> bitcoin_internals::error::input_string::InputString
pub fn bitcoin_internals::error::input_string::InputString::cmp(&self, other: &bitcoin_internals::error::input_string::InputString) -> core::cmp::Ordering
@@ -175,7 +174,6 @@ pub macro bitcoin_internals::write_err!
pub mod bitcoin_internals
pub mod bitcoin_internals::array
pub mod bitcoin_internals::array_vec
-pub mod bitcoin_internals::compact_size
pub mod bitcoin_internals::const_casts
pub mod bitcoin_internals::const_tools
pub mod bitcoin_internals::error
diff --git a/api/internals/no-features.txt b/api/internals/no-features.txt
index 6b1533d5..0153c69a 100644
--- a/api/internals/no-features.txt
+++ b/api/internals/no-features.txt
@@ -121,7 +121,6 @@ pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::fmt(&self, f: &mut core::
pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::hash<H: core::hash::Hasher>(&self, state: &mut H)
pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::pop(&mut self) -> core::option::Option<T>
pub fn bitcoin_internals::array_vec::ArrayVec<T, CAP>::push(&mut self, element: T)
-pub fn bitcoin_internals::compact_size::decode_unchecked(slice: &mut &[u8]) -> u64
pub fn bitcoin_internals::error::input_string::CannotParse<'_, T>::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub fn bitcoin_internals::error::input_string::InputString::clone(&self) -> bitcoin_internals::error::input_string::InputString
pub fn bitcoin_internals::error::input_string::InputString::cmp(&self, other: &bitcoin_internals::error::input_string::InputString) -> core::cmp::Ordering
@@ -169,7 +168,6 @@ pub macro bitcoin_internals::write_err!
pub mod bitcoin_internals
pub mod bitcoin_internals::array
pub mod bitcoin_internals::array_vec
-pub mod bitcoin_internals::compact_size
pub mod bitcoin_internals::const_casts
pub mod bitcoin_internals::const_tools
pub mod bitcoin_internals::error
diff --git a/internals/src/compact_size.rs b/internals/src/compact_size.rs
deleted file mode 100644
index 81821ab6..00000000
--- a/internals/src/compact_size.rs
+++ /dev/null
@@ -1,135 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Variable length integer encoding A.K.A [`CompactSize`].
-//!
-//! An integer can be encoded depending on the represented value to save space. Variable length
-//! integers always precede an array/vector of a type of data that may vary in length.
-//!
-//! [`CompactSize`]: <https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer>
-
-/// Gets the compact size encoded value from `slice` and moves slice past the encoding.
-///
-/// Caller to guarantee that the encoding is well formed. Well formed is defined as:
-///
-/// * Being at least long enough.
-/// * Containing a minimal encoding.
-///
-/// # Panics
-///
-/// * Panics in release mode if the `slice` does not contain a valid minimal compact size encoding.
-/// * Panics in debug mode if the encoding is not minimal (referred to as "non-canonical" in Core).
-pub fn decode_unchecked(slice: &mut &[u8]) -> u64 {
- assert!(!slice.is_empty(), "tried to decode an empty slice");
-
- match slice[0] {
- 0xFF => {
- const SIZE: usize = 9;
- assert!(slice.len() >= SIZE, "slice too short, expected at least 9 bytes");
-
- let mut bytes = [0_u8; SIZE - 1];
- bytes.copy_from_slice(&slice[1..SIZE]);
-
- let v = u64::from_le_bytes(bytes);
- debug_assert!(v > u32::MAX.into(), "non-minimal encoding of a u64");
- *slice = &slice[SIZE..];
- v
- }
- 0xFE => {
- const SIZE: usize = 5;
- assert!(slice.len() >= SIZE, "slice too short, expected at least 5 bytes");
-
- let mut bytes = [0_u8; SIZE - 1];
- bytes.copy_from_slice(&slice[1..SIZE]);
-
- let v = u32::from_le_bytes(bytes);
- debug_assert!(v > u16::MAX.into(), "non-minimal encoding of a u32");
- *slice = &slice[SIZE..];
- u64::from(v)
- }
- 0xFD => {
- const SIZE: usize = 3;
- assert!(slice.len() >= SIZE, "slice too short, expected at least 3 bytes");
-
- let mut bytes = [0_u8; SIZE - 1];
- bytes.copy_from_slice(&slice[1..SIZE]);
-
- let v = u16::from_le_bytes(bytes);
- debug_assert!(v >= 0xFD, "non-minimal encoding of a u16");
- *slice = &slice[SIZE..];
- u64::from(v)
- }
- n => {
- *slice = &slice[1..];
- u64::from(n)
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn decode_value_1_byte() {
- // Check lower bound, upper bound.
- for v in [0x00, 0x01, 0x02, 0xFA, 0xFB, 0xFC] {
- let raw = [v];
- let mut slice = raw.as_slice();
- let got = decode_unchecked(&mut slice);
- assert_eq!(got, u64::from(v));
- assert!(slice.is_empty());
- }
- }
-
- macro_rules! check_decode {
- ($($test_name:ident, $size:expr, $want:expr, $encoded:expr);* $(;)?) => {
- $(
- #[test]
- fn $test_name() {
- let mut slice = $encoded.as_slice();
- let got = decode_unchecked(&mut slice);
- assert_eq!(got, $want);
- assert_eq!(slice.len(), $encoded.len() - $size);
- }
- )*
- }
- }
-
- check_decode! {
- // 3 byte encoding.
- decode_from_3_byte_slice_lower_bound, 3, 0xFD, [0xFD, 0xFD, 0x00];
- decode_from_3_byte_slice_three_over_lower_bound, 3, 0x0100, [0xFD, 0x00, 0x01];
- decode_from_3_byte_slice_endianness, 3, 0xABCD, [0xFD, 0xCD, 0xAB];
- decode_from_3_byte_slice_upper_bound, 3, 0xFFFF, [0xFD, 0xFF, 0xFF];
-
- // 5 byte encoding.
- decode_from_5_byte_slice_lower_bound, 5, 0x0001_0000, [0xFE, 0x00, 0x00, 0x01, 0x00];
- decode_from_5_byte_slice_endianness, 5, 0x0123_4567, [0xFE, 0x67, 0x45, 0x23, 0x01];
- decode_from_5_byte_slice_upper_bound, 5, 0xFFFF_FFFF, [0xFE, 0xFF, 0xFF, 0xFF, 0xFF];
- // 9 byte encoding.
- decode_from_9_byte_slice_lower_bound, 9, 0x0000_0001_0000_0000, [0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00];
- decode_from_9_byte_slice_endianness, 9, 0x0123_4567_89AB_CDEF, [0xFF, 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01];
- decode_from_9_byte_slice_upper_bound, 9, u64::MAX, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
-
- // Check slices that are bigger than the actual encoding.
- decode_1_byte_from_bigger_slice, 1, 32, [0x20, 0xAB, 0xBC];
- decode_3_byte_from_bigger_slice, 3, 0xFFFF, [0xFD, 0xFF, 0xFF, 0xAB, 0xBC];
- decode_5_byte_from_bigger_slice, 5, 0xFFFF_FFFF, [0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xAB, 0xBC];
- decode_9_byte_from_bigger_slice, 9, u64::MAX, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAB, 0xBC];
- }
-
- #[test]
- #[should_panic(expected = "tried to decode an empty slice")]
- fn decode_from_empty_slice_panics() {
- let mut slice = [].as_slice();
- let _ = decode_unchecked(&mut slice);
- }
-
- #[test]
- #[should_panic(expected = "slice too short")]
- // Non-minimal is referred to as non-canonical in Core (`bitcoin/src/serialize.h`).
- fn decode_non_minimal_panics() {
- let mut slice = [0xFE, 0xCD, 0xAB].as_slice();
- let _ = decode_unchecked(&mut slice);
- }
-}
diff --git a/internals/src/lib.rs b/internals/src/lib.rs
index 2425927e..b1d24040 100644
--- a/internals/src/lib.rs
+++ b/internals/src/lib.rs
@@ -38,7 +38,6 @@ pub mod _export {
pub mod array;
pub mod array_vec;
-pub mod compact_size;
pub mod const_tools;
pub mod error;
pub mod macros;
diff --git a/primitives/src/witness.rs b/primitives/src/witness.rs
index a9448843..b5e8917f 100644
--- a/primitives/src/witness.rs
+++ b/primitives/src/witness.rs
@@ -20,7 +20,7 @@ use encoding::{
use hex::DecodeVariableLengthBytesError;
use internals::slice::SliceExt;
use internals::wrap_debug::WrapDebug;
-use internals::{compact_size, write_err};
+use internals::write_err;
use crate::prelude::{Box, Vec};
#[cfg(doc)]
@@ -212,7 +212,7 @@ impl Witness {
let pos = decode_cursor(&self.content, self.indices_start, index)?;
let mut slice = &self.content[pos..]; // Start of element.
- let element_len = compact_size::decode_unchecked(&mut slice);
+ let element_len = decode_unchecked(&mut slice);
let end = cast_to_usize_if_valid(element_len)?;
Some(&slice[..end])
}
@@ -617,7 +617,7 @@ impl<'a> Iterator for Iter<'a> {
fn next(&mut self) -> Option<Self::Item> {
let index = decode_cursor(self.inner, self.indices_start, self.current_index)?;
let mut slice = &self.inner[index..]; // Start of element.
- let element_len = compact_size::decode_unchecked(&mut slice);
+ let element_len = decode_unchecked(&mut slice);
let end = cast_to_usize_if_valid(element_len)?;
self.current_index += 1;
Some(&slice[..end])
@@ -880,6 +880,64 @@ fn cast_to_usize_if_valid(n: u64) -> Option<usize> {
usize::try_from(n).ok()
}
+/// Gets the compact size encoded value from `slice` and moves slice past the encoding.
+///
+/// Caller to guarantee that the encoding is well formed. Well formed is defined as:
+///
+/// * Being at least long enough.
+/// * Containing a minimal encoding.
+///
+/// # Panics
+///
+/// * Panics in release mode if the `slice` does not contain a valid minimal compact size encoding.
+/// * Panics in debug mode if the encoding is not minimal (referred to as "non-canonical" in Core).
+fn decode_unchecked(slice: &mut &[u8]) -> u64 {
+ assert!(!slice.is_empty(), "tried to decode an empty slice");
+
+ match slice[0] {
+ 0xFF => {
+ const SIZE: usize = 9;
+ assert!(slice.len() >= SIZE, "slice too short, expected at least 9 bytes");
+
+ let mut bytes = [0_u8; SIZE - 1];
+ bytes.copy_from_slice(&slice[1..SIZE]);
+
+ let v = u64::from_le_bytes(bytes);
+ debug_assert!(v > u32::MAX.into(), "non-minimal encoding of a u64");
+ *slice = &slice[SIZE..];
+ v
+ }
+ 0xFE => {
+ const SIZE: usize = 5;
+ assert!(slice.len() >= SIZE, "slice too short, expected at least 5 bytes");
+
+ let mut bytes = [0_u8; SIZE - 1];
+ bytes.copy_from_slice(&slice[1..SIZE]);
+
+ let v = u32::from_le_bytes(bytes);
+ debug_assert!(v > u16::MAX.into(), "non-minimal encoding of a u32");
+ *slice = &slice[SIZE..];
+ u64::from(v)
+ }
+ 0xFD => {
+ const SIZE: usize = 3;
+ assert!(slice.len() >= SIZE, "slice too short, expected at least 3 bytes");
+
+ let mut bytes = [0_u8; SIZE - 1];
+ bytes.copy_from_slice(&slice[1..SIZE]);
+
+ let v = u16::from_le_bytes(bytes);
+ debug_assert!(v >= 0xFD, "non-minimal encoding of a u16");
+ *slice = &slice[SIZE..];
+ u64::from(v)
+ }
+ n => {
+ *slice = &slice[1..];
+ u64::from(n)
+ }
+ }
+}
+
#[cfg(test)]
mod test {
#[cfg(feature = "alloc")]
@@ -1487,4 +1545,68 @@ mod test {
witness.push([0u8; 253]);
assert_eq!(witness.size(), encoding::encode_to_vec(&witness).len());
}
+
+ #[test]
+ fn decode_value_1_byte() {
+ // Check lower bound, upper bound.
+ for v in [0x00, 0x01, 0x02, 0xFA, 0xFB, 0xFC] {
+ let raw = [v];
+ let mut slice = raw.as_slice();
+ let got = decode_unchecked(&mut slice);
+ assert_eq!(got, u64::from(v));
+ assert!(slice.is_empty());
+ }
+ }
+
+ macro_rules! check_decode {
+ ($($test_name:ident, $size:expr, $want:expr, $encoded:expr);* $(;)?) => {
+ $(
+ #[test]
+ fn $test_name() {
+ let mut slice = $encoded.as_slice();
+ let got = decode_unchecked(&mut slice);
+ assert_eq!(got, $want);
+ assert_eq!(slice.len(), $encoded.len() - $size);
+ }
+ )*
+ }
+ }
+
+ check_decode! {
+ // 3 byte encoding.
+ decode_from_3_byte_slice_lower_bound, 3, 0xFD, [0xFD, 0xFD, 0x00];
+ decode_from_3_byte_slice_three_over_lower_bound, 3, 0x0100, [0xFD, 0x00, 0x01];
+ decode_from_3_byte_slice_endianness, 3, 0xABCD, [0xFD, 0xCD, 0xAB];
+ decode_from_3_byte_slice_upper_bound, 3, 0xFFFF, [0xFD, 0xFF, 0xFF];
+
+ // 5 byte encoding.
+ decode_from_5_byte_slice_lower_bound, 5, 0x0001_0000, [0xFE, 0x00, 0x00, 0x01, 0x00];
+ decode_from_5_byte_slice_endianness, 5, 0x0123_4567, [0xFE, 0x67, 0x45, 0x23, 0x01];
+ decode_from_5_byte_slice_upper_bound, 5, 0xFFFF_FFFF, [0xFE, 0xFF, 0xFF, 0xFF, 0xFF];
+ // 9 byte encoding.
+ decode_from_9_byte_slice_lower_bound, 9, 0x0000_0001_0000_0000, [0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00];
+ decode_from_9_byte_slice_endianness, 9, 0x0123_4567_89AB_CDEF, [0xFF, 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01];
+ decode_from_9_byte_slice_upper_bound, 9, u64::MAX, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
+
+ // Check slices that are bigger than the actual encoding.
+ decode_1_byte_from_bigger_slice, 1, 32, [0x20, 0xAB, 0xBC];
+ decode_3_byte_from_bigger_slice, 3, 0xFFFF, [0xFD, 0xFF, 0xFF, 0xAB, 0xBC];
+ decode_5_byte_from_bigger_slice, 5, 0xFFFF_FFFF, [0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xAB, 0xBC];
+ decode_9_byte_from_bigger_slice, 9, u64::MAX, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAB, 0xBC];
+ }
+
+ #[test]
+ #[should_panic(expected = "tried to decode an empty slice")]
+ fn decode_from_empty_slice_panics() {
+ let mut slice = [].as_slice();
+ let _ = decode_unchecked(&mut slice);
+ }
+
+ #[test]
+ #[should_panic(expected = "slice too short, expected at least 5 bytes")]
+ // Non-minimal is referred to as non-canonical in Core (`bitcoin/src/serialize.h`).
+ fn decode_non_minimal_panics() {
+ let mut slice = [0xFE, 0xCD, 0xAB].as_slice();
+ let _ = decode_unchecked(&mut slice);
+ }
}
Why this scored 18/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.