units: Replace decoder definitions with decoder_newtype macro
What changed, and why it matters
This commit is a routine code cleanup in the rust-bitcoin library. It replaces several nearly identical decoder definitions with a single macro, reducing boilerplate. There is no change to user-facing behavior, no bug fix, and no security relevance.
No action required. Treat as normal maintenance/refactoring.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces a private decoder_newtype! macro in include/decoder_newtype.rs and uses it to generate the same Decoder implementations that previously existed for AmountDecoder, BlockHeightDecoder, LockTimeDecoder, CompactTargetDecoder, SequenceDecoder, and BlockTimeDecoder. The generated code preserves the same struct visibility, derives, Default impls, new() constructors, push_bytes error mapping, end logic, and read_limit delegation. It is a pure refactor under the encoding feature flag.
Changed components
units/src/amount/unsigned.rsunits/src/block.rsunits/src/lib.rsunits/src/locktime/absolute/mod.rsunits/src/pow.rsunits/src/sequence.rsunits/src/time.rsinclude/decoder_newtype.rsInspect captured patch +273 / −161
diff --git a/include/decoder_newtype.rs b/include/decoder_newtype.rs
new file mode 100644
index 00000000..93d70e4e
--- /dev/null
+++ b/include/decoder_newtype.rs
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: CC0-1.0
+
+/// Constructs a newtype wrapper around an inner [`encoding::Decoder`] type.
+///
+/// The generated struct wraps an inner decoder and implements [`encoding::Decoder`] by delegating
+/// `push_bytes` and `read_limit` to the inner decoder, then transforming the result in `end`.
+///
+/// ## Required items
+///
+/// * **Struct definition** - declares the newtype with its inner decoder type.
+/// * `fn end` - receives the `Result` returned by the inner decoder's `end` method (i.e.
+/// `Result<InnerOutput, InnerError>`) and must return a `Result<Output, Error>` for the
+/// newtype decoder.
+///
+/// ## Optional items
+///
+/// * `fn new` - a custom constructor. If provided, this macro also generates a [`Default`]
+/// impl that calls through to `new()`. You may specify any visibility and/or make the function
+/// const. If omitted, the resulting type will not have a new function or a [`Default`] impl.
+/// * `fn map_push_bytes_err` - a custom error mapping function to tranform any error from the inner
+/// decoder's `push_bytes` to the wrapper decoder's error type. If omitted, the macro assumes
+/// the error type is a single value newtype that directly wraps the inner error.
+///
+/// Both `fn new` and `fn map_push_bytes_err` are independently optional, giving four possible forms.
+/// Due to limitations in macros, the order must be `new`, `push_bytes_err`, then `end`.
+///
+/// ## Attributes
+///
+/// You can add arbitrary doc comments or attributes to the struct definition and the new function.
+/// Note that the new function always has #[inline].
+///
+/// # Examples
+///
+/// Minimal form (no `new`, no `push_bytes_err`):
+///
+/// ```ignore
+/// decoder_newtype! {
+/// /// The decoder for the [`Block`] type.
+/// pub struct BlockDecoder(BlockInnerDecoder);
+///
+/// fn end(
+/// result: Result<(Header, Vec<Transaction>), <BlockInnerDecoder as Decoder>::Error>
+/// ) -> Result<Block, BlockDecoderError> {
+/// let (header, transactions) = result.map_err(BlockDecoderError)?;
+/// Ok(Block::new_unchecked(header, transactions))
+/// }
+/// }
+/// ```
+///
+/// With a custom constructor:
+///
+/// ```ignore
+/// decoder_newtype! {
+/// /// The decoder for the [`BlockHeight`] type.
+/// pub struct BlockHeightDecoder(encoding::ArrayDecoder<4>);
+///
+/// /// Constructs a new [`BlockHeight`] decoder.
+/// pub const fn new() -> Self { Self(encoding::ArrayDecoder::new()) }
+///
+/// fn end(result: Result<[u8; 4], encoding::UnexpectedEofError>) -> Result<BlockHeight, BlockHeightDecoderError> {
+/// let value = result.map_err(BlockHeightDecoderError)?;
+/// let n = u32::from_le_bytes(value);
+/// Ok(BlockHeight::from_u32(n))
+/// }
+/// }
+/// ```
+///
+/// With a custom `push_bytes` error mapping:
+///
+/// ```ignore
+/// decoder_newtype! {
+/// /// The decoder for the [`Header`] type.
+/// pub struct HeaderDecoder(HeaderInnerDecoder);
+///
+/// fn map_push_bytes_err(err: <HeaderInnerDecoder as Decoder>::Error) -> HeaderDecoderError {
+/// Self::from_inner(err)
+/// }
+///
+/// fn end(
+/// result: Result<<HeaderInnerDecoder as Decoder>::Output, <HeaderInnerDecoder as Decoder>::Error>
+/// ) -> Result<Header, HeaderDecoderError> {
+/// let (version, prev_blockhash, merkle_root, time, bits, nonce) = result.map_err(Self::from_inner)?;
+/// let nonce = u32::from_le_bytes(nonce);
+/// Ok(Header { version, prev_blockhash, merkle_root, time, bits, nonce })
+/// }
+/// }
+/// ```
+macro_rules! decoder_newtype {
+ // Arm 1: without new, without push_bytes_err
+ (
+ $(#[$($struct_attr:tt)*])*
+ $vis:vis struct $name:ident($decoder:ty);
+
+ fn end($result_name:ident: $result_ty:ty) -> Result<$output:ty, $err:ident> $end_impl:block
+ ) => {
+ crate::_decoder_newtype_internal! {
+ $(#[$($struct_attr)*])*
+ $vis struct $name($decoder);
+
+ (err: <$decoder as encoding::Decoder>::Error) -> $err { $err(err) }
+ ($result_name: $result_ty) -> Result<$output, $err> $end_impl
+ }
+ };
+ // Arm 2: with new, without push_bytes_err
+ (
+ $(#[$($struct_attr:tt)*])*
+ $vis:vis struct $name:ident($decoder:ty);
+
+ $(#[$($new_attr:tt)*])*
+ $new_vis:vis $(const $($const:block)?)? fn new() -> Self $new_impl:block
+
+ fn end($result_name:ident: $result_ty:ty) -> Result<$output:ty, $err:ident> $end_impl:block
+ ) => {
+ crate::_decoder_newtype_internal! {
+ $(#[$($struct_attr)*])*
+ $vis struct $name($decoder);
+
+ (err: <$decoder as encoding::Decoder>::Error) -> $err { $err(err) }
+ ($result_name: $result_ty) -> Result<$output, $err> $end_impl
+
+ $(#[$($new_attr)*])*
+ $new_vis $(const $($const)?)? fn new() -> Self $new_impl
+ }
+ };
+ // Arm 3: without new, with push_bytes_err
+ (
+ $(#[$($struct_attr:tt)*])*
+ $vis:vis struct $name:ident($decoder:ty);
+
+ fn map_push_bytes_err($err_var:ident: $inner_err:ty) -> $err_name:ident $on_err_impl:block
+ fn end($result_name:ident: $result_ty:ty) -> Result<$output:ty, $err:ident> $end_impl:block
+ ) => {
+ crate::_decoder_newtype_internal! {
+ $(#[$($struct_attr)*])*
+ $vis struct $name($decoder);
+
+ ($err_var: $inner_err) -> $err_name $on_err_impl
+ ($result_name: $result_ty) -> Result<$output, $err> $end_impl
+ }
+ };
+ // Arm 4: with new, with push_bytes_err
+ (
+ $(#[$($struct_attr:tt)*])*
+ $vis:vis struct $name:ident($decoder:ty);
+
+ $(#[$($new_attr:tt)*])*
+ $new_vis:vis $(const $($const:block)?)? fn new() -> Self $new_impl:block
+
+ fn map_push_bytes_err($err_var:ident: $inner_err:ty) -> $err_name:ident $on_err_impl:block
+ fn end($result_name:ident: $result_ty:ty) -> Result<$output:ty, $err:ident> $end_impl:block
+ ) => {
+ crate::_decoder_newtype_internal! {
+ $(#[$($struct_attr)*])*
+ $vis struct $name($decoder);
+
+ ($err_var: $inner_err) -> $err_name $on_err_impl
+ ($result_name: $result_ty) -> Result<$output, $err> $end_impl
+
+ $(#[$($new_attr)*])*
+ $new_vis $(const $($const)?)? fn new() -> Self $new_impl
+ }
+ };
+}
+pub(crate) use decoder_newtype;
+
+// Due to macro ambiguity, the new needs to go at the end.
+macro_rules! _decoder_newtype_internal {
+ (
+ $(#[$($struct_attr:tt)*])*
+ $vis:vis struct $name:ident($decoder:ty);
+
+ ($err_var:ident: $inner_err:ty) -> $err_name:ident $on_err_impl:block
+ ($result_name:ident: $result_ty:ty) -> Result<$output:ty, $err:ident> $end_impl:block
+
+ $(
+ $(#[$($new_attr:tt)*])*
+ $new_vis:vis $(const $($const:block)?)? fn new() -> Self $new_impl:block
+ )?
+ ) => {
+ $(#[$($struct_attr)*])*
+ $vis struct $name($decoder);
+
+ $(
+ impl Default for $name {
+ #[inline]
+ fn default() -> Self { Self::new() }
+ }
+
+ impl $name {
+ $(#[$($new_attr)*])*
+ #[inline]
+ $new_vis $(const $($const)?)? fn new() -> Self $new_impl
+ }
+ )?
+
+ impl $name {
+ /// INTERNAL ONLY: Converts an inner decoder error into the correct error type.
+ /// Needed because we don't want to have a From impl in the public API just for this.
+ /// Only used by the `push_bytes` method.
+ #[inline]
+ fn push_bytes_map_err($err_var: $inner_err) -> $err $on_err_impl
+ }
+
+ impl encoding::Decoder for $name {
+ type Output = $output;
+ type Error = $err;
+
+ #[inline]
+ fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
+ self.0.push_bytes(bytes).map_err(Self::push_bytes_map_err)
+ }
+
+ #[inline]
+ fn end(self) -> Result<Self::Output, Self::Error> {
+ let end = |$result_name: $result_ty| $end_impl;
+ end(self.0.end())
+ }
+
+ #[inline]
+ fn read_limit(&self) -> usize { self.0.read_limit() }
+ }
+ };
+}
+pub(crate) use _decoder_newtype_internal;
diff --git a/units/src/amount/unsigned.rs b/units/src/amount/unsigned.rs
index ff08fd0a..cf207586 100644
--- a/units/src/amount/unsigned.rs
+++ b/units/src/amount/unsigned.rs
@@ -605,40 +605,24 @@ impl encoding::Encodable for Amount {
}
}
-/// The decoder for the [`Amount`] type.
#[cfg(feature = "encoding")]
-#[derive(Debug, Clone)]
-pub struct AmountDecoder(encoding::ArrayDecoder<8>);
+crate::decoder_newtype! {
+ /// The decoder for the [`Amount`] type.
+ #[derive(Debug, Clone)]
+ pub struct AmountDecoder(encoding::ArrayDecoder<8>);
-#[cfg(feature = "encoding")]
-impl AmountDecoder {
/// Constructs a new [`Amount`] decoder.
pub const fn new() -> Self { Self(encoding::ArrayDecoder::new()) }
-}
-
-#[cfg(feature = "encoding")]
-impl Default for AmountDecoder {
- fn default() -> Self { Self::new() }
-}
-#[cfg(feature = "encoding")]
-impl encoding::Decoder for AmountDecoder {
- type Output = Amount;
- type Error = AmountDecoderError;
-
- #[inline]
- fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
- self.0.push_bytes(bytes).map_err(AmountDecoderError::eof)
+ fn map_push_bytes_err(e: encoding::UnexpectedEofError) -> AmountDecoderError {
+ AmountDecoderError::eof(e)
}
- #[inline]
- fn end(self) -> Result<Self::Output, Self::Error> {
- let a = u64::from_le_bytes(self.0.end().map_err(AmountDecoderError::eof)?);
+ fn end(result: Result<[u8; 8], encoding::UnexpectedEofError>) -> Result<Amount, AmountDecoderError> {
+ let value = result.map_err(AmountDecoderError::eof)?;
+ let a = u64::from_le_bytes(value);
Amount::from_sat(a).map_err(AmountDecoderError::out_of_range)
}
-
- #[inline]
- fn read_limit(&self) -> usize { self.0.read_limit() }
}
#[cfg(feature = "encoding")]
diff --git a/units/src/block.rs b/units/src/block.rs
index 7753bf36..b3368a0c 100644
--- a/units/src/block.rs
+++ b/units/src/block.rs
@@ -212,40 +212,20 @@ impl encoding::Encodable for BlockHeight {
}
}
-/// The decoder for the [`BlockHeight`] type.
#[cfg(feature = "encoding")]
-#[derive(Debug, Clone)]
-pub struct BlockHeightDecoder(encoding::ArrayDecoder<4>);
-
-#[cfg(feature = "encoding")]
-impl Default for BlockHeightDecoder {
- fn default() -> Self { Self::new() }
-}
+crate::decoder_newtype! {
+ /// The decoder for the [`BlockHeight`] type.
+ #[derive(Debug, Clone)]
+ pub struct BlockHeightDecoder(encoding::ArrayDecoder<4>);
-#[cfg(feature = "encoding")]
-impl BlockHeightDecoder {
/// Constructs a new [`BlockHeight`] decoder.
pub const fn new() -> Self { Self(encoding::ArrayDecoder::new()) }
-}
-
-#[cfg(feature = "encoding")]
-impl encoding::Decoder for BlockHeightDecoder {
- type Output = BlockHeight;
- type Error = BlockHeightDecoderError;
-
- #[inline]
- fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
- self.0.push_bytes(bytes).map_err(BlockHeightDecoderError)
- }
- #[inline]
- fn end(self) -> Result<Self::Output, Self::Error> {
- let n = u32::from_le_bytes(self.0.end().map_err(BlockHeightDecoderError)?);
+ fn end(result: Result<[u8; 4], encoding::UnexpectedEofError>) -> Result<BlockHeight, BlockHeightDecoderError> {
+ let value = result.map_err(BlockHeightDecoderError)?;
+ let n = u32::from_le_bytes(value);
Ok(BlockHeight::from_u32(n))
}
-
- #[inline]
- fn read_limit(&self) -> usize { self.0.read_limit() }
}
#[cfg(feature = "encoding")]
diff --git a/units/src/lib.rs b/units/src/lib.rs
index bfaeeba8..1ead9a77 100644
--- a/units/src/lib.rs
+++ b/units/src/lib.rs
@@ -82,3 +82,7 @@ pub use self::{
#[deprecated(since = "1.0.0-rc.0", note = "use `BlockHeightInterval` instead")]
#[doc(hidden)]
pub type BlockInterval = BlockHeightInterval;
+
+// decoder_newtype! macro
+#[cfg(feature = "encoding")]
+include!("../../include/decoder_newtype.rs");
diff --git a/units/src/locktime/absolute/mod.rs b/units/src/locktime/absolute/mod.rs
index 510f799f..feee9be9 100644
--- a/units/src/locktime/absolute/mod.rs
+++ b/units/src/locktime/absolute/mod.rs
@@ -414,40 +414,20 @@ impl encoding::Encodable for LockTime {
}
}
-/// The decoder for the [`LockTime`] type.
#[cfg(feature = "encoding")]
-#[derive(Debug, Clone)]
-pub struct LockTimeDecoder(encoding::ArrayDecoder<4>);
+crate::decoder_newtype! {
+ /// The decoder for the [`LockTime`] type.
+ #[derive(Debug, Clone)]
+ pub struct LockTimeDecoder(encoding::ArrayDecoder<4>);
-#[cfg(feature = "encoding")]
-impl LockTimeDecoder {
/// Constructs a new [`LockTime`] decoder.
pub const fn new() -> Self { Self(encoding::ArrayDecoder::new()) }
-}
-
-#[cfg(feature = "encoding")]
-impl Default for LockTimeDecoder {
- fn default() -> Self { Self::new() }
-}
-
-#[cfg(feature = "encoding")]
-impl encoding::Decoder for LockTimeDecoder {
- type Output = LockTime;
- type Error = LockTimeDecoderError;
-
- #[inline]
- fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
- Ok(self.0.push_bytes(bytes).map_err(LockTimeDecoderError)?)
- }
- #[inline]
- fn end(self) -> Result<Self::Output, Self::Error> {
- let n = u32::from_le_bytes(self.0.end().map_err(LockTimeDecoderError)?);
+ fn end(result: Result<[u8; 4], encoding::UnexpectedEofError>) -> Result<LockTime, LockTimeDecoderError> {
+ let value = result.map_err(LockTimeDecoderError)?;
+ let n = u32::from_le_bytes(value);
Ok(LockTime::from_consensus(n))
}
-
- #[inline]
- fn read_limit(&self) -> usize { self.0.read_limit() }
}
#[cfg(feature = "encoding")]
diff --git a/units/src/pow.rs b/units/src/pow.rs
index 0553f53a..70a936b9 100644
--- a/units/src/pow.rs
+++ b/units/src/pow.rs
@@ -104,40 +104,20 @@ impl encoding::Encodable for CompactTarget {
}
}
-/// The decoder for the [`CompactTarget`] type.
#[cfg(feature = "encoding")]
-#[derive(Debug, Clone)]
-pub struct CompactTargetDecoder(encoding::ArrayDecoder<4>);
+crate::decoder_newtype! {
+ /// The decoder for the [`CompactTarget`] type.
+ #[derive(Debug, Clone)]
+ pub struct CompactTargetDecoder(encoding::ArrayDecoder<4>);
-#[cfg(feature = "encoding")]
-impl CompactTargetDecoder {
/// Constructs a new [`CompactTarget`] decoder.
pub const fn new() -> Self { Self(encoding::ArrayDecoder::new()) }
-}
-
-#[cfg(feature = "encoding")]
-impl Default for CompactTargetDecoder {
- fn default() -> Self { Self::new() }
-}
-
-#[cfg(feature = "encoding")]
-impl encoding::Decoder for CompactTargetDecoder {
- type Output = CompactTarget;
- type Error = CompactTargetDecoderError;
-
- #[inline]
- fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
- self.0.push_bytes(bytes).map_err(CompactTargetDecoderError)
- }
- #[inline]
- fn end(self) -> Result<Self::Output, Self::Error> {
- let n = u32::from_le_bytes(self.0.end().map_err(CompactTargetDecoderError)?);
+ fn end(result: Result<[u8; 4], encoding::UnexpectedEofError>) -> Result<CompactTarget, CompactTargetDecoderError> {
+ let value = result.map_err(CompactTargetDecoderError)?;
+ let n = u32::from_le_bytes(value);
Ok(CompactTarget::from_consensus(n))
}
-
- #[inline]
- fn read_limit(&self) -> usize { self.0.read_limit() }
}
#[cfg(feature = "encoding")]
diff --git a/units/src/sequence.rs b/units/src/sequence.rs
index 49a18f60..d683d0c9 100644
--- a/units/src/sequence.rs
+++ b/units/src/sequence.rs
@@ -275,40 +275,20 @@ impl encoding::Encodable for Sequence {
}
}
-/// The decoder for the [`Sequence`] type.
#[cfg(feature = "encoding")]
-#[derive(Debug, Clone)]
-pub struct SequenceDecoder(encoding::ArrayDecoder<4>);
-
-#[cfg(feature = "encoding")]
-impl Default for SequenceDecoder {
- fn default() -> Self { Self::new() }
-}
+crate::decoder_newtype! {
+ /// The decoder for the [`Sequence`] type.
+ #[derive(Debug, Clone)]
+ pub struct SequenceDecoder(encoding::ArrayDecoder<4>);
-#[cfg(feature = "encoding")]
-impl SequenceDecoder {
/// Constructs a new [`Sequence`] decoder.
pub const fn new() -> Self { Self(encoding::ArrayDecoder::new()) }
-}
-
-#[cfg(feature = "encoding")]
-impl encoding::Decoder for SequenceDecoder {
- type Output = Sequence;
- type Error = SequenceDecoderError;
-
- #[inline]
- fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
- self.0.push_bytes(bytes).map_err(SequenceDecoderError)
- }
- #[inline]
- fn end(self) -> Result<Self::Output, Self::Error> {
- let n = u32::from_le_bytes(self.0.end().map_err(SequenceDecoderError)?);
+ fn end(result: Result<[u8; 4], encoding::UnexpectedEofError>) -> Result<Sequence, SequenceDecoderError> {
+ let value = result.map_err(SequenceDecoderError)?;
+ let n = u32::from_le_bytes(value);
Ok(Sequence::from_consensus(n))
}
-
- #[inline]
- fn read_limit(&self) -> usize { self.0.read_limit() }
}
#[cfg(feature = "encoding")]
diff --git a/units/src/time.rs b/units/src/time.rs
index de2e6cd9..917b8d82 100644
--- a/units/src/time.rs
+++ b/units/src/time.rs
@@ -133,40 +133,20 @@ impl encoding::Encodable for BlockTime {
}
}
-/// The decoder for the [`BlockTime`] type.
#[cfg(feature = "encoding")]
-#[derive(Debug, Clone)]
-pub struct BlockTimeDecoder(encoding::ArrayDecoder<4>);
-
-#[cfg(feature = "encoding")]
-impl Default for BlockTimeDecoder {
- fn default() -> Self { Self::new() }
-}
+crate::decoder_newtype! {
+ /// The decoder for the [`BlockTime`] type.
+ #[derive(Debug, Clone)]
+ pub struct BlockTimeDecoder(encoding::ArrayDecoder<4>);
-#[cfg(feature = "encoding")]
-impl BlockTimeDecoder {
/// Constructs a new [`BlockTime`] decoder.
pub const fn new() -> Self { Self(encoding::ArrayDecoder::new()) }
-}
-
-#[cfg(feature = "encoding")]
-impl encoding::Decoder for BlockTimeDecoder {
- type Output = BlockTime;
- type Error = BlockTimeDecoderError;
- #[inline]
- fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
- self.0.push_bytes(bytes).map_err(BlockTimeDecoderError)
+ fn end(result: Result<[u8; 4], encoding::UnexpectedEofError>) -> Result<BlockTime, BlockTimeDecoderError> {
+ let value = result.map_err(BlockTimeDecoderError)?;
+ let n = u32::from_le_bytes(value);
+ Ok(BlockTime::from_u32(n))
}
-
- #[inline]
- fn end(self) -> Result<Self::Output, Self::Error> {
- let t = u32::from_le_bytes(self.0.end().map_err(BlockTimeDecoderError)?);
- Ok(BlockTime::from_u32(t))
- }
-
- #[inline]
- fn read_limit(&self) -> usize { self.0.read_limit() }
}
#[cfg(feature = "encoding")]
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.