consensus_encoding: bump compact size default decoding limit
What changed, and why it matters
This commit changes the default maximum decoded size for a compact-size length prefix from 4 MB to 32 MB in the rust-bitcoin library. To avoid accidentally allowing larger memory allocations, every existing place that used the old default is switched to an explicit 4 MB limit. The change is framed as matching Bitcoin Core's anti-DoS setting, not as fixing a known vulnerability. It could affect how downstream code behaves if it relied on the old 4 MB default without setting its own limit.
Review any downstream code that constructs CompactSizeDecoder::new() directly and confirm it is acceptable for it to inherit the new 32 MB default. If stricter limits are needed, switch such call sites to CompactSizeDecoder::new_with_limit(). No immediate patch is required because existing internal uses retain their previous 4 MB limits.
Security signals we found
Change to resource/DoS decoding limit
Explicit preservation of old limits at all call sites
Reference to Bitcoin Core anti-DoS serialization limit
Potential downstream behavior change for code using CompactSizeDecoder::new() directly
Evidence from the diff
The patch renames MAX_VEC_SIZE to MAX_COMPACT_SIZE and raises it from 4,000,000 to 0x0200_0000 (33,554,432 bytes, i.e., 32 MiB) in consensus_encoding/src/compact_size.rs. CompactSizeDecoder::new() now uses this 32 MB default. To preserve prior behavior, ByteVecDecoder, VecDecoder, BIP152 PrefilledTransaction/Offset decoders, and WitnessDecoder are all switched to CompactSizeDecoder::new_with_limit(4_000_000) or tighter witness-specific limits. The commit notes this breaks 16-bit platforms because the constant no longer fits in usize.
Changed components
consensus_encoding/src/compact_size.rsconsensus_encoding/src/decode/decoders.rsp2p/src/bip152.rsprimitives/src/witness.rsInspect captured patch +56 / −25
diff --git a/consensus_encoding/src/compact_size.rs b/consensus_encoding/src/compact_size.rs
index 77806065..46530920 100644
--- a/consensus_encoding/src/compact_size.rs
+++ b/consensus_encoding/src/compact_size.rs
@@ -14,11 +14,14 @@ use crate::error::{
CompactSizeDecoderError, CompactSizeDecoderErrorInner, LengthPrefixExceedsMaxError,
};
-/// Maximum size, in bytes, of a vector we are allowed to decode.
+/// Default maximum size of a decoded object in bytes.
///
-/// This is also the default value limit that can be decoded with a decoder from
-/// [`CompactSizeDecoder::new`].
-pub(crate) const MAX_VEC_SIZE: usize = 4_000_000;
+/// Matches Bitcoin Core's default [serialization limit]. This is
+/// a high level anti-DoS limit which all bitcoin types should
+/// easily fit within.
+///
+/// [serialization limit]: https://github.com/bitcoin/bitcoin/blob/a7c29df0e5ace05b6186612671d6103c112ec922/src/serialize.h#L32
+const MAX_COMPACT_SIZE: usize = 0x0200_0000;
/// The maximum length of a compact size encoding.
const SIZE: usize = 9;
@@ -142,18 +145,14 @@ pub struct CompactSizeDecoder {
}
impl CompactSizeDecoder {
- /// Constructs a new compact size decoder with the default length limit.
- ///
- /// The decoded value must not exceed 4,000,000 and must fit in a `usize`, otherwise
- /// [`end`](Self::end) will return an error. This default limit reflects the maximum sensible
- /// vector length under the 4 MB block weight limit.
- pub const fn new() -> Self { Self { buf: ArrayVec::new(), limit: MAX_VEC_SIZE } }
+ /// Constructs a new compact size decoder with the default 32MB length limit.
+ pub const fn new() -> Self { Self { buf: ArrayVec::new(), limit: MAX_COMPACT_SIZE } }
/// Constructs a new compact size decoder with a custom length limit.
///
/// The decoded value must not exceed `limit`, otherwise [`end`](Self::end) will return an
/// error. Use this when you know the field you are decoding has a tighter bound than the
- /// default limit of 4,000,000.
+ /// default limit of 32MB.
pub const fn new_with_limit(limit: usize) -> Self { Self { buf: ArrayVec::new(), limit } }
}
@@ -377,23 +376,25 @@ mod tests {
fn compact_size_new_values_too_large() {
use CompactSizeDecoderErrorInner as E;
- const EXCESS_VEC_SIZE: u64 = (MAX_VEC_SIZE + 1) as u64; // can't use try_from for const
+ const EXCESS_COMPACT_SIZE: u64 = (MAX_COMPACT_SIZE + 1) as u64;
- // MAX_VEC_SIZE should succeed for `new` constructor
+ // MAX_COMPACT_SIZE should succeed for `new` constructor
+ // 0x0200_0000 as minimal 5-byte compact size: 0xFE + u32 little-endian
let mut decoder = CompactSizeDecoder::new();
- decoder.push_bytes(&mut [0xFE, 0x00, 0x09, 0x3D, 0x00].as_slice()).unwrap();
+ decoder.push_bytes(&mut [0xFE, 0x00, 0x00, 0x00, 0x02].as_slice()).unwrap();
let got = decoder.end().unwrap();
- assert_eq!(got, MAX_VEC_SIZE);
+ assert_eq!(got, MAX_COMPACT_SIZE);
- // MAX_VEC_SIZE + 1 should fail for `new` constructor
+ // MAX_COMPACT_SIZE + 1 should fail for `new` constructor
+ // 0x0200_0001 as minimal 5-byte compact size: 0xFE + u32 little-endian
let mut decoder = CompactSizeDecoder::new();
- decoder.push_bytes(&mut [0xFE, 0x01, 0x09, 0x3D, 0x00].as_slice()).unwrap();
+ decoder.push_bytes(&mut [0xFE, 0x01, 0x00, 0x00, 0x02].as_slice()).unwrap();
let got = decoder.end().unwrap_err();
assert!(matches!(
got,
CompactSizeDecoderError(E::ValueExceedsLimit(LengthPrefixExceedsMaxError {
- limit: MAX_VEC_SIZE,
- value: EXCESS_VEC_SIZE,
+ limit: MAX_COMPACT_SIZE,
+ value: EXCESS_COMPACT_SIZE,
})),
));
}
diff --git a/consensus_encoding/src/decode/decoders.rs b/consensus_encoding/src/decode/decoders.rs
index 16db3f4a..8aa581c8 100644
--- a/consensus_encoding/src/decode/decoders.rs
+++ b/consensus_encoding/src/decode/decoders.rs
@@ -21,6 +21,14 @@ use crate::{Decoder2Error, Decoder3Error, Decoder4Error, Decoder6Error, Unexpect
#[cfg(feature = "alloc")]
const MAX_VECTOR_ALLOCATE: usize = 1_000_000;
+/// Maximum number of elements in a decoded vector.
+///
+/// This is an anti-DoS limit based on Bitcoin's 4MB block weight limit.
+/// Applied to both byte vectors [`ByteVecDecoder`] and typed vectors [`VecDecoder`],
+/// regardless of whether the element type is a single byte or a larger structure.
+#[cfg(feature = "alloc")]
+const MAX_VEC_SIZE: usize = 4_000_000;
+
/// A decoder that decodes a byte vector.
///
/// The encoding is expected to start with the number of encoded bytes (length prefix).
@@ -38,7 +46,7 @@ impl ByteVecDecoder {
/// Constructs a new byte decoder.
pub const fn new() -> Self {
Self {
- prefix_decoder: Some(CompactSizeDecoder::new()),
+ prefix_decoder: Some(CompactSizeDecoder::new_with_limit(MAX_VEC_SIZE)),
buffer: Vec::new(),
bytes_expected: 0,
bytes_written: 0,
@@ -180,7 +188,7 @@ impl<T: Decodable> VecDecoder<T> {
/// Constructs a new byte decoder.
pub const fn new() -> Self {
Self {
- prefix_decoder: Some(CompactSizeDecoder::new()),
+ prefix_decoder: Some(CompactSizeDecoder::new_with_limit(MAX_VEC_SIZE)),
length: 0,
buffer: Vec::new(),
decoder: None,
diff --git a/p2p/src/bip152.rs b/p2p/src/bip152.rs
index 52c6ccb5..b4805233 100644
--- a/p2p/src/bip152.rs
+++ b/p2p/src/bip152.rs
@@ -25,6 +25,12 @@ use io::{BufRead, Write};
use primitives::block::{BlockHashDecoder, BlockHashEncoder, Header, HeaderDecoder, HeaderEncoder};
use primitives::transaction::{TransactionDecoder, TransactionEncoder};
+/// Maximum number of elements in a vector.
+///
+/// This is an anti-DoS limit which won't possibly reject any block,
+/// or part of a block, on the network.
+const MAX_VEC_SIZE: usize = 4_000_000;
+
/// A BIP-0152 error
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
@@ -138,7 +144,7 @@ impl encoding::Decodable for PrefilledTransaction {
fn decoder() -> Self::Decoder {
PrefilledTransactionDecoder(Decoder2::new(
- CompactSizeDecoder::new(),
+ CompactSizeDecoder::new_with_limit(MAX_VEC_SIZE),
TransactionDecoder::new(),
))
}
@@ -630,7 +636,9 @@ impl encoding::Decoder for OffsetDecoder {
impl encoding::Decodable for Offset {
type Decoder = OffsetDecoder;
- fn decoder() -> Self::Decoder { OffsetDecoder(CompactSizeDecoder::new()) }
+ fn decoder() -> Self::Decoder {
+ OffsetDecoder(CompactSizeDecoder::new_with_limit(MAX_VEC_SIZE))
+ }
}
/// A [`BlockTransactionsRequest`] structure is used to list transaction indexes
diff --git a/primitives/src/witness.rs b/primitives/src/witness.rs
index 9b846cd7..8af111bb 100644
--- a/primitives/src/witness.rs
+++ b/primitives/src/witness.rs
@@ -30,6 +30,20 @@ use crate::TxIn;
#[cfg(feature = "alloc")]
const MAX_VECTOR_ALLOCATE: usize = 1_000_000;
+/// Maximum number of items in a witness stack.
+///
+/// This is an anti-DoS limit based on Bitcoin's 4MB block weight limit.
+/// Witness data is part of transactions, which are part of blocks, so witness
+/// items (assuming 1-byte per item) cannot exceed what fits in a block.
+const MAX_WITNESS_STACK_ITEMS: usize = 4_000_000;
+
+/// Maximum byte size of a single witness stack item.
+///
+/// This is an anti-DoS limit based on Bitcoin's 4MB block weight limit.
+/// Witness data is part of transactions, which are part of blocks, so a
+/// single witness item cannot exceed what fits in a block.
+const MAX_WITNESS_ITEM_SIZE: usize = 4_000_000;
+
/// The Witness is the data used to unlock bitcoin since the [SegWit upgrade].
///
/// Can be logically seen as an array of bytestrings, i.e. `Vec<Vec<u8>>`, and it is serialized on the wire
@@ -315,9 +329,9 @@ impl WitnessDecoder {
content: Vec::new(),
cursor: 0,
witness_elements: None,
- witness_count_decoder: CompactSizeDecoder::new(),
+ witness_count_decoder: CompactSizeDecoder::new_with_limit(MAX_WITNESS_STACK_ITEMS),
element_idx: 0,
- element_length_decoder: CompactSizeDecoder::new(),
+ element_length_decoder: CompactSizeDecoder::new_with_limit(MAX_WITNESS_ITEM_SIZE),
element_bytes_remaining: None,
}
}
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.