Implement Decodable for Witness using encoding crate
What changed, and why it matters
This commit replaces the custom code that reads Bitcoin transaction witness data with a shared decoding library. The change is described by the author as a temporary, slightly messy refactor to keep an older decoding helper working while a newer encoding crate takes over. There is no claim in the commit that this fixes a security bug, and the diff itself is mostly code removal and plumbing a new error variant. It could affect how malformed witness data is rejected, but no vulnerability is disclosed or demonstrated.
Treat as a routine refactor. Reviewers should verify that the new `bitcoin-consensus-encoding` witness decoder preserves the previous bounds (e.g., `MAX_VEC_SIZE`) and error behavior, and that dropping `PartialEq`/`Eq` on `ParseError` does not break downstream consumers. No immediate security response is indicated by the supplied materials.
Security signals we found
Refactor of consensus deserialization path for SegWit witness data
Removal of manual compact-size and bounds checks in favor of library implementation
New error variant introduced to wrap witness decoding failures
No security relevance claimed by commit message or diff
No CVE, advisory, or researcher attribution present in supplied materials
Evidence from the diff
The patch removes the hand-written Decodable implementation for Witness in bitcoin/src/blockdata/witness.rs and delegates decoding to io::decode_from_read from the bitcoin-consensus-encoding crate. To propagate failures, it adds a ParseError::Witness(io::ReadError<primitives::witness::WitnessDecoderError>) variant. The DeserializeError and ParseError enums lose their Clone, PartialEq, and Eq derives because the new error type does not implement them. Tests are updated to use matches! instead of equality. The commit adds bitcoin-consensus-encoding as a dependency and wires the std feature.
Changed components
bitcoin/src/blockdata/witness.rsbitcoin/src/consensus/error.rsbitcoin/src/consensus/encode.rsbitcoin/src/consensus/serde.rsbitcoin/Cargo.tomlInspect captured patch +15 / −89
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index 847aea25..c917155d 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -54,6 +54,7 @@ dependencies = [
"base64",
"bech32",
"bincode",
+ "bitcoin-consensus-encoding",
"bitcoin-internals",
"bitcoin-io",
"bitcoin-primitives",
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index 045b2218..cfce4db7 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -53,6 +53,7 @@ dependencies = [
"base64",
"bech32",
"bincode",
+ "bitcoin-consensus-encoding",
"bitcoin-internals",
"bitcoin-io",
"bitcoin-primitives",
diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml
index 9ed73b0b..627db5d5 100644
--- a/bitcoin/Cargo.toml
+++ b/bitcoin/Cargo.toml
@@ -16,7 +16,7 @@ exclude = ["tests", "contrib"]
# If you change features or optional dependencies in any way please update the "# Cargo features" section in lib.rs as well.
[features]
default = [ "std", "secp-recovery" ]
-std = ["base58/std", "bech32/std", "hashes/std", "hex/std", "internals/std", "io/std", "primitives/std", "secp256k1/std", "units/std", "base64?/std", "bitcoinconsensus?/std"]
+std = ["base58/std", "bech32/std", "encoding/std", "hashes/std", "hex/std", "internals/std", "io/std", "primitives/std", "secp256k1/std", "units/std", "base64?/std", "bitcoinconsensus?/std"]
rand = ["secp256k1/rand"]
serde = ["base64", "dep:serde", "hashes/serde", "internals/serde", "primitives/serde", "secp256k1/serde", "units/serde"]
secp-global-context = ["secp256k1/global-context"]
@@ -28,6 +28,7 @@ arbitrary = ["dep:arbitrary", "units/arbitrary", "primitives/arbitrary"]
base58 = { package = "base58ck", path = "../base58", version = "0.2.0", default-features = false, features = ["alloc"] }
bech32 = { version = "0.11.0", default-features = false, features = ["alloc"] }
hashes = { package = "bitcoin_hashes", path = "../hashes", version = "0.18.0", default-features = false, features = ["alloc", "hex"] }
+encoding = { package = "bitcoin-consensus-encoding", path = "../consensus_encoding", version = "=1.0.0-rc.2", default-features = false, features = ["alloc"] }
hex = { package = "hex-conservative", version = "0.3.0", default-features = false, features = ["alloc"] }
internals = { package = "bitcoin-internals", path = "../internals", version = "0.4.2", features = ["alloc", "hex"] }
io = { package = "bitcoin-io", path = "../io", version = "0.3.0", default-features = false, features = ["alloc", "hashes"] }
diff --git a/bitcoin/src/blockdata/witness.rs b/bitcoin/src/blockdata/witness.rs
index db4d0aef..3961fc3e 100644
--- a/bitcoin/src/blockdata/witness.rs
+++ b/bitcoin/src/blockdata/witness.rs
@@ -4,14 +4,12 @@
//!
//! This module contains the [`Witness`] struct and related methods to operate on it
-use internals::compact_size;
use io::{BufRead, Write};
-use crate::consensus::encode::{self, Error, ReadExt, WriteExt, MAX_VEC_SIZE};
+use crate::consensus::encode::{self, Error, ParseError, WriteExt};
use crate::consensus::{Decodable, Encodable};
use crate::crypto::ecdsa;
use crate::crypto::key::SerializedXOnlyPublicKey;
-use crate::prelude::Vec;
use crate::taproot::{self, ControlBlock, LeafScript, TaprootMerkleBranch, TAPROOT_ANNEX_PREFIX};
use crate::{internal_macros, TapScript, WitnessScript};
@@ -25,77 +23,7 @@ pub use primitives::witness::UnexpectedEofError;
impl Decodable for Witness {
fn consensus_decode<R: BufRead + ?Sized>(r: &mut R) -> Result<Self, Error> {
- let witness_elements = r.read_compact_size()? as usize;
- // Minimum size of witness element is 1 byte, so if the count is
- // greater than MAX_VEC_SIZE we must return an error.
- if witness_elements > MAX_VEC_SIZE {
- return Err(encode::ParseError::OversizedVectorAllocation {
- requested: witness_elements,
- max: MAX_VEC_SIZE,
- }
- .into());
- }
- if witness_elements == 0 {
- Ok(Self::default())
- } else {
- // Leave space at the head for element positions.
- // We will rotate them to the end of the Vec later.
- let witness_index_space = witness_elements * 4;
- let mut cursor = witness_index_space;
-
- // this number should be determined as high enough to cover most witness, and low enough
- // to avoid wasting space without reallocating
- let mut content = vec![0u8; cursor + 128];
-
- for i in 0..witness_elements {
- let element_size = r.read_compact_size()? as usize;
- let element_size_len = compact_size::encoded_size(element_size);
- let required_len = cursor
- .checked_add(element_size)
- .ok_or(encode::Error::Parse(encode::ParseError::OversizedVectorAllocation {
- requested: usize::MAX,
- max: MAX_VEC_SIZE,
- }))?
- .checked_add(element_size_len)
- .ok_or(encode::Error::Parse(encode::ParseError::OversizedVectorAllocation {
- requested: usize::MAX,
- max: MAX_VEC_SIZE,
- }))?;
-
- if required_len > MAX_VEC_SIZE + witness_index_space {
- return Err(encode::ParseError::OversizedVectorAllocation {
- requested: required_len,
- max: MAX_VEC_SIZE,
- }
- .into());
- }
-
- // We will do content.rotate_left(witness_index_space) later.
- // Encode the position's value AFTER we rotate left.
- encode_cursor(&mut content, 0, i, cursor - witness_index_space);
-
- resize_if_needed(&mut content, required_len);
- cursor += (&mut content[cursor..cursor + element_size_len])
- .emit_compact_size(element_size)?;
- r.read_exact(&mut content[cursor..cursor + element_size])?;
- cursor += element_size;
- }
- content.truncate(cursor);
- // Index space is now at the end of the Vec
- content.rotate_left(witness_index_space);
- let indices_start = cursor - witness_index_space;
- Ok(Self::from_parts__unstable(content, witness_elements, indices_start))
- }
- }
-}
-
-fn resize_if_needed(vec: &mut Vec<u8>, required_len: usize) {
- if required_len >= vec.len() {
- let mut new_len = vec.len().max(1);
- while new_len <= required_len {
- new_len *= 2;
- }
- vec.resize(new_len, 0);
+ io::decode_from_read(r).map_err(|e| Error::Parse(ParseError::Witness(e)))
}
}
@@ -307,16 +235,6 @@ mod sealed {
impl Sealed for super::Witness {}
}
-/// Correctness Requirements: value must always fit within u32
-// This is duplicated in `primitives::witness`, if you change it please do so over there also.
-#[inline]
-fn encode_cursor(bytes: &mut [u8], start_of_indices: usize, index: usize, value: usize) {
- let start = start_of_indices + index * 4;
- let end = start + 4;
- bytes[start..end]
- .copy_from_slice(&u32::to_ne_bytes(value.try_into().expect("larger than u32")));
-}
-
#[cfg(test)]
mod test {
use hex_lit::hex;
diff --git a/bitcoin/src/consensus/encode.rs b/bitcoin/src/consensus/encode.rs
index c9edbc6b..f3d56cd1 100644
--- a/bitcoin/src/consensus/encode.rs
+++ b/bitcoin/src/consensus/encode.rs
@@ -1003,7 +1003,7 @@ mod tests {
// Check serialization that `if len > MAX_VEC_SIZE {return err}` isn't inclusive,
// by making sure it fails with `MissingData` and not an `OversizedVectorAllocation` Error.
let err = deserialize::<BlockHash>(&serialize(&(super::MAX_VEC_SIZE as u32))).unwrap_err();
- assert_eq!(err, DeserializeError::Parse(ParseError::MissingData));
+ assert!(matches!(err, DeserializeError::Parse(ParseError::MissingData)));
test_len_is_max_vec::<u8>();
test_len_is_max_vec::<BlockHash>();
@@ -1023,7 +1023,7 @@ mod tests {
let mut buf = Vec::new();
buf.emit_compact_size(super::MAX_VEC_SIZE / mem::size_of::<T>()).unwrap();
let err = deserialize::<Vec<T>>(&buf).unwrap_err();
- assert_eq!(err, DeserializeError::Parse(ParseError::MissingData));
+ assert!(matches!(err, DeserializeError::Parse(ParseError::MissingData)));
}
#[test]
diff --git a/bitcoin/src/consensus/error.rs b/bitcoin/src/consensus/error.rs
index 60a053a4..d16b1a3f 100644
--- a/bitcoin/src/consensus/error.rs
+++ b/bitcoin/src/consensus/error.rs
@@ -12,7 +12,7 @@ use internals::write_err;
use super::IterReader;
/// Error deserializing from a slice.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug)]
#[non_exhaustive]
pub enum DeserializeError {
/// Error parsing encoded object.
@@ -136,7 +136,7 @@ impl From<ParseError> for Error {
}
/// Encoding is invalid.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug)]
#[non_exhaustive]
pub enum ParseError {
/// Missing data (early end of file or slice too short).
@@ -161,6 +161,8 @@ pub enum ParseError {
ParseFailed(&'static str),
/// Unsupported SegWit flag.
UnsupportedSegwitFlag(u8),
+ /// Witness decoding error.
+ Witness(io::ReadError<primitives::witness::WitnessDecoderError>),
}
impl From<Infallible> for ParseError {
@@ -182,6 +184,7 @@ impl fmt::Display for ParseError {
Self::ParseFailed(ref s) => write!(f, "parse failed: {}", s),
Self::UnsupportedSegwitFlag(ref swflag) =>
write!(f, "unsupported SegWit version: {}", swflag),
+ Self::Witness(ref e) => write_err!(f, "witness"; e),
}
}
}
@@ -190,6 +193,7 @@ impl fmt::Display for ParseError {
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
+ Self::Witness(ref e) => Some(e),
Self::MissingData
| Self::OversizedVectorAllocation { .. }
| Self::InvalidChecksum { .. }
diff --git a/bitcoin/src/consensus/serde.rs b/bitcoin/src/consensus/serde.rs
index d673085b..79a3e7d7 100644
--- a/bitcoin/src/consensus/serde.rs
+++ b/bitcoin/src/consensus/serde.rs
@@ -378,6 +378,7 @@ fn consensus_error_into_serde<E: serde::de::Error>(error: ParseError) -> E {
ParseError::ParseFailed(msg) => E::custom(msg),
ParseError::UnsupportedSegwitFlag(flag) =>
E::invalid_value(Unexpected::Unsigned(flag.into()), &"segwit version 1 flag"),
+ ParseError::Witness(e) => E::custom(e),
}
}
Why this scored 34/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.