What changed, and why it matters
This commit adds new test-only helper functions that let developers check whether an encoder produces the expected bytes without needing to allocate memory. It also updates existing compact-size tests to use these helpers, making the tests simpler and usable in no-allocation environments. There is no change to production behavior or security-sensitive logic.
No security action needed. This is a test-infrastructure refactor. Normal code review and CI verification are sufficient.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces check_encode and check_encoder in consensus_encoding/src/encode/mod.rs. These helpers iterate over an encoder’s chunks and compare them against an expected byte slice, panicking on mismatch. They are marked for tests only and exported in lib.rs. The compact_size.rs test file is refactored to use check_encode instead of encode_to_vec, removing #[cfg(feature = "alloc")] guards from many tests. No production encoding/decoding code is modified.
Changed components
consensus_encoding/src/encode/mod.rsconsensus_encoding/src/lib.rsconsensus_encoding/tests/compact_size.rsInspect captured patch +84 / −71
diff --git a/consensus_encoding/src/encode/mod.rs b/consensus_encoding/src/encode/mod.rs
index 5b30d5fd..6da3e2b5 100644
--- a/consensus_encoding/src/encode/mod.rs
+++ b/consensus_encoding/src/encode/mod.rs
@@ -347,6 +347,59 @@ where
Ok(())
}
+/// Checks that the given `value` encodes to `expected`, panicking if it doesn't.
+///
+/// Note that the function does not impose any requirements on chunking - whether the encoded bytes
+/// are returned as a few large chunks or they are many smaller chunks makes no difference (other
+/// than potentially performance difference), as long as the bytes yielded are what is expected, in
+/// the correct order.
+///
+/// This is intended for tests only.
+///
+/// # Panics
+///
+/// If the bytes yielded from the encoder of `value` don't match the bytes in `expected`.
+#[track_caller]
+pub fn check_encode<T: Encode + ?Sized>(value: &T, expected: &[u8]) {
+ check_encoder(&mut value.encoder(), expected)
+}
+
+/// Checks that the given `encoder` yields `expected`, panicking if it doesn't.
+///
+/// Note that the function does not impose any requirements on chunking - whether the encoded bytes
+/// are returned as a few large chunks or they are many smaller chunks makes no difference (other
+/// than potentially performance difference), as long as the bytes yielded are what is expected, in
+/// the correct order.
+///
+/// This is intended for tests only.
+///
+/// # Panics
+///
+/// If the bytes yielded from the encoder don't match the bytes in `expected`.
+#[track_caller]
+pub fn check_encoder<T: Encoder + ?Sized>(encoder: &mut T, mut expected: &[u8]) {
+ let orig_expected_len = expected.len();
+ let mut chunk_number = 0usize;
+ let mut bytes_processed = 0usize;
+
+ loop {
+ let chunk = encoder.current_chunk();
+ if chunk.len() > expected.len() {
+ panic!("encoder yielded more bytes ({}) than expected ({})", bytes_processed + chunk.len(), orig_expected_len);
+ }
+ if let Some((i, _)) = chunk.iter().zip(&expected[..chunk.len()]).enumerate().find(|&(_, (a, b))| a != b) {
+ panic!("encoder did not yield expected bytes - difference in chunk #{}, after {} bytes", chunk_number, bytes_processed + i);
+ }
+ bytes_processed += chunk.len();
+ expected = &expected[chunk.len()..];
+ chunk_number += 1;
+ if !encoder.advance() {
+ break;
+ }
+ }
+ assert!(expected.is_empty(), "encoder did not yield enough bytes - {} more expected", expected.len());
+}
+
impl<T: Encoder> Encoder for Option<T> {
fn current_chunk(&self) -> &[u8] {
match self {
diff --git a/consensus_encoding/src/lib.rs b/consensus_encoding/src/lib.rs
index 2b87d8d2..d9d08f75 100644
--- a/consensus_encoding/src/lib.rs
+++ b/consensus_encoding/src/lib.rs
@@ -96,7 +96,7 @@ pub use self::encode::{drain_to_vec, encode_to_vec};
#[doc(inline)]
pub use self::encode::{drain_to_writer, encode_to_writer};
#[doc(inline)]
-pub use self::encode::{Encode, Encoder, EncoderByteIter, ExactSizeEncoder};
+pub use self::encode::{check_encode, check_encoder, Encode, Encoder, EncoderByteIter, ExactSizeEncoder};
#[cfg(feature = "alloc")]
#[doc(no_inline)]
pub use self::error::LengthPrefixExceedsMaxError;
diff --git a/consensus_encoding/tests/compact_size.rs b/consensus_encoding/tests/compact_size.rs
index 26a4acc1..b5384e91 100644
--- a/consensus_encoding/tests/compact_size.rs
+++ b/consensus_encoding/tests/compact_size.rs
@@ -2,18 +2,15 @@
//! Round-trip integration tests for `CompactSize` codec.
-#[cfg(feature = "alloc")]
use bitcoin_consensus_encoding::{
- decode_from_slice, encode_to_vec, CompactSizeDecoderError, CompactSizeEncoder,
+ check_encode, decode_from_slice, CompactSizeDecoderError, CompactSizeEncoder,
CompactSizeU64Decoder, Decode, Encode,
};
use bitcoin_consensus_encoding::{CompactSizeDecoder, Decoder};
/// A `usize` value encoded and decoded as a compact size length prefix.
-#[cfg(feature = "alloc")]
struct CompactSizeUsize(usize);
-#[cfg(feature = "alloc")]
impl Encode for CompactSizeUsize {
type Encoder<'e>
= CompactSizeEncoder
@@ -23,11 +20,9 @@ impl Encode for CompactSizeUsize {
}
/// Wraps `CompactSizeDecoder` to produce `CompactSizeUsize`.
-#[cfg(feature = "alloc")]
#[derive(Default)]
struct CompactSizeUsizeDecoderWrapper(CompactSizeDecoder);
-#[cfg(feature = "alloc")]
impl Decoder for CompactSizeUsizeDecoderWrapper {
type Output = CompactSizeUsize;
type Error = CompactSizeDecoderError;
@@ -41,16 +36,13 @@ impl Decoder for CompactSizeUsizeDecoderWrapper {
fn read_limit(&self) -> usize { self.0.read_limit() }
}
-#[cfg(feature = "alloc")]
impl Decode for CompactSizeUsize {
type Decoder = CompactSizeUsizeDecoderWrapper;
}
/// A `u64` value encoded and decoded as a compact size integer.
-#[cfg(feature = "alloc")]
struct CompactSizeU64(u64);
-#[cfg(feature = "alloc")]
impl Encode for CompactSizeU64 {
type Encoder<'e>
= CompactSizeEncoder
@@ -60,11 +52,9 @@ impl Encode for CompactSizeU64 {
}
/// Wraps `CompactSizeU64Decoder` to produce `CompactSizeU64`.
-#[cfg(feature = "alloc")]
#[derive(Default)]
struct CompactSizeU64DecoderWrapper(CompactSizeU64Decoder);
-#[cfg(feature = "alloc")]
impl Decoder for CompactSizeU64DecoderWrapper {
type Output = CompactSizeU64;
type Error = CompactSizeDecoderError;
@@ -78,136 +68,107 @@ impl Decoder for CompactSizeU64DecoderWrapper {
fn read_limit(&self) -> usize { self.0.read_limit() }
}
-#[cfg(feature = "alloc")]
impl Decode for CompactSizeU64 {
type Decoder = CompactSizeU64DecoderWrapper;
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_usize_zero() {
- let bytes = encode_to_vec(&CompactSizeUsize(0x00));
- assert_eq!(bytes, [0x00]);
- assert_eq!(decode_from_slice::<CompactSizeUsize>(&bytes).unwrap().0, 0x00);
+ check_encode(&CompactSizeUsize(0x00), &[0x00]);
+ assert_eq!(decode_from_slice::<CompactSizeUsize>(&[0x00]).unwrap().0, 0x00);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_usize_one_byte_max() {
// 0xFC is the largest value that fits in a single byte.
- let bytes = encode_to_vec(&CompactSizeUsize(0xFC));
- assert_eq!(bytes, [0xFC]);
- assert_eq!(decode_from_slice::<CompactSizeUsize>(&bytes).unwrap().0, 0xFC);
+ check_encode(&CompactSizeUsize(0xFC), &[0xFC]);
+ assert_eq!(decode_from_slice::<CompactSizeUsize>(&[0xFC]).unwrap().0, 0xFC);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_usize_three_byte_min() {
// 0xFD is the smallest value that requires the 0xFD prefix.
- let bytes = encode_to_vec(&CompactSizeUsize(0xFD));
- assert_eq!(bytes, [0xFD, 0xFD, 0x00]);
- assert_eq!(decode_from_slice::<CompactSizeUsize>(&bytes).unwrap().0, 0xFD);
+ check_encode(&CompactSizeUsize(0xFD), &[0xFD, 0xFD, 0x00]);
+ assert_eq!(decode_from_slice::<CompactSizeUsize>(&[0xFD, 0xFD, 0x00]).unwrap().0, 0xFD);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_usize_three_byte_max() {
- let bytes = encode_to_vec(&CompactSizeUsize(0xFFFF));
- assert_eq!(bytes, [0xFD, 0xFF, 0xFF]);
- assert_eq!(decode_from_slice::<CompactSizeUsize>(&bytes).unwrap().0, 0xFFFF);
+ check_encode(&CompactSizeUsize(0xFFFF), &[0xFD, 0xFF, 0xFF]);
+ assert_eq!(decode_from_slice::<CompactSizeUsize>(&[0xFD, 0xFF, 0xFF]).unwrap().0, 0xFFFF);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_usize_five_byte_min() {
// 0x10000 is the smallest value that requires the 0xFE prefix.
- let bytes = encode_to_vec(&CompactSizeUsize(0x10000));
- assert_eq!(bytes, [0xFE, 0x00, 0x00, 0x01, 0x00]);
- assert_eq!(decode_from_slice::<CompactSizeUsize>(&bytes).unwrap().0, 0x10000);
+ check_encode(&CompactSizeUsize(0x10000), &[0xFE, 0x00, 0x00, 0x01, 0x00]);
+ assert_eq!(decode_from_slice::<CompactSizeUsize>(&[0xFE, 0x00, 0x00, 0x01, 0x00]).unwrap().0, 0x10000);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_u64_zero() {
- let bytes = encode_to_vec(&CompactSizeU64(0x00));
- assert_eq!(bytes, [0x00]);
- assert_eq!(decode_from_slice::<CompactSizeU64>(&bytes).unwrap().0, 0x00);
+ check_encode(&CompactSizeU64(0x00), &[0x00]);
+ assert_eq!(decode_from_slice::<CompactSizeU64>(&[0x00]).unwrap().0, 0x00);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_u64_one_byte_max() {
- let bytes = encode_to_vec(&CompactSizeU64(0xFC));
- assert_eq!(bytes, [0xFC]);
- assert_eq!(decode_from_slice::<CompactSizeU64>(&bytes).unwrap().0, 0xFC);
+ // 0xFC is the largest value that fits in a single byte.
+ check_encode(&CompactSizeU64(0xFC), &[0xFC]);
+ assert_eq!(decode_from_slice::<CompactSizeU64>(&[0xFC]).unwrap().0, 0xFC);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_u64_three_byte_min() {
- let bytes = encode_to_vec(&CompactSizeU64(0xFD));
- assert_eq!(bytes, [0xFD, 0xFD, 0x00]);
- assert_eq!(decode_from_slice::<CompactSizeU64>(&bytes).unwrap().0, 0xFD);
+ // 0xFD is the smallest value that requires the 0xFD prefix.
+ check_encode(&CompactSizeU64(0xFD), &[0xFD, 0xFD, 0x00]);
+ assert_eq!(decode_from_slice::<CompactSizeU64>(&[0xFD, 0xFD, 0x00]).unwrap().0, 0xFD);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_u64_three_byte_max() {
- let bytes = encode_to_vec(&CompactSizeU64(0xFFFF));
- assert_eq!(bytes, [0xFD, 0xFF, 0xFF]);
- assert_eq!(decode_from_slice::<CompactSizeU64>(&bytes).unwrap().0, 0xFFFF);
+ check_encode(&CompactSizeU64(0xFFFF), &[0xFD, 0xFF, 0xFF]);
+ assert_eq!(decode_from_slice::<CompactSizeU64>(&[0xFD, 0xFF, 0xFF]).unwrap().0, 0xFFFF);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_u64_five_byte_min() {
- // 0x1_0000 is the smallest value that requires the 0xFE prefix.
- let bytes = encode_to_vec(&CompactSizeU64(0x1_0000));
- assert_eq!(bytes, [0xFE, 0x00, 0x00, 0x01, 0x00]);
- assert_eq!(decode_from_slice::<CompactSizeU64>(&bytes).unwrap().0, 0x1_0000);
+ // 0x10000 is the smallest value that requires the 0xFE prefix.
+ check_encode(&CompactSizeU64(0x10000), &[0xFE, 0x00, 0x00, 0x01, 0x00]);
+ assert_eq!(decode_from_slice::<CompactSizeU64>(&[0xFE, 0x00, 0x00, 0x01, 0x00]).unwrap().0, 0x10000);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_u64_five_byte_max() {
- let bytes = encode_to_vec(&CompactSizeU64(0xFFFF_FFFF));
- assert_eq!(bytes, [0xFE, 0xFF, 0xFF, 0xFF, 0xFF]);
- assert_eq!(decode_from_slice::<CompactSizeU64>(&bytes).unwrap().0, 0xFFFF_FFFF);
+ check_encode(&CompactSizeU64(0xFFFF_FFFF), &[0xFE, 0xFF, 0xFF, 0xFF, 0xFF]);
+ assert_eq!(decode_from_slice::<CompactSizeU64>(&[0xFE, 0xFF, 0xFF, 0xFF, 0xFF]).unwrap().0, 0xFFFF_FFFF);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_u64_nine_byte_min() {
- // 0x1_0000_0000 is the smallest value that requires the 0xFF prefix.
- let bytes = encode_to_vec(&CompactSizeU64(0x1_0000_0000));
- assert_eq!(bytes, [0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]);
- assert_eq!(decode_from_slice::<CompactSizeU64>(&bytes).unwrap().0, 0x1_0000_0000);
+ check_encode(&CompactSizeU64(0x1_0000_0000), &[0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]);
+ assert_eq!(decode_from_slice::<CompactSizeU64>(&[0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]).unwrap().0, 0x1_0000_0000);
}
#[test]
-#[cfg(feature = "alloc")]
fn round_trip_u64_max() {
- let bytes = encode_to_vec(&CompactSizeU64(u64::MAX));
- assert_eq!(bytes, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
- assert_eq!(decode_from_slice::<CompactSizeU64>(&bytes).unwrap().0, u64::MAX);
+ check_encode(&CompactSizeU64(u64::MAX), &[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
+ assert_eq!(decode_from_slice::<CompactSizeU64>(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).unwrap().0, u64::MAX);
}
#[test]
-#[cfg(feature = "alloc")]
fn non_minimal_rejected_u64_using_fd_prefix_for_small_value() {
// 0x42 fits in one byte but is encoded with the 0xFD (3-byte) prefix.
assert!(decode_from_slice::<CompactSizeU64>(&[0xFD, 0x42, 0x00]).is_err());
}
#[test]
-#[cfg(feature = "alloc")]
fn non_minimal_rejected_u64_using_fe_prefix_for_small_value() {
// 0x42 fits in one byte but is encoded with the 0xFE (5-byte) prefix.
assert!(decode_from_slice::<CompactSizeU64>(&[0xFE, 0x42, 0x00, 0x00, 0x00]).is_err());
}
#[test]
-#[cfg(feature = "alloc")]
fn non_minimal_rejected_u64_using_ff_prefix_for_small_value() {
// 0x42 fits in one byte but is encoded with the 0xFF (9-byte) prefix.
assert!(decode_from_slice::<CompactSizeU64>(&[
@@ -217,7 +178,6 @@ fn non_minimal_rejected_u64_using_ff_prefix_for_small_value() {
}
#[test]
-#[cfg(feature = "alloc")]
fn non_minimal_rejected_usize_using_fd_prefix_for_small_value() {
assert!(decode_from_slice::<CompactSizeUsize>(&[0xFD, 0x10, 0x00]).is_err());
}
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.