consensus_encoding: add checked decode functions for tests
What changed, and why it matters
This commit only adds new helper functions for unit tests and refactors existing tests to use them. It does not change any production behavior, fix a bug, or alter security-relevant code paths. There is no indication this is a security patch.
No security action required; this is a test-only refactoring change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces check_decode and check_decoder test-only utilities in consensus_encoding/src/decode/mod.rs, then refactors test files (compact_size.rs, composition.rs, decode.rs) to call these helpers instead of inline decode/assert sequences. It also moves some public re-exports between std and non-std feature gates so the new helpers are available without std. The production decode/encode logic is unchanged.
Changed components
consensus_encoding/src/decode/mod.rsconsensus_encoding/src/lib.rsconsensus_encoding/tests/compact_size.rsconsensus_encoding/tests/composition.rsconsensus_encoding/tests/decode.rsconsensus_encoding/tests/iter.rsInspect captured patch +108 / −90
diff --git a/consensus_encoding/src/decode/mod.rs b/consensus_encoding/src/decode/mod.rs
index 07007574..2b9c3ceb 100644
--- a/consensus_encoding/src/decode/mod.rs
+++ b/consensus_encoding/src/decode/mod.rs
@@ -302,3 +302,52 @@ where
decoder.end().map_err(ReadError::Decode)
}
+
+/// Checks that the given bytes decode to the expected value, panicking if they don't.
+///
+/// This is intended for tests only.
+///
+/// # Panics
+///
+/// If the decoded value doesn't match the expected value, or if decoding fails.
+#[track_caller]
+pub fn check_decode<T: Decode + Eq + core::fmt::Debug>(bytes: &[u8], expected: &T)
+where
+ <T::Decoder as Decoder>::Error: core::fmt::Debug,
+{
+ let decoder = T::decoder();
+ check_decoder(decoder, bytes, expected);
+}
+
+/// Checks that the given `decoder` produces the expected value, panicking if it doesn't.
+///
+/// This is intended for tests only.
+///
+/// # Panics
+///
+/// If the decoder doesn't produce the expected value or if decoding fails.
+#[track_caller]
+pub fn check_decoder<D: Decoder>(mut decoder: D, mut bytes: &[u8], expected: &D::Output)
+where
+ D::Output: Eq + core::fmt::Debug,
+ D::Error: core::fmt::Debug,
+{
+ loop {
+ match decoder.push_bytes(&mut bytes) {
+ Ok(status) => {
+ if status.is_ready() {
+ break;
+ }
+ assert!(!bytes.is_empty(), "decoder needs more data but no bytes remaining");
+ }
+ Err(e) => panic!("decoder failed with error: {e:?}"),
+ }
+ }
+
+ match decoder.end() {
+ Ok(result) => {
+ assert_eq!(&result, expected, "decoded value doesn't match expected value");
+ }
+ Err(e) => panic!("decoder finalization failed with error: {e:?}"),
+ }
+}
diff --git a/consensus_encoding/src/lib.rs b/consensus_encoding/src/lib.rs
index 16493970..78904ad5 100644
--- a/consensus_encoding/src/lib.rs
+++ b/consensus_encoding/src/lib.rs
@@ -77,14 +77,15 @@ pub use self::decode::decoders::{ArrayDecoder, Decoder2, Decoder3, Decoder4, Dec
#[cfg(feature = "alloc")]
#[doc(inline)]
pub use self::decode::decoders::{ByteVecDecoder, VecDecoder};
-#[cfg(feature = "std")]
#[doc(inline)]
pub use self::decode::{
- decode_from_read, decode_from_read_unbuffered, decode_from_read_unbuffered_with,
+ check_decode, check_decoder, decode_from_slice, decode_from_slice_unbounded, Decode, Decoder,
+ DecoderStatus,
};
+#[cfg(feature = "std")]
#[doc(inline)]
pub use self::decode::{
- decode_from_slice, decode_from_slice_unbounded, Decode, Decoder, DecoderStatus,
+ decode_from_read, decode_from_read_unbuffered, decode_from_read_unbuffered_with,
};
#[doc(inline)]
pub use self::encode::encoders::{
diff --git a/consensus_encoding/tests/compact_size.rs b/consensus_encoding/tests/compact_size.rs
index 4bb6d398..be06eb03 100644
--- a/consensus_encoding/tests/compact_size.rs
+++ b/consensus_encoding/tests/compact_size.rs
@@ -3,11 +3,12 @@
//! Round-trip integration tests for `CompactSize` codec.
use bitcoin_consensus_encoding::{
- check_encode, decode_from_slice, CompactSizeDecoder, CompactSizeDecoderError,
+ check_decode, check_encode, decode_from_slice, CompactSizeDecoder, CompactSizeDecoderError,
CompactSizeEncoder, CompactSizeU64Decoder, Decode, Decoder, Encode, ExactSizeEncoder,
};
/// A `usize` value encoded and decoded as a compact size length prefix.
+#[derive(Debug, Eq, PartialEq)]
struct CompactSizeUsize(usize);
impl Encode for CompactSizeUsize {
@@ -43,6 +44,7 @@ impl Decode for CompactSizeUsize {
}
/// A `u64` value encoded and decoded as a compact size integer.
+#[derive(Debug, Eq, PartialEq)]
struct CompactSizeU64(u64);
impl Encode for CompactSizeU64 {
@@ -80,82 +82,73 @@ impl Decode for CompactSizeU64 {
#[test]
fn round_trip_usize_zero() {
check_encode(&CompactSizeUsize(0x00), &[0x00]);
- assert_eq!(decode_from_slice::<CompactSizeUsize>(&[0x00]).unwrap().0, 0x00);
+ check_decode(&[0x00], &CompactSizeUsize(0x00));
}
#[test]
fn round_trip_usize_one_byte_max() {
// 0xFC is the largest value that fits in a single byte.
check_encode(&CompactSizeUsize(0xFC), &[0xFC]);
- assert_eq!(decode_from_slice::<CompactSizeUsize>(&[0xFC]).unwrap().0, 0xFC);
+ check_decode(&[0xFC], &CompactSizeUsize(0xFC));
}
#[test]
fn round_trip_usize_three_byte_min() {
// 0xFD is the smallest value that requires the 0xFD prefix.
check_encode(&CompactSizeUsize(0xFD), &[0xFD, 0xFD, 0x00]);
- assert_eq!(decode_from_slice::<CompactSizeUsize>(&[0xFD, 0xFD, 0x00]).unwrap().0, 0xFD);
+ check_decode(&[0xFD, 0xFD, 0x00], &CompactSizeUsize(0xFD));
}
#[test]
fn round_trip_usize_three_byte_max() {
check_encode(&CompactSizeUsize(0xFFFF), &[0xFD, 0xFF, 0xFF]);
- assert_eq!(decode_from_slice::<CompactSizeUsize>(&[0xFD, 0xFF, 0xFF]).unwrap().0, 0xFFFF);
+ check_decode(&[0xFD, 0xFF, 0xFF], &CompactSizeUsize(0xFFFF));
}
#[test]
fn round_trip_usize_five_byte_min() {
// 0x10000 is the smallest value that requires the 0xFE prefix.
check_encode(&CompactSizeUsize(0x10000), &[0xFE, 0x00, 0x00, 0x01, 0x00]);
- assert_eq!(
- decode_from_slice::<CompactSizeUsize>(&[0xFE, 0x00, 0x00, 0x01, 0x00]).unwrap().0,
- 0x10000
- );
+ check_decode(&[0xFE, 0x00, 0x00, 0x01, 0x00], &CompactSizeUsize(0x10000));
}
#[test]
fn round_trip_u64_zero() {
check_encode(&CompactSizeU64(0x00), &[0x00]);
- assert_eq!(decode_from_slice::<CompactSizeU64>(&[0x00]).unwrap().0, 0x00);
+ check_decode(&[0x00], &CompactSizeU64(0x00));
}
#[test]
fn round_trip_u64_one_byte_max() {
// 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);
+ check_decode(&[0xFC], &CompactSizeU64(0xFC));
}
#[test]
fn round_trip_u64_three_byte_min() {
// 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);
+ check_decode(&[0xFD, 0xFD, 0x00], &CompactSizeU64(0xFD));
}
#[test]
fn round_trip_u64_three_byte_max() {
check_encode(&CompactSizeU64(0xFFFF), &[0xFD, 0xFF, 0xFF]);
- assert_eq!(decode_from_slice::<CompactSizeU64>(&[0xFD, 0xFF, 0xFF]).unwrap().0, 0xFFFF);
+ check_decode(&[0xFD, 0xFF, 0xFF], &CompactSizeU64(0xFFFF));
}
#[test]
fn round_trip_u64_five_byte_min() {
// 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
- );
+ check_decode(&[0xFE, 0x00, 0x00, 0x01, 0x00], &CompactSizeU64(0x10000));
}
#[test]
fn round_trip_u64_five_byte_max() {
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
- );
+ check_decode(&[0xFE, 0xFF, 0xFF, 0xFF, 0xFF], &CompactSizeU64(0xFFFF_FFFF));
}
#[test]
@@ -164,13 +157,9 @@ fn round_trip_u64_nine_byte_min() {
&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
+ check_decode(
+ &[0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00],
+ &CompactSizeU64(0x1_0000_0000),
);
}
@@ -180,13 +169,9 @@ fn round_trip_u64_max() {
&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
+ check_decode(
+ &[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
+ &CompactSizeU64(u64::MAX),
);
}
diff --git a/consensus_encoding/tests/composition.rs b/consensus_encoding/tests/composition.rs
index 9f970519..c9fe2fb7 100644
--- a/consensus_encoding/tests/composition.rs
+++ b/consensus_encoding/tests/composition.rs
@@ -2,13 +2,13 @@
//! Test composition of encoders and decoders.
+#[cfg(feature = "alloc")]
use bitcoin_consensus_encoding::{
- check_encoder, ArrayDecoder, BytesEncoder, Decoder, Decoder2, Decoder2Error, Decoder6,
- DecoderStatus, Encoder3, UnexpectedEofError,
+ check_decode, check_encoder, drain_to_vec, encode_to_vec, ArrayEncoder, BytesEncoder, Decode,
+ Encode, Encoder2, Encoder3, Encoder6,
};
-#[cfg(feature = "alloc")]
use bitcoin_consensus_encoding::{
- drain_to_vec, encode_to_vec, ArrayEncoder, Decode, Encode, Encoder2, Encoder6,
+ ArrayDecoder, Decoder, Decoder2, Decoder2Error, Decoder6, DecoderStatus, UnexpectedEofError,
};
#[cfg(feature = "alloc")]
@@ -94,14 +94,7 @@ impl Decode for CompositeData {
fn composition_chain() {
let original = CompositeData { first: [0x01, 0x02, 0x03, 0x04], second: [0x05, 0x06] };
let encoded_bytes = encode_to_vec(&original);
- // Decode using the push decoder.
- let mut decoder = CompositeData::decoder();
- let mut bytes = &encoded_bytes[..];
- let status = decoder.push_bytes(&mut bytes).unwrap();
- assert!(status.is_ready(), "CompositeData decoder should be ready to end");
- assert_eq!(bytes, EMPTY);
- let decoded = decoder.end().unwrap();
- assert_eq!(original, decoded);
+ check_decode(&encoded_bytes, &original);
}
#[cfg(feature = "alloc")]
@@ -320,6 +313,7 @@ fn composition_error_unification() {
}
#[test]
+#[cfg(feature = "alloc")]
fn empty_encoders() {
let bytes = [0x01, 2, 3, 4];
let mut encoder = Encoder3::new(
diff --git a/consensus_encoding/tests/decode.rs b/consensus_encoding/tests/decode.rs
index b0cf73cd..70b0bb07 100644
--- a/consensus_encoding/tests/decode.rs
+++ b/consensus_encoding/tests/decode.rs
@@ -5,6 +5,9 @@
#[cfg(feature = "std")]
use std::io::{Cursor, Read};
+#[cfg(feature = "alloc")]
+use bitcoin_consensus_encoding::check_decode;
+use bitcoin_consensus_encoding::check_decoder;
#[cfg(feature = "std")]
use bitcoin_consensus_encoding::{decode_from_read, decode_from_read_unbuffered, ReadError};
use bitcoin_consensus_encoding::{
@@ -520,46 +523,21 @@ check_decode_one_byte_at_a_time! {
#[cfg(feature = "alloc")]
fn vec_decoder_empty() {
// Empty with a couple of arbitrary extra bytes.
- let encoded = vec![0x00, 0xFF, 0xFF];
-
- let mut slice = encoded.as_slice();
- let mut decoder = Test::decoder();
- assert!(decoder.push_bytes(&mut slice).unwrap().is_ready());
-
- let got = decoder.end().unwrap();
- let want = Test(vec![]);
-
- assert_eq!(got, want);
+ check_decode(&[0x00], &Test(vec![]));
}
#[test]
#[cfg(feature = "alloc")]
fn vec_decoder_one_item() {
let encoded = vec![0x01, 0xEF, 0xBE, 0xAD, 0xDE];
-
- let mut slice = encoded.as_slice();
- let mut decoder = Test::decoder();
- decoder.push_bytes(&mut slice).unwrap();
-
- let got = decoder.end().unwrap();
- let want = Test(vec![Inner(0xDEAD_BEEF)]);
-
- assert_eq!(got, want);
+ check_decode(&encoded, &Test(vec![Inner(0xDEAD_BEEF)]));
}
#[test]
#[cfg(feature = "alloc")]
fn vec_decoder_two_items() {
let encoded = vec![0x02, 0xEF, 0xBE, 0xAD, 0xDE, 0xBE, 0xBA, 0xFE, 0xCA];
-
- let mut slice = encoded.as_slice();
- let mut decoder = Test::decoder();
- decoder.push_bytes(&mut slice).unwrap();
-
- let got = decoder.end().unwrap();
- let want = Test(vec![Inner(0xDEAD_BEEF), Inner(0xCAFE_BABE)]);
-
- assert_eq!(got, want);
+ check_decode(&encoded, &Test(vec![Inner(0xDEAD_BEEF), Inner(0xCAFE_BABE)]));
}
#[test]
@@ -611,17 +589,7 @@ check_decode_one_byte_at_a_time! {
#[cfg(feature = "alloc")]
fn vec_decoder_one_item_plus_more_data() {
// One u32 plus some other bytes.
- let encoded = vec![0x01, 0xEF, 0xBE, 0xAD, 0xDE, 0xff, 0xff, 0xff, 0xff];
-
- let mut slice = encoded.as_slice();
-
- let mut decoder = Test::decoder();
- decoder.push_bytes(&mut slice).unwrap();
-
- let got = decoder.end().unwrap();
- let want = Test(vec![Inner(0xDEAD_BEEF)]);
-
- assert_eq!(got, want);
+ check_decode(&[0x01, 0xEF, 0xBE, 0xAD, 0xDE], &Test(vec![Inner(0xDEAD_BEEF)]));
}
#[cfg(feature = "std")]
@@ -687,3 +655,25 @@ fn decode_vec_decoder_end_incomplete_item() {
let err = decoder.end().unwrap_err();
assert!(matches!(err, bitcoin_consensus_encoding::VecDecoderError { .. }));
}
+
+#[test]
+#[cfg(feature = "alloc")]
+fn check_decode_panic_on_mismatched_value() {
+ let encoded = [0xEF, 0xBE, 0xAD, 0xDEu8];
+ let expected = Inner(0x1234_5678);
+ let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ check_decode(&encoded, &expected);
+ }));
+ assert!(result.is_err());
+}
+
+#[test]
+fn check_decoder_panic_on_mismatched_value() {
+ let decoder = ArrayDecoder::<1>::new();
+ let bytes = &[0x42u8][..];
+ let expected = [0x99u8];
+ let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ check_decoder(decoder, bytes, &expected);
+ }));
+ assert!(result.is_err());
+}
diff --git a/consensus_encoding/tests/iter.rs b/consensus_encoding/tests/iter.rs
index 918a20e3..1f84ac8f 100644
--- a/consensus_encoding/tests/iter.rs
+++ b/consensus_encoding/tests/iter.rs
@@ -76,7 +76,6 @@ fn hex_iter_cat_encoder() {
assert_eq!(char::from(iter_chars[0]), hi);
let lo = chars.next().unwrap();
assert_eq!(char::from(iter_chars[1]), lo);
-
}
let none = iter.next();
assert_eq!(none, None);
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.