consensus_encoding: Error on byte-less end for VecDecoder
What changed, and why it matters
This commit fixes a behavioral mismatch in a new Bitcoin data-decoding library. Previously, the new decoder would silently accept an empty byte slice as an empty list, while the older code rejected it because it lacked a length prefix. The change makes the new decoder reject empty inputs too, preventing subtle parsing differences that could affect how transactions or network messages are interpreted across versions.
Review other decoder types in `consensus_encoding` for similar lenient-empty-input behavior, add regression tests for truncated length prefixes, and ensure downstream consumers do not rely on the previous permissive behavior.
Security signals we found
Behavioral divergence between old and new consensus decoders
Silent acceptance of truncated/malformed empty inputs
Potential for malleability or deserialization inconsistency in transaction/message parsing
Fix adds explicit EOF validation at decoder finalization
Evidence from the diff
In the rust-bitcoin consensus_encoding crate, VecDecoder<T>::end() previously returned Ok(self.buffer) when no bytes had been consumed, treating an empty input as a zero-length vector. The patch adds a check: if self.prefix_decoder is still present (meaning the compact-size length prefix was never read), it returns an UnexpectedEof error. A regression test verifies that decoding an empty slice without a length prefix now fails. This aligns the new decoder with the legacy Vec<T> consensus encoding behavior.
Changed components
consensus_encoding/src/decode/decoders.rsVecDecoder<T>Compact-size length prefix decodingInspect captured patch +23 / −0
diff --git a/consensus_encoding/src/decode/decoders.rs b/consensus_encoding/src/decode/decoders.rs
index 0afa46a3..f0b01b44 100644
--- a/consensus_encoding/src/decode/decoders.rs
+++ b/consensus_encoding/src/decode/decoders.rs
@@ -265,6 +265,12 @@ impl<T: Decodable> Decoder for VecDecoder<T> {
fn end(self) -> Result<Self::Output, Self::Error> {
use VecDecoderErrorInner as E;
+ if let Some(ref prefix_decoder) = self.prefix_decoder {
+ return Err(VecDecoderError(E::UnexpectedEof(UnexpectedEofError {
+ missing: prefix_decoder.read_limit(),
+ })));
+ }
+
if self.buffer.len() == self.length {
Ok(self.buffer)
} else {
@@ -1078,6 +1084,23 @@ mod tests {
assert_eq!(got, want);
}
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn vec_decoder_empty_no_bytes() {
+ // Empty slice. Note the lack of any length prefix compact size.
+ let encoded = &[];
+
+ let mut slice = encoded.as_slice();
+ let mut decoder = Test::decoder();
+ // Should want more bytes since we've provided nothing
+ assert!(decoder.push_bytes(&mut slice).unwrap());
+
+ assert!(matches!(
+ decoder.end().unwrap_err(),
+ VecDecoderError(VecDecoderErrorInner::UnexpectedEof(_))
+ ));
+ }
+
#[test]
#[cfg(feature = "alloc")]
fn vec_decoder_one_item() {
Why this scored 51/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.