consensus_encoding: add common trait implementations
What changed, and why it matters
This commit adds standard Rust helper traits (Debug for printing, Clone for copying) to the library's byte encoders and decoders. It also adds a few new unit tests. There is no security fix or vulnerability here; it is a routine API-quality improvement.
No security action needed. Review as normal API-maintenance change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change derives or manually implements Debug and Clone for various encoder/decoder structs in consensus_encoding. Manual impls are used where derive would add unwanted bounds (e.g., T: Clone when only T::Encoder is cloned). Composite decoders (Decoder2/3/4) only get Debug, not Clone, to avoid Clone bounds on intermediate outputs. Added tests cover empty/one/two-item vector decoding and cloning a decoder mid-decode. No behavioral logic changes that would affect consensus parsing or serialization.
Changed components
consensus_encoding/src/decode/decoders.rsconsensus_encoding/src/encode/encoders.rsconsensus_encoding/src/encode/mod.rsconsensus_encoding/tests/decode.rsInspect captured patch +187 / −2
diff --git a/consensus_encoding/src/decode/decoders.rs b/consensus_encoding/src/decode/decoders.rs
index cb2402bf..36fba50e 100644
--- a/consensus_encoding/src/decode/decoders.rs
+++ b/consensus_encoding/src/decode/decoders.rs
@@ -28,6 +28,7 @@ const MAX_VECTOR_ALLOCATE: usize = 1_000_000;
///
/// The encoding is expected to start with the number of encoded bytes (length prefix).
#[cfg(feature = "alloc")]
+#[derive(Debug, Clone)]
pub struct ByteVecDecoder {
prefix_decoder: Option<CompactSizeDecoder>,
buffer: Vec<u8>,
@@ -145,6 +146,38 @@ pub struct VecDecoder<T: Decodable> {
decoder: Option<<T as Decodable>::Decoder>,
}
+#[cfg(feature = "alloc")]
+impl<T: Decodable> fmt::Debug for VecDecoder<T>
+where
+ T::Decoder: fmt::Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("VecDecoder")
+ .field("prefix_decoder", &self.prefix_decoder)
+ .field("length", &self.length)
+ // Print the count rather than contents to avoid requiring `T: Debug`.
+ .field("buffer_len", &self.buffer.len())
+ .field("decoder", &self.decoder)
+ .finish()
+ }
+}
+
+#[cfg(feature = "alloc")]
+impl<T: Decodable> Clone for VecDecoder<T>
+where
+ T: Clone,
+ T::Decoder: Clone,
+{
+ fn clone(&self) -> Self {
+ Self {
+ prefix_decoder: self.prefix_decoder.clone(),
+ length: self.length,
+ buffer: self.buffer.clone(),
+ decoder: self.decoder.clone(),
+ }
+ }
+}
+
#[cfg(feature = "alloc")]
impl<T: Decodable> VecDecoder<T> {
/// Constructs a new byte decoder.
@@ -264,6 +297,7 @@ impl<T: Decodable> Decoder for VecDecoder<T> {
}
/// A decoder that expects exactly N bytes and returns them as an array.
+#[derive(Debug, Clone)]
pub struct ArrayDecoder<const N: usize> {
buffer: [u8; N],
bytes_written: usize,
@@ -340,6 +374,23 @@ where
}
}
+impl<A, B> fmt::Debug for Decoder2<A, B>
+where
+ A: Decoder + fmt::Debug,
+ B: Decoder + fmt::Debug,
+ A::Output: fmt::Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match &self.state {
+ Decoder2State::First(a, b) =>
+ f.debug_tuple("First").field(a).field(b).finish(),
+ Decoder2State::Second(out, b) =>
+ f.debug_tuple("Second").field(out).field(b).finish(),
+ Decoder2State::Errored => write!(f, "Errored"),
+ }
+ }
+}
+
impl<A, B> Decoder for Decoder2<A, B>
where
A: Decoder,
@@ -435,6 +486,17 @@ where
}
}
+impl<A, B, C> fmt::Debug for Decoder3<A, B, C>
+where
+ A: Decoder + fmt::Debug,
+ B: Decoder + fmt::Debug,
+ C: Decoder + fmt::Debug,
+ A::Output: fmt::Debug,
+ B::Output: fmt::Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.inner.fmt(f) }
+}
+
impl<A, B, C> Decoder for Decoder3<A, B, C>
where
A: Decoder,
@@ -493,6 +555,19 @@ where
}
}
+impl<A, B, C, D> fmt::Debug for Decoder4<A, B, C, D>
+where
+ A: Decoder + fmt::Debug,
+ B: Decoder + fmt::Debug,
+ C: Decoder + fmt::Debug,
+ D: Decoder + fmt::Debug,
+ A::Output: fmt::Debug,
+ B::Output: fmt::Debug,
+ C::Output: fmt::Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.inner.fmt(f) }
+}
+
impl<A, B, C, D> Decoder for Decoder4<A, B, C, D>
where
A: Decoder,
@@ -1193,6 +1268,7 @@ mod tests {
/// The decoder for the [`Inner`] type.
#[cfg(feature = "alloc")]
+ #[derive(Clone)]
pub struct InnerDecoder(ArrayDecoder<4>);
#[cfg(feature = "alloc")]
@@ -1224,7 +1300,7 @@ mod tests {
/// The decoder for the [`Test`] type.
#[cfg(feature = "alloc")]
- #[derive(Default)]
+ #[derive(Clone, Default)]
pub struct TestDecoder(VecDecoder<Inner>);
#[cfg(feature = "alloc")]
@@ -1252,6 +1328,52 @@ mod tests {
#[test]
#[cfg(feature = "alloc")]
+ fn vec_decoder_empty() {
+ // Empty with a couple of arbitrary extra bytes.
+ let encoded = vec![0x00, 0xFF, 0xFF];
+
+ let mut slice = encoded.as_slice();
+ let mut decoder = Test::decoder();
+ assert!(!decoder.push_bytes(&mut slice).unwrap());
+
+ let got = decoder.end().unwrap();
+ let want = Test(vec![]);
+
+ assert_eq!(got, want);
+ }
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn vec_decoder_one_item() {
+ let encoded = vec![0x01, 0xEF, 0xBE, 0xAD, 0xDE];
+
+ let mut slice = encoded.as_slice();
+ let mut decoder = Test::decoder();
+ decoder.push_bytes(&mut slice).unwrap();
+
+ let got = decoder.end().unwrap();
+ let want = Test(vec![Inner(0xDEAD_BEEF)]);
+
+ assert_eq!(got, want);
+ }
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn vec_decoder_two_items() {
+ let encoded = vec![0x02, 0xEF, 0xBE, 0xAD, 0xDE, 0xBE, 0xBA, 0xFE, 0xCA];
+
+ let mut slice = encoded.as_slice();
+ let mut decoder = Test::decoder();
+ decoder.push_bytes(&mut slice).unwrap();
+
+ let got = decoder.end().unwrap();
+ let want = Test(vec![Inner(0xDEAD_BEEF), Inner(0xCAFE_BABE)]);
+
+ assert_eq!(got, want);
+ }
+
+ #[test]
+ #[cfg(feature = "alloc")]
fn vec_decoder_reserves_in_batches() {
// A small number of extra elements so we extend exactly by the remainder
// instead of another full batch.
diff --git a/consensus_encoding/src/encode/encoders.rs b/consensus_encoding/src/encode/encoders.rs
index 65f8d620..297d9e15 100644
--- a/consensus_encoding/src/encode/encoders.rs
+++ b/consensus_encoding/src/encode/encoders.rs
@@ -12,6 +12,8 @@
//! [`encoder_newtype_exact`] macros.
//!
+use core::fmt;
+
use internals::array_vec::ArrayVec;
use super::{Encodable, Encoder, ExactSizeEncoder};
@@ -20,6 +22,7 @@ use super::{Encodable, Encoder, ExactSizeEncoder};
const SIZE: usize = 9;
/// An encoder for a single byte slice.
+#[derive(Debug, Clone)]
pub struct BytesEncoder<'sl> {
sl: Option<&'sl [u8]>,
}
@@ -44,6 +47,7 @@ impl<'sl> ExactSizeEncoder for BytesEncoder<'sl> {
}
/// An encoder for a single array.
+#[derive(Debug, Clone)]
pub struct ArrayEncoder<const N: usize> {
arr: Option<[u8; N]>,
}
@@ -73,6 +77,7 @@ impl<const N: usize> ExactSizeEncoder for ArrayEncoder<N> {
///
/// This encoder borrows the array instead of taking ownership, avoiding a copy
/// when the array is already available by reference (e.g., as a struct field).
+#[derive(Debug, Clone)]
pub struct ArrayRefEncoder<'e, const N: usize> {
arr: Option<&'e [u8; N]>,
}
@@ -120,6 +125,27 @@ impl<'e, T: Encodable> SliceEncoder<'e, T> {
}
}
+impl<'e, T: Encodable> fmt::Debug for SliceEncoder<'e, T>
+where
+ T::Encoder<'e>: fmt::Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("SliceEncoder")
+ .field("sl", &self.sl.len())
+ .field("cur_enc", &self.cur_enc)
+ .finish()
+ }
+}
+
+// Manual impl rather than #[derive(Clone)] because derive would constrain `where T: Clone`,
+// but `T` itself is never cloned, only the associated type `T::Encoder<'e>`.
+impl<'e, T: Encodable> Clone for SliceEncoder<'e, T>
+where
+ T::Encoder<'e>: Clone,
+{
+ fn clone(&self) -> Self { Self { sl: self.sl, cur_enc: self.cur_enc.clone() } }
+}
+
impl<T: Encodable> Encoder for SliceEncoder<'_, T> {
fn current_chunk(&self) -> &[u8] {
// `advance` sets `cur_enc` to `None` once the slice encoder is completely exhausted.
@@ -162,6 +188,7 @@ macro_rules! define_encoder_n {
$(($enc_idx:literal, $enc_ty:ident, $enc_field:ident),)*
) => {
$(#[$attr])*
+ #[derive(Debug, Clone)]
pub struct $name<$($enc_ty,)*> {
cur_idx: usize,
$($enc_field: $enc_ty,)*
@@ -243,6 +270,7 @@ define_encoder_n! {
}
/// Encoder for a compact size encoded integer.
+#[derive(Debug, Clone)]
pub struct CompactSizeEncoder {
buf: Option<ArrayVec<u8, SIZE>>,
}
diff --git a/consensus_encoding/src/encode/mod.rs b/consensus_encoding/src/encode/mod.rs
index bcb2e33f..fe2fcf0b 100644
--- a/consensus_encoding/src/encode/mod.rs
+++ b/consensus_encoding/src/encode/mod.rs
@@ -118,6 +118,7 @@ macro_rules! encoder_newtype_exact {
}
/// Yields bytes from any [`Encodable`] instance.
+#[derive(Debug)]
pub struct EncodableByteIter<'e, T: Encodable + 'e> {
enc: T::Encoder<'e>,
position: usize,
@@ -128,6 +129,15 @@ impl<'e, T: Encodable + 'e> EncodableByteIter<'e, T> {
pub fn new(encodable: &'e T) -> Self { Self { enc: encodable.encoder(), position: 0 } }
}
+// Manual impl rather than #[derive(Clone)] because derive would constrain `where T: Clone`,
+// but `T` itself is never cloned, only the associated type `T::Encoder<'e>`.
+impl<'e, T: Encodable + 'e> Clone for EncodableByteIter<'e, T>
+where
+ T::Encoder<'e>: Clone,
+{
+ fn clone(&self) -> Self { Self { enc: self.enc.clone(), position: self.position } }
+}
+
impl<'e, T: Encodable + 'e> Iterator for EncodableByteIter<'e, T> {
type Item = u8;
diff --git a/consensus_encoding/tests/decode.rs b/consensus_encoding/tests/decode.rs
index a75e307d..c76c1e6d 100644
--- a/consensus_encoding/tests/decode.rs
+++ b/consensus_encoding/tests/decode.rs
@@ -380,6 +380,7 @@ fn decode_from_read_unbuffered_extra_data() {
struct Inner(u32);
#[cfg(feature = "alloc")]
+#[derive(Clone)]
struct InnerDecoder(ArrayDecoder<4>);
#[cfg(feature = "alloc")]
@@ -410,7 +411,7 @@ impl Decodable for Inner {
struct Test(Vec<Inner>);
#[cfg(feature = "alloc")]
-#[derive(Default)]
+#[derive(Clone, Default)]
struct TestDecoder(VecDecoder<Inner>);
#[cfg(feature = "alloc")]
@@ -574,6 +575,30 @@ fn vec_decoder_two_items() {
assert_eq!(got, want);
}
+#[test]
+#[cfg(feature = "alloc")]
+fn vec_decoder_clone_mid_decode() {
+ // Feed the length prefix and first item, clone, then feed the second item to both.
+ let prefix = vec![0x02, 0xEF, 0xBE, 0xAD, 0xDE]; // length=2, first item
+ let second = vec![0xBE, 0xBA, 0xFE, 0xCA]; // second item
+
+ let mut slice = prefix.as_slice();
+ let mut decoder = Test::decoder();
+ decoder.push_bytes(&mut slice).unwrap();
+
+ let mut clone = decoder.clone();
+
+ let mut slice = second.as_slice();
+ decoder.push_bytes(&mut slice).unwrap();
+ let got = decoder.end().unwrap();
+ assert_eq!(got, Test(vec![Inner(0xDEAD_BEEF), Inner(0xCAFE_BABE)]));
+
+ let mut slice = second.as_slice();
+ clone.push_bytes(&mut slice).unwrap();
+ let got = clone.end().unwrap();
+ assert_eq!(got, Test(vec![Inner(0xDEAD_BEEF), Inner(0xCAFE_BABE)]));
+}
+
#[cfg(feature = "alloc")]
fn two_fifty_six_elements() -> Test {
Test(core::iter::repeat(Inner(0xDEAD_BEEF)).take(256).collect())
Why this scored 19/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.