consensus_encoding: fix bug in the composite encoder's exact size
What changed, and why it matters
This commit fixes a bookkeeping bug in a Rust Bitcoin library's composite encoder. The encoder is a tool that combines multiple pieces of data into a single byte stream, and it has a method that is supposed to report how many bytes are left to write. The bug made this method report the total size of all pieces even after some pieces had already been written, which could mislead any code that relies on an accurate remaining-byte count. The fix makes the method only count bytes that have not yet been written.
Review all callers of `ExactSizeEncoder::len()` in consensus-encoding and downstream crates to determine whether the overcounting could have caused incorrect length prefixes, fee calculations, or buffer sizing. Apply the patch and run the new regression test. Consider whether this bug warrants a security advisory if any consensus-relevant serialization could be affected.
Security signals we found
Incorrect implementation of a documented size/length contract
Potential for callers to over-allocate buffers or miscompute transaction/witness sizes
Composite encoder used in consensus-critical serialization paths
No explicit memory-safety or cryptographic weakness introduced by the diff itself
Evidence from the diff
The ExactSizeEncoder::len() method in consensus_encoding is documented to return the number of bytes remaining in the encoder. The macro-generated composite encoders (Encoder2, Encoder3, Encoder4, Encoder6) were unconditionally summing the lengths of all sub-encoders, regardless of how many had already been consumed via advance(). The patch adds a cur_idx guard so only sub-encoders at or beyond the current index contribute to the total. A regression test verifies that after advancing past the first sub-encoder, len() equals the remaining sub-encoder’s length.
Changed components
consensus_encoding/src/encode/encoders.rsExactSizeEncoder trait implementationComposite encoders: Encoder2, Encoder3, Encoder4, Encoder6Inspect captured patch +19 / −1
### consensus_encoding/CHANGELOG.md
@@ -2,6 +2,10 @@
## [Unreleased]
+- 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.
+
## [1.2.0] - 2026-08-11
- Expose lower level encoder/decoder interfaces [#6690](https://github.com/rust-bitcoin/rust-bitcoin/pull/6690)
### consensus_encoding/src/encode/encoders.rs
@@ -244,7 +244,9 @@ macro_rules! define_encoder_n {
{
#[inline]
fn len(&self) -> usize {
- 0 $(+ self.$enc_field.len())*
+ let mut total = 0;
+ $(if self.cur_idx <= $enc_idx { total += self.$enc_field.len(); })*
+ total
}
}
};
### consensus_encoding/tests/encode.rs
@@ -569,3 +569,15 @@ fn drain_hex_multi_chunk() {
let hex = bitcoin_consensus_encoding::drain_to_hex(encoder, hex::Case::Lower);
assert_eq!(hex, "deadbeef");
}
+
+#[test]
+fn check_encoder_composite_exact_size_len_contract() {
+ // After advancing past the first sub-encoder, len() should reflect
+ // only the remaining bytes from the second sub-encoder.
+ let mut e = Encoder2::new(
+ ArrayEncoder::without_length_prefix([0xAA]),
+ ArrayEncoder::without_length_prefix([0xBB, 0xCC]),
+ );
+ let _ = e.advance();
+ assert_eq!(e.len(), 2, "len() should be 2 after first encoder exhausted");
+}Why this scored 37/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.