Add regression test for decode_from_read_unbuffered_with bug
What changed, and why it matters
This commit only adds a regression test for a previously existing bug in a decoding helper. It does not change any production code, so by itself it cannot introduce a security vulnerability. The test documents a bug where a decoder could lose unconsumed bytes under specific conditions, but the actual fix must have happened in an earlier commit not shown here.
No action required for this commit. If reviewing the broader fix, locate the commit that actually changed decode_from_read_unbuffered_with and assess whether the decoder now preserves all unconsumed bytes across read boundaries.
Security signals we found
No production code changes
Regression test only
Historical decoder byte-loss bug referenced in commit message
Evidence from the diff
The diff adds a single test, decode_from_read_unbuffered_with_loses_unconsumed_bytes, in consensus_encoding/tests/decode.rs. The test constructs a custom decoder that consumes one byte per push_bytes call and verifies that decode_from_read_unbuffered_with correctly decodes four bytes. Because the commit contains no library code changes, there is no new attack surface or vulnerability introduced in this patch. The referenced bug is historical; this commit is purely a regression test.
Changed components
consensus_encoding/tests/decode.rsInspect captured patch +62 / −0
diff --git a/consensus_encoding/tests/decode.rs b/consensus_encoding/tests/decode.rs
index 936a4cde..5098fcc6 100644
--- a/consensus_encoding/tests/decode.rs
+++ b/consensus_encoding/tests/decode.rs
@@ -698,3 +698,65 @@ fn check_decoder_panic_on_mismatched_value() {
let expected = [0x99u8];
check_decoder(decoder, bytes, &expected);
}
+
+#[test]
+#[cfg(feature = "std")]
+#[cfg(feature = "alloc")]
+fn decode_from_read_unbuffered_with_loses_unconsumed_bytes() {
+ use std::io::Cursor;
+
+ use bitcoin_consensus_encoding::{
+ decode_from_read_unbuffered_with, Decode, Decoder, DecoderStatus,
+ };
+
+ #[derive(Default)]
+ struct OneAtATimeDecoder {
+ buf: Vec<u8>,
+ }
+
+ #[derive(Debug, PartialEq)]
+ struct OneAtATime(Vec<u8>);
+
+ #[derive(Debug)]
+ struct OneAtATimeError;
+
+ impl Decoder for OneAtATimeDecoder {
+ type Output = OneAtATime;
+ type Error = OneAtATimeError;
+
+ fn push_bytes(
+ &mut self,
+ bytes: &mut &[u8],
+ ) -> core::result::Result<DecoderStatus, Self::Error> {
+ if self.buf.len() < 4 && !bytes.is_empty() {
+ self.buf.push(bytes[0]);
+ *bytes = &bytes[1..];
+ }
+ if self.buf.len() == 4 {
+ Ok(DecoderStatus::Ready)
+ } else {
+ Ok(DecoderStatus::NeedsMore)
+ }
+ }
+
+ fn end(self) -> core::result::Result<Self::Output, Self::Error> {
+ if self.buf.len() == 4 {
+ Ok(OneAtATime(self.buf))
+ } else {
+ Err(OneAtATimeError)
+ }
+ }
+
+ fn read_limit(&self) -> usize { 4 - self.buf.len() }
+ }
+
+ impl Decode for OneAtATime {
+ type Decoder = OneAtATimeDecoder;
+ }
+
+ let data = [0x11, 0x22, 0x33, 0x44];
+ let cursor = Cursor::new(&data);
+ let decoded: OneAtATime =
+ decode_from_read_unbuffered_with::<_, _, 16>(cursor).expect("decode succeeds");
+ assert_eq!(decoded.0, vec![0x11, 0x22, 0x33, 0x44]);
+}
Why this scored 11/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.