consenus_encoding: Add a SliceEncoder
What changed, and why it matters
This commit adds a new helper type called SliceEncoder to a Rust Bitcoin library. It is purely additive: it lets developers encode a list of items into Bitcoin's wire format. There is no bug fix, no change to existing behavior, and no security issue visible in the code.
No security action required. Treat as normal feature addition; review for API correctness during standard code review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces SliceEncoder<’e, T: Encodable> in consensus_encoding/src/encode/encoders.rs, re-exports it in lib.rs, adds an example and an integration test, and enables the example in test configuration. The encoder yields a compact-size length prefix followed by each element’s encoding. The implementation is straightforward and does not modify any existing encoder logic or parsing paths.
Changed components
consensus_encoding/src/encode/encoders.rsconsensus_encoding/src/lib.rsconsensus_encoding/examples/encoder.rsconsensus_encoding/tests/wrappers.rsInspect captured patch +196 / −5
diff --git a/consensus_encoding/Cargo.toml b/consensus_encoding/Cargo.toml
index 4e8bcd33..defe6140 100644
--- a/consensus_encoding/Cargo.toml
+++ b/consensus_encoding/Cargo.toml
@@ -25,6 +25,10 @@ internals = { package = "bitcoin-internals", path = "../internals" }
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
+[[example]]
+name = "encoder"
+required-features = ["alloc"]
+
[lints.rust]
unexpected_cfgs = { level = "deny", check-cfg = [] }
diff --git a/consensus_encoding/contrib/test_vars.sh b/consensus_encoding/contrib/test_vars.sh
index 92dc0940..b7f17173 100644
--- a/consensus_encoding/contrib/test_vars.sh
+++ b/consensus_encoding/contrib/test_vars.sh
@@ -11,4 +11,4 @@ FEATURES_WITH_STD=""
FEATURES_WITHOUT_STD="alloc"
# Run these examples.
-EXAMPLES=""
+EXAMPLES="encoder:alloc"
diff --git a/consensus_encoding/examples/encoder.rs b/consensus_encoding/examples/encoder.rs
new file mode 100644
index 00000000..d861b0ec
--- /dev/null
+++ b/consensus_encoding/examples/encoder.rs
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: CC0-1.0
+
+//! Example of creating an encoder that encodes a slice of encodable objects.
+
+use consensus_encoding as encoding;
+use encoding::{ArrayEncoder, BytesEncoder, Encodable, Encoder2, SliceEncoder};
+
+fn main() {
+ let v = vec![Inner::new(0xcafe_babe), Inner::new(0xdead_beef)];
+ let b = vec![0xab, 0xcd];
+
+ let adt = Adt::new(v, b);
+ let encoded = encoding::encode_to_vec(&adt);
+
+ let want = [0x02, 0xca, 0xfe, 0xba, 0xbe, 0xde, 0xad, 0xbe, 0xef, 0xab, 0xcd];
+ assert_eq!(encoded, want);
+}
+
+/// Some abstract data type.
+struct Adt {
+ v: Vec<Inner>,
+ b: Vec<u8>,
+}
+
+impl Adt {
+ /// Constructs a new `Adt`.
+ pub fn new(v: Vec<Inner>, b: Vec<u8>) -> Self { Self { v, b } }
+}
+
+encoding::encoder_newtype! {
+ /// The encoder for the [`Adt`] type.
+ pub struct AdtEncoder<'e>(Encoder2<SliceEncoder<'e, Inner>, BytesEncoder<'e>>);
+}
+
+impl Encodable for Adt {
+ type Encoder<'a>
+ = AdtEncoder<'a>
+ where
+ Self: 'a;
+
+ fn encoder(&self) -> Self::Encoder<'_> {
+ let a = SliceEncoder::with_length_prefix(&self.v);
+ let b = BytesEncoder::without_length_prefix(self.b.as_ref());
+
+ AdtEncoder(Encoder2::new(a, b))
+ }
+}
+
+/// A simple data type to use as list item.
+#[derive(Debug, Default, Clone)]
+pub struct Inner(u32);
+
+impl Inner {
+ /// Constructs a new `Inner`.
+ pub fn new(x: u32) -> Self { Self(x) }
+
+ /// Returns some meaningful 4 byte array for this type.
+ pub fn to_array(&self) -> [u8; 4] { self.0.to_be_bytes() }
+}
+
+encoding::encoder_newtype! {
+ /// The encoder for the [`Inner`] type.
+ pub struct InnerEncoder(ArrayEncoder<4>);
+}
+
+impl Encodable for Inner {
+ type Encoder<'e> = InnerEncoder;
+ fn encoder(&self) -> Self::Encoder<'_> {
+ InnerEncoder(ArrayEncoder::without_length_prefix(self.to_array()))
+ }
+}
diff --git a/consensus_encoding/src/encode/encoders.rs b/consensus_encoding/src/encode/encoders.rs
index ab88f82c..3f4931f7 100644
--- a/consensus_encoding/src/encode/encoders.rs
+++ b/consensus_encoding/src/encode/encoders.rs
@@ -14,7 +14,7 @@
use internals::array_vec::ArrayVec;
use internals::compact_size;
-use super::Encoder;
+use super::{Encodable, Encoder};
/// The maximum length of a compact size encoding.
const SIZE: usize = compact_size::MAX_ENCODING_SIZE;
@@ -76,6 +76,78 @@ impl<const N: usize> Encoder<'_> for ArrayEncoder<N> {
}
}
+/// An encoder for a list of encodable types.
+pub struct SliceEncoder<'e, T: Encodable> {
+ /// The list of references to the objects we are encoding.
+ ///
+ /// This is **never** mutated. All accesses are done by array accesses because
+ /// of lifetimes and the borrow checker.
+ sl: &'e [T],
+ /// The length prefix.
+ compact_size: Option<ArrayVec<u8, SIZE>>,
+ /// Index into `sl` of the element we are currently encoding.
+ cur_idx: usize,
+ /// Current encoder (for `sl[self.cur_idx]`).
+ cur_enc: Option<T::Encoder<'e>>,
+}
+
+impl<'e, T: Encodable> SliceEncoder<'e, T> {
+ /// Constructs an encoder which encodes the slice with a length prefix.
+ pub fn with_length_prefix(sl: &'e [T]) -> Self {
+ let len = sl.len();
+ let compact_size = Some(compact_size::encode(len));
+
+ // In this `map` call we cannot remove the closure. Seems to be a bug in the compiler.
+ // Perhaps https://github.com/rust-lang/rust/issues/102540 which is 3 years old with
+ // no replies or even an acknowledgement. We will not bother filing our own issue.
+ Self { sl, compact_size, cur_idx: 0, cur_enc: sl.first().map(|x| T::encoder(x)) }
+ }
+}
+
+impl<'e, T: Encodable> Encoder<'e> for SliceEncoder<'e, T> {
+ fn current_chunk(&self) -> Option<&[u8]> {
+ if let Some(compact_size) = self.compact_size.as_ref() {
+ return Some(compact_size);
+ }
+
+ // `advance` sets `cur_enc` to `None` once the slice encoder is completely exhausted.
+ // `current_chunk` is required to return `None` if called after the encoder is exhausted.
+ self.cur_enc.as_ref().and_then(T::Encoder::current_chunk)
+ }
+
+ fn advance(&mut self) -> bool {
+ let Some(cur) = self.cur_enc.as_mut() else {
+ return false;
+ };
+
+ loop {
+ if self.compact_size.is_some() {
+ // On the first call to advance(), just mark the compact_size as already
+ // yielded and leave self.cur_idx at 0.
+ self.compact_size = None;
+ } else {
+ // On subsequent calls, attempt to advance the current encoder and return
+ // success if this succeeds.
+ if cur.advance() {
+ return true;
+ }
+ self.cur_idx += 1;
+ }
+
+ // If advancing the current encoder failed, attempt to move to the next encoder.
+ if let Some(x) = self.sl.get(self.cur_idx) {
+ *cur = x.encoder();
+ if cur.current_chunk().is_some() {
+ return true;
+ }
+ } else {
+ self.cur_enc = None; // shortcut the next call to advance()
+ return false;
+ }
+ }
+ }
+}
+
/// An encoder which encodes two objects, one after the other.
pub struct Encoder2<A, B> {
enc_idx: usize,
@@ -239,4 +311,4 @@ mod tests {
let want = [0u8];
assert_eq!(got, want);
}
-}
+ }
diff --git a/consensus_encoding/src/lib.rs b/consensus_encoding/src/lib.rs
index 3246d1c3..5ca545b1 100644
--- a/consensus_encoding/src/lib.rs
+++ b/consensus_encoding/src/lib.rs
@@ -31,6 +31,6 @@ pub use self::encode::encode_to_vec;
#[cfg(feature = "std")]
pub use self::encode::encode_to_writer;
pub use self::encode::encoders::{
- ArrayEncoder, BytesEncoder, Encoder2, Encoder3, Encoder4, Encoder6,
+ ArrayEncoder, BytesEncoder, Encoder2, Encoder3, Encoder4, Encoder6, SliceEncoder,
};
pub use self::encode::{encode_to_hash_engine, Encodable, Encoder};
diff --git a/consensus_encoding/tests/wrappers.rs b/consensus_encoding/tests/wrappers.rs
index 53bf5803..457ff755 100644
--- a/consensus_encoding/tests/wrappers.rs
+++ b/consensus_encoding/tests/wrappers.rs
@@ -3,7 +3,7 @@
#![cfg(feature = "std")]
use consensus_encoding as encoding;
-use encoding::{ArrayEncoder, BytesEncoder, Encodable, Encoder2};
+use encoding::{ArrayEncoder, BytesEncoder, Encodable, Encoder2, SliceEncoder};
encoding::encoder_newtype! {
/// An encoder that uses an inner `ArrayEncoder`.
@@ -109,3 +109,47 @@ fn two_encoder() {
assert_eq!(got, want);
}
+
+#[test]
+fn slice_encoder() {
+ #[derive(Debug, Default, Clone)]
+ pub struct Test(Vec<Inner>);
+
+ encoding::encoder_newtype! {
+ /// An encoder that uses an inner `SliceEncoder`.
+ pub struct TestEncoder<'e>(SliceEncoder<'e, Inner>);
+ }
+
+ impl Encodable for Test {
+ type Encoder<'a>
+ = TestEncoder<'a>
+ where
+ Self: 'a;
+
+ fn encoder(&self) -> Self::Encoder<'_> {
+ TestEncoder(SliceEncoder::with_length_prefix(&self.0))
+ }
+ }
+
+ #[derive(Debug, Default, Clone)]
+ pub struct Inner(u32);
+
+ encoding::encoder_newtype! {
+ /// The encoder for the [`Inner`] type.
+ pub struct InnerArrayEncoder(ArrayEncoder<4>);
+ }
+
+ impl Encodable for Inner {
+ type Encoder<'e> = InnerArrayEncoder;
+ fn encoder(&self) -> Self::Encoder<'_> {
+ // Big-endian to make reading the test assertion easier.
+ InnerArrayEncoder(ArrayEncoder::without_length_prefix(self.0.to_be_bytes()))
+ }
+ }
+
+ let t = Test(vec![Inner(0xcafe_babe), Inner(0xdead_beef)]);
+ let encoded = encoding::encode_to_vec(&t);
+
+ let want = [0x02, 0xca, 0xfe, 0xba, 0xbe, 0xde, 0xad, 0xbe, 0xef];
+ assert_eq!(encoded, want);
+}
Why this scored 15/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.