Merge rust-bitcoin/rust-bitcoin#6906: consensus_encoding, primitives: expose exact encoding size for block and transaction
What changed, and why it matters
This commit adds a way to ask, in advance, exactly how many bytes a Bitcoin block or transaction will take when serialized. It is a feature addition for the library's encoding system, not a fix for a vulnerability. There is no indication it addresses a security bug or was triggered by a security report.
No security action required. Treat as a normal feature/API enhancement. Reviewers may optionally verify that the new len() implementations correctly account for all bytes (including length prefixes) and do not introduce panics on large collections, but this is code-quality rather than security.
Security signals we found
No security-relevant signals in commit message or diff
Feature addition: expose exact encoded size
No mention of vulnerability, CVE, bug bounty, or security report
No bounds/overflow/underflow fixes observed
Evidence from the diff
The change exposes ExactSizeEncoder for BlockEncoder and TransactionEncoder by implementing exact-size logic for SliceEncoder, PrefixedSliceEncoder, and IterEncoder. It also refactors WitnessesEncoder to use IterEncoder
Changed components
consensus_encoding/src/encode/encoders.rsconsensus_encoding/src/encode/iter.rsprimitives/src/block.rsprimitives/src/transaction.rsInspect captured patch +129 / −106
### consensus_encoding/CHANGELOG.md
@@ -5,6 +5,11 @@
- Fix `ExactSizeEncoder::len()` overcounting in composite encoders (`Encoder2`/`3`/`4`/`6`) after
sub-encoders are exhausted. The `len()` method now correctly reports only the remaining bytes
rather than unconditionally summing all sub-encoder lengths.
+- Add `ExactSizeEncoder` implementations for `IterEncoder`, `SliceEncoder`, and
+ `PrefixedSliceEncoder`. When the inner encoder types implement `ExactSizeEncoder`, these
+ encoder combinators now provide exact size information, cascading automatically through the
+ encoder composition tree. Enables higher level protocols like PSBT and BIP324 to obtain the
+ encoded size of variable-length collections (e.g. `Transaction` inputs/outputs).
## [1.2.0] - 2026-08-11
### consensus_encoding/src/encode/encoders.rs
@@ -149,6 +149,14 @@ impl<T: Encode> Encoder for SliceEncoder<'_, T> {
fn advance(&mut self) -> EncoderStatus { self.0.advance() }
}
+impl<'e, T: Encode> ExactSizeEncoder for SliceEncoder<'e, T>
+where
+ T::Encoder<'e>: ExactSizeEncoder,
+{
+ #[inline]
+ fn len(&self) -> usize { self.0.len() }
+}
+
/// An encoder for a list of consensus encodable types, including a length prefix.
pub struct PrefixedSliceEncoder<'e, T: Encode>(Encoder2<CompactSizeEncoder, SliceEncoder<'e, T>>);
@@ -187,6 +195,14 @@ impl<T: Encode> Encoder for PrefixedSliceEncoder<'_, T> {
fn advance(&mut self) -> EncoderStatus { self.0.advance() }
}
+impl<'e, T: Encode> ExactSizeEncoder for PrefixedSliceEncoder<'e, T>
+where
+ T::Encoder<'e>: ExactSizeEncoder,
+{
+ #[inline]
+ fn len(&self) -> usize { self.0.len() }
+}
+
/// Helper macro to define an unrolled `EncoderN` composite encoder.
macro_rules! define_encoder_n {
(
### consensus_encoding/src/encode/iter.rs
@@ -2,7 +2,7 @@
use core::fmt;
-use super::{Encode, Encoder, EncoderStatus};
+use super::{Encode, Encoder, EncoderStatus, ExactSizeEncoder};
/// An iterator bridge which maps consensus encodable items to its encoder.
///
@@ -143,3 +143,23 @@ where
}
}
}
+
+impl<I: Iterator> ExactSizeEncoder for IterEncoder<I>
+where
+ I: Clone,
+ I::Item: Encoder + ExactSizeEncoder,
+{
+ fn len(&self) -> usize {
+ match &self.state {
+ EncoderState::Encoding { current, remaining } => {
+ let mut total = current.len();
+ let remaining = remaining.clone();
+ for item in remaining {
+ total += item.len();
+ }
+ total
+ }
+ EncoderState::Done => 0,
+ }
+ }
+}
### consensus_encoding/tests/encode.rs
@@ -314,6 +314,7 @@ fn encode_slice_with_elements() {
let slice = &[TestArray([0x34, 0x12, 0x00, 0x00]), TestArray([0x78, 0x56, 0x00, 0x00])];
let mut encoder = SliceEncoder::without_length_prefix(slice);
+ assert_eq!(encoder.len(), 8);
check_encoder(&mut encoder, &[0x34, 0x12, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00]);
}
@@ -341,6 +342,7 @@ fn encode_slice_with_prefix() {
let slice = &[TestArray([0x34, 0x12, 0x00, 0x00]), TestArray([0x78, 0x56, 0x00, 0x00])];
let mut encoder = PrefixedSliceEncoder::new(slice);
+ assert_eq!(encoder.len(), 9);
check_encoder(&mut encoder, &[0x02, 0x34, 0x12, 0x00, 0x00, 0x78, 0x56, 0x00, 0x00]);
}
@@ -359,6 +361,7 @@ fn encode_slice_with_prefix_and_zero_sized_arrays() {
let slice = &[TestArray([]), TestArray([])];
let mut encoder = PrefixedSliceEncoder::new(slice);
+ assert_eq!(encoder.len(), 1);
check_encoder(&mut encoder, &[0x02]);
}
### primitives/CHANGELOG.md
@@ -2,6 +2,9 @@
## [Unreleased]
+- `TransactionEncoder` and `BlockEncoder` now implement `ExactSizeEncoder`, enabling callers to
+ obtain the exact encoded size of transactions and blocks before serialization.
+
# [0.103.1] - 2026-08-06
- Explicitly depend on `consensus-encoding 1.1.0`
### primitives/src/block.rs
@@ -567,7 +567,7 @@ impl encoding::Decode for Block<Unchecked> {
}
#[cfg(feature = "alloc")]
-encoding::encoder_newtype! {
+encoding::encoder_newtype_exact! {
/// The encoder for the [`Block`] type.
#[derive(Debug, Clone)]
pub struct BlockEncoder<'e>(
### primitives/src/transaction.rs
@@ -68,8 +68,8 @@ use encoding::FromHexError;
use encoding::{ArrayEncoder, BytesEncoder, Encoder2};
#[cfg(feature = "alloc")]
use encoding::{
- Decoder2, Decoder3, DecoderStatus, Encode as _, Encoder3, Encoder6, EncoderStatus,
- PrefixedSliceEncoder, VecDecoder,
+ Decoder2, Decoder3, DecoderStatus, Encode as _, Encoder3, Encoder6,
+ IterEncoder, PrefixedSliceEncoder, VecDecoder,
};
#[cfg(feature = "alloc")]
use hashes::sha256d;
@@ -434,7 +434,7 @@ impl encoding::Encode for Transaction {
if self.uses_segwit_serialization() {
let segwit = ArrayEncoder::without_length_prefix([0x00, 0x01]);
- let witnesses = WitnessesEncoder::new(self.inputs.as_slice());
+ let witnesses = WitnessesEncoder::from_inputs(self.inputs.as_slice());
TransactionEncoder::new(Encoder6::new(
version,
Some(segwit),
@@ -465,7 +465,7 @@ type TransactionEncoderInner<'e> = Encoder6<
>;
#[cfg(feature = "alloc")]
-encoding::encoder_newtype! {
+encoding::encoder_newtype_exact! {
/// The encoder for the [`Transaction`] type.
#[derive(Debug, Clone)]
pub struct TransactionEncoder<'e>(TransactionEncoderInner<'e>);
@@ -746,57 +746,35 @@ enum IsSegwit {
No,
}
-/// Encodes the witnesses from a list of inputs.
+/// An iterator that yields [`WitnessEncoder`]s for each transaction input.
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)]
-struct WitnessesEncoder<'e> {
- inputs: &'e [TxIn],
- /// Encoder for the current witness being encoded.
- cur_enc: Option<WitnessEncoder<'e>>,
+struct WitnessIter<'e> {
+ inputs: core::slice::Iter<'e, TxIn>,
}
#[cfg(feature = "alloc")]
-impl<'e> WitnessesEncoder<'e> {
- /// Constructs a new encoder for all witnesses in a list of transaction inputs.
- #[inline]
- pub fn new(inputs: &'e [TxIn]) -> Self {
- Self { inputs, cur_enc: inputs.first().map(|input| input.witness.encoder()) }
+impl<'e> Iterator for WitnessIter<'e> {
+ type Item = WitnessEncoder<'e>;
+
+ fn next(&mut self) -> Option<WitnessEncoder<'e>> {
+ self.inputs.next().map(|input| input.witness.encoder())
}
}
#[cfg(feature = "alloc")]
-impl encoding::Encoder for WitnessesEncoder<'_> {
- #[inline]
- fn current_chunk(&self) -> &[u8] {
- self.cur_enc.as_ref().map(WitnessEncoder::current_chunk).unwrap_or_default()
- }
+encoding::encoder_newtype_exact! {
+ /// Encodes the witnesses from a list of inputs.
+ #[derive(Debug, Clone)]
+ struct WitnessesEncoder<'e>(IterEncoder<WitnessIter<'e>>);
+}
+#[cfg(feature = "alloc")]
+impl<'e> WitnessesEncoder<'e> {
+ /// Constructs a new encoder for all witnesses in a list of transaction inputs.
#[inline]
- fn advance(&mut self) -> EncoderStatus {
- let Some(cur) = self.cur_enc.as_mut() else {
- return EncoderStatus::Finished;
- };
-
- loop {
- // On subsequent calls, attempt to advance the current encoder and return
- // success if this succeeds.
- if cur.advance().has_more() {
- return EncoderStatus::HasMore;
- }
- // self.inputs guaranteed to be non-empty if cur_enc is non-None.
- self.inputs = &self.inputs[1..];
-
- // If advancing the current encoder failed, attempt to move to the next encoder.
- if let Some(input) = self.inputs.first() {
- *cur = input.witness.encoder();
- if !cur.current_chunk().is_empty() {
- return EncoderStatus::HasMore;
- }
- } else {
- self.cur_enc = None; // shortcut the next call to advance()
- return EncoderStatus::Finished;
- }
- }
+ pub fn from_inputs(inputs: &'e [TxIn]) -> Self {
+ Self::new(IterEncoder::new(WitnessIter { inputs: inputs.iter() }))
}
}
@@ -2558,7 +2536,7 @@ mod tests {
#[test]
#[cfg(feature = "alloc")]
fn witnesses_encoder_empty_inputs() {
- let mut encoder = WitnessesEncoder::new(&[]);
+ let mut encoder = WitnessesEncoder::from_inputs(&[]);
encoding::check_encoder(&mut encoder, &[]);
}
### primitives/tests/encoding.rs
@@ -13,7 +13,7 @@ use bitcoin_primitives::{
absolute, Amount, Block, BlockHash, BlockHeader, BlockTime, BlockVersion, CompactTarget,
ScriptPubKeyBuf, ScriptSigBuf, Sequence, Witness,
};
-use encoding::{check_encode, Decode as _, Decoder as _};
+use encoding::{check_encode, Decode as _, Decoder as _, Encode as _, ExactSizeEncoder as _};
use hex::hex;
const TC_TXID_BYTES: [u8; 32] = [
@@ -120,26 +120,25 @@ fn encode_segwit_transaction() {
outputs: vec![tx_out()],
};
- check_encode(
- &tx,
- &concat_slices!(
- &[2u8, 0, 0, 0],
- &TC_SEGWIT_MARKER_AND_FLAG,
- &[1u8],
- &TC_TXID_BYTES,
- &TC_VOUT_BYTES,
- &[3u8],
- &TC_SCRIPT_BYTES,
- &TC_SEQ_MAX_BYTES,
- &[1u8],
- &TC_ONE_SAT_BYTES,
- &[3u8],
- &TC_SCRIPT_BYTES,
- &[1u8],
- &TC_WITNESS_ELEM_LEN_AND_DATA,
- &TC_LOCK_TIME_ZERO_BYTES
- ),
+ let expected = concat_slices!(
+ &[2u8, 0, 0, 0],
+ &TC_SEGWIT_MARKER_AND_FLAG,
+ &[1u8],
+ &TC_TXID_BYTES,
+ &TC_VOUT_BYTES,
+ &[3u8],
+ &TC_SCRIPT_BYTES,
+ &TC_SEQ_MAX_BYTES,
+ &[1u8],
+ &TC_ONE_SAT_BYTES,
+ &[3u8],
+ &TC_SCRIPT_BYTES,
+ &[1u8],
+ &TC_WITNESS_ELEM_LEN_AND_DATA,
+ &TC_LOCK_TIME_ZERO_BYTES
);
+ assert_eq!(tx.encoder().len(), expected.len());
+ check_encode(&tx, &expected);
}
#[test]
@@ -194,46 +193,45 @@ fn encode_block() {
};
let block = Block::new_unchecked(header, vec![tx]);
- check_encode(
- &block,
- &concat_slices!(
- // The block version.
- &[2u8, 0, 0, 0],
- // The previous block's blockhash.
- &[
- 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171,
- 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171
- ],
- &[
- 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
- 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205
- ],
- // The block time.
- &[80, 195, 137, 98],
- // The target (bits).
- &[239, 190, 0, 0],
- // The nonce.
- &[254, 202, 0, 0],
- // The transaction list length prefix.
- &[1u8],
- // The transaction (same as tested above).
- &[2u8, 0, 0, 0],
- &TC_SEGWIT_MARKER_AND_FLAG,
- &[1u8],
- &TC_TXID_BYTES,
- &TC_VOUT_BYTES,
- &[3u8],
- &TC_SCRIPT_BYTES,
- &TC_SEQ_MAX_BYTES,
- &[1u8],
- &TC_ONE_SAT_BYTES,
- &[3u8],
- &TC_SCRIPT_BYTES,
- &[1u8],
- &TC_WITNESS_ELEM_LEN_AND_DATA,
- &TC_LOCK_TIME_ZERO_BYTES
- ),
+ let expected = concat_slices!(
+ // The block version.
+ &[2u8, 0, 0, 0],
+ // The previous block's blockhash.
+ &[
+ 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171,
+ 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171
+ ],
+ &[
+ 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205,
+ 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205
+ ],
+ // The block time.
+ &[80, 195, 137, 98],
+ // The target (bits).
+ &[239, 190, 0, 0],
+ // The nonce.
+ &[254, 202, 0, 0],
+ // The transaction list length prefix.
+ &[1u8],
+ // The transaction (same as tested above).
+ &[2u8, 0, 0, 0],
+ &TC_SEGWIT_MARKER_AND_FLAG,
+ &[1u8],
+ &TC_TXID_BYTES,
+ &TC_VOUT_BYTES,
+ &[3u8],
+ &TC_SCRIPT_BYTES,
+ &TC_SEQ_MAX_BYTES,
+ &[1u8],
+ &TC_ONE_SAT_BYTES,
+ &[3u8],
+ &TC_SCRIPT_BYTES,
+ &[1u8],
+ &TC_WITNESS_ELEM_LEN_AND_DATA,
+ &TC_LOCK_TIME_ZERO_BYTES
);
+ assert_eq!(block.encoder().len(), expected.len());
+ check_encode(&block, &expected);
}
#[test]Why this scored 20/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.