Move serde_as_consensus to consensus_encoding
What changed, and why it matters
This commit is a routine code reorganization: it moves a serde helper module from one internal crate to another and updates import paths. There is no change to how data is encoded, decoded, or validated, and no security bug is introduced or fixed.
No security action required. Treat as normal refactoring; verify downstream consumers update their import paths if they used the removed re-export.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change relocates serde_as_consensus from bitcoin_primitives to bitcoin_consensus_encoding, removes the re-export from bitcoin, and updates call sites in examples and tests to use the new path (encoding::serde_as_consensus). The implementation logic is copied almost verbatim; only import paths and crate prefixes change. No functional or security-relevant behavior is modified.
Changed components
bitcoin/examples/serde.rsbitcoin/src/lib.rsconsensus_encoding/src/lib.rsconsensus_encoding/src/serde_as_consensus.rsprimitives/src/block.rsprimitives/src/lib.rsInspect captured patch +267 / −289
diff --git a/bitcoin/examples/serde.rs b/bitcoin/examples/serde.rs
index 758acf3a..85d7be43 100644
--- a/bitcoin/examples/serde.rs
+++ b/bitcoin/examples/serde.rs
@@ -14,7 +14,7 @@ pub struct Foo {
/// Use `serde_as_consensus` for any type that implements `encoding::{Decodable, Encodable}`.
///
/// Consensus encode then use hex or binary depending on the serializer.
- #[serde(with = "bitcoin::serde_as_consensus")]
+ #[serde(with = "encoding::serde_as_consensus")]
header: Header,
/// `Amount` can use sats or bitcoin (`as_btc`).
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index 7a93f2c0..ad27a7c4 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -134,9 +134,6 @@ pub mod taproot;
// Re-export the type from where it is defined but the module from the highest place up the stack
// that it is available in the event that we add some functionality there.
#[doc(inline)]
-#[cfg(feature = "serde")]
-pub use primitives::serde_as_consensus;
-#[doc(inline)]
pub use primitives::{
block::{
compute_merkle_root, compute_witness_root, Block, BlockHash, Checked as BlockChecked,
diff --git a/consensus_encoding/src/lib.rs b/consensus_encoding/src/lib.rs
index 8ee7e83e..ef37ca74 100644
--- a/consensus_encoding/src/lib.rs
+++ b/consensus_encoding/src/lib.rs
@@ -86,6 +86,9 @@ mod decode;
mod encode;
pub mod error;
+#[cfg(feature = "hex")]
+#[cfg(feature = "serde")]
+pub mod serde_as_consensus;
#[doc(inline)]
pub use self::compact_size::{CompactSizeDecoder, CompactSizeEncoder, CompactSizeU64Decoder};
diff --git a/consensus_encoding/src/serde_as_consensus.rs b/consensus_encoding/src/serde_as_consensus.rs
new file mode 100644
index 00000000..6dccfc52
--- /dev/null
+++ b/consensus_encoding/src/serde_as_consensus.rs
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: CC0-1.0
+
+// Methods are an implementation of a standardized serde-specific signature.
+#![allow(missing_docs)]
+#![allow(clippy::missing_errors_doc)]
+
+//! `serde` serialize and deserialize types using consensus encoding.
+//!
+//! Use with `#[serde(with = "bitcoin_consensus_encoding::serde_as_consensus")]`.
+//!
+//! This module works with any type `T` that implements both [`Encode`] and [`Decode`].
+//! In human-readable formats (like JSON), the value is serialized as a hex string.
+//! In non-human-readable formats (like bincode), raw bytes are used.
+
+use core::fmt;
+use core::marker::PhantomData;
+
+use serde::{de, Deserializer, Serializer};
+
+use crate::{Decode, Encode};
+
+/// Serializes a type as a consensus-encoded hex string.
+///
+/// # Type Parameters
+///
+/// * `T` - The type to serialize, must implement [`Encode`] and [`Decode`]
+/// * `S` - The serializer type
+pub fn serialize<T, S>(value: &T, s: S) -> Result<S::Ok, S::Error>
+where
+ T: Encode + Decode,
+ S: Serializer,
+{
+ if s.is_human_readable() {
+ struct ConsensusHex<'a, T>(&'a T);
+
+ impl<T: Encode> fmt::Display for ConsensusHex<'_, T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let encoder = self.0.encoder();
+ let byte_iter = crate::EncoderByteIter::new(encoder);
+ let iter = hex::BytesToHexIter::new(byte_iter, hex::Case::Lower).flatten();
+
+ for ch in iter {
+ fmt::Display::fmt(&ch, f)?;
+ }
+
+ Ok(())
+ }
+ }
+
+ s.collect_str(&ConsensusHex(value))
+ } else {
+ // For non-human-readable formats, serialize as bytes.
+ let bytes = crate::encode_to_vec(value);
+ s.serialize_bytes(&bytes)
+ }
+}
+
+/// Deserializes a type from a consensus-encoded hex string.
+///
+/// # Type Parameters
+///
+/// * `T` - The type to deserialize, must implement [`Encode`] and [`Decode`]
+/// * `D` - The deserializer type
+pub fn deserialize<'d, T, D>(d: D) -> Result<T, D::Error>
+where
+ T: Encode + Decode,
+ D: Deserializer<'d>,
+{
+ if d.is_human_readable() {
+ use alloc::string::String;
+
+ use serde::Deserialize;
+
+ let hex_str = String::deserialize(d)?;
+ crate::decode_from_hex(&hex_str)
+ .map_err(|_| de::Error::custom("failed to decode hex string"))
+ } else {
+ // For non-human-readable formats, deserialize from bytes
+ struct BytesVisitor<T>(PhantomData<T>);
+
+ impl<'de, T> serde::de::Visitor<'de> for BytesVisitor<T>
+ where
+ T: Encode + Decode,
+ {
+ type Value = T;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a byte array")
+ }
+
+ fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ crate::decode_from_slice(v)
+ .map_err(|_| serde::de::Error::custom("failed to decode from bytes"))
+ }
+
+ fn visit_byte_buf<E>(self, v: alloc::vec::Vec<u8>) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ crate::decode_from_slice(&v)
+ .map_err(|_| serde::de::Error::custom("failed to decode from bytes"))
+ }
+ }
+
+ d.deserialize_bytes(BytesVisitor(PhantomData))
+ }
+}
+
+pub mod opt {
+ //! `serde` serialize and deserialize `Option<T>`.
+ //!
+ //! **WARNING:**
+ //!
+ //! This module is specifically for using `serde` to be able to serialize any object that is
+ //! `Encode`/`Decode` if said object is in an `Option`.
+ //!
+ //! Use with `#[serde(with = "bitcoin_consensus_encoding::serde_as_consensus::opt")]`.
+
+ use core::fmt;
+ use core::marker::PhantomData;
+
+ use serde::{de, Deserializer, Serializer};
+
+ use crate::{Decode, Encode};
+
+ #[allow(clippy::ref_option)] // API forced by serde.
+ pub fn serialize<T, S>(t: &Option<T>, s: S) -> Result<S::Ok, S::Error>
+ where
+ T: Encode + Decode,
+ S: Serializer,
+ {
+ struct AsConsensus<'a, T>(&'a T);
+
+ impl<T: Encode + Decode> serde::Serialize for AsConsensus<'_, T> {
+ fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
+ super::serialize(self.0, s)
+ }
+ }
+
+ match *t {
+ Some(ref t) => s.serialize_some(&AsConsensus(t)),
+ None => s.serialize_none(),
+ }
+ }
+
+ pub fn deserialize<'d, T, D>(d: D) -> Result<Option<T>, D::Error>
+ where
+ T: Encode + Decode,
+ D: Deserializer<'d>,
+ {
+ struct OptVisitor<X>(PhantomData<X>);
+
+ impl<'de, X> de::Visitor<'de> for OptVisitor<X>
+ where
+ X: Encode + Decode,
+ {
+ type Value = Option<X>;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ write!(formatter, "an Option<T> where T: encoding::Decode")
+ }
+
+ fn visit_none<E>(self) -> Result<Self::Value, E>
+ where
+ E: de::Error,
+ {
+ Ok(None)
+ }
+
+ fn visit_some<D>(self, d: D) -> Result<Self::Value, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ Ok(Some(super::deserialize(d)?))
+ }
+ }
+ d.deserialize_option(OptVisitor::<T>(PhantomData))
+ }
+}
+
+pub mod vec {
+ //! `serde` serialize and deserialize `Vec<T>`.
+ //!
+ //! **WARNING:**
+ //!
+ //! This is not a consensus encoded vector (i.e, with length prefix). This module is
+ //! specifically for using `serde` to be able to serialize any object that is
+ //! `Encode`/`Decode` if said object is in a `Vec`.
+ //!
+ //! Use with `#[serde(with = "bitcoin_consensus_encoding::serde_as_consensus::vec")]`.
+
+ use alloc::vec::Vec;
+ use core::fmt;
+ use core::marker::PhantomData;
+
+ use serde::{de, Deserializer, Serializer};
+
+ use crate::{Decode, Encode};
+
+ pub fn serialize<T, S>(v: &[T], s: S) -> Result<S::Ok, S::Error>
+ where
+ T: Encode + Decode,
+ S: Serializer,
+ {
+ struct AsConsensus<'a, T>(&'a T);
+
+ impl<T: Encode + Decode> serde::Serialize for AsConsensus<'_, T> {
+ fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
+ super::serialize(self.0, s)
+ }
+ }
+
+ s.collect_seq(v.iter().map(|item| AsConsensus(item)))
+ }
+
+ pub fn deserialize<'d, T, D>(d: D) -> Result<Vec<T>, D::Error>
+ where
+ T: Encode + Decode,
+ D: Deserializer<'d>,
+ {
+ struct VecVisitor<X>(PhantomData<X>);
+
+ impl<'de, X> de::Visitor<'de> for VecVisitor<X>
+ where
+ X: Encode + Decode,
+ {
+ type Value = Vec<X>;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ write!(formatter, "a sequence of consensus-encodable items")
+ }
+
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: de::SeqAccess<'de>,
+ {
+ struct Wrap<X>(X);
+
+ impl<'de, X> de::Deserialize<'de> for Wrap<X>
+ where
+ X: Encode + Decode,
+ {
+ fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
+ super::deserialize::<X, D>(d).map(Wrap)
+ }
+ }
+
+ let mut out = Vec::new();
+ while let Some(Wrap(item)) = seq.next_element::<Wrap<X>>()? {
+ out.push(item);
+ }
+ Ok(out)
+ }
+ }
+
+ d.deserialize_seq(VecVisitor::<T>(PhantomData))
+ }
+}
diff --git a/primitives/src/block.rs b/primitives/src/block.rs
index 576a7587..c4bc7cfa 100644
--- a/primitives/src/block.rs
+++ b/primitives/src/block.rs
@@ -1946,9 +1946,9 @@ mod tests {
#[cfg(feature = "serde")]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Adt {
- #[serde(with = "crate::serde_as_consensus")]
+ #[serde(with = "encoding::serde_as_consensus")]
header: Header,
- #[serde(with = "crate::serde_as_consensus")]
+ #[serde(with = "encoding::serde_as_consensus")]
block: Block,
}
diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs
index 49c2fc5f..4eaa5080 100644
--- a/primitives/src/lib.rs
+++ b/primitives/src/lib.rs
@@ -52,10 +52,6 @@ pub mod merkle_tree;
pub mod opcodes;
#[cfg(feature = "alloc")]
pub mod script;
-#[cfg(feature = "alloc")]
-#[cfg(feature = "hex")]
-#[cfg(feature = "serde")]
-pub mod serde_as_consensus;
pub mod transaction;
#[cfg(feature = "alloc")]
pub mod witness;
diff --git a/primitives/src/serde_as_consensus.rs b/primitives/src/serde_as_consensus.rs
deleted file mode 100644
index cc6e2b8f..00000000
--- a/primitives/src/serde_as_consensus.rs
+++ /dev/null
@@ -1,279 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-// Methods are an implementation of a standardized serde-specific signature.
-#![allow(missing_docs)]
-#![allow(clippy::missing_errors_doc)]
-
-//! `serde` serialize and deserialize types using consensus encoding.
-//!
-//! Use with `#[serde(with = "bitcoin_primitives::serde_as_consensus")]`.
-//!
-//! This module works with any type `T` that implements both [`Encode`] and [`Decode`].
-//! In human-readable formats (like JSON), the value is serialized as a hex string.
-//! In non-human-readable formats (like bincode), raw bytes are used.
-//!
-//! # Examples
-//!
-//! ```
-//! use serde::{Serialize, Deserialize};
-//! use bitcoin_primitives::block::{Block, Header, Unchecked};
-//! use bitcoin_primitives::TxOut;
-//!
-//! #[derive(Serialize, Deserialize)]
-//! pub struct MyStruct {
-//! // Serialize as hex when using human-readable formats (JSON, etc.)
-//! #[serde(with = "bitcoin_primitives::serde_as_consensus")]
-//! pub header: Header,
-//! // We support options too.
-//! #[serde(with = "bitcoin_primitives::serde_as_consensus::opt")]
-//! pub block: Option<Block<Unchecked>>,
-//! // And we support vectors.
-//! #[serde(with = "bitcoin_primitives::serde_as_consensus::vec")]
-//! pub tx_outs: Vec<TxOut>,
-//! }
-//! ```
-
-use core::fmt;
-use core::marker::PhantomData;
-
-use encoding::{Decode, Encode};
-use serde::{de, Deserializer, Serializer};
-
-/// Serializes a type as a consensus-encoded hex string.
-///
-/// # Type Parameters
-///
-/// * `T` - The type to serialize, must implement [`Encode`] and [`Decode`]
-/// * `S` - The serializer type
-pub fn serialize<T, S>(value: &T, s: S) -> Result<S::Ok, S::Error>
-where
- T: Encode + Decode,
- S: Serializer,
-{
- if s.is_human_readable() {
- struct ConsensusHex<'a, T>(&'a T);
-
- impl<T: Encode> fmt::Display for ConsensusHex<'_, T> {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let encoder = self.0.encoder();
- let byte_iter = encoding::EncoderByteIter::new(encoder);
- let iter = hex::BytesToHexIter::new(byte_iter, hex::Case::Lower).flatten();
-
- for ch in iter {
- fmt::Display::fmt(&ch, f)?;
- }
-
- Ok(())
- }
- }
-
- s.collect_str(&ConsensusHex(value))
- } else {
- // For non-human-readable formats, serialize as bytes.
- let bytes = encoding::encode_to_vec(value);
- s.serialize_bytes(&bytes)
- }
-}
-
-/// Deserializes a type from a consensus-encoded hex string.
-///
-/// # Type Parameters
-///
-/// * `T` - The type to deserialize, must implement [`Encode`] and [`Decode`]
-/// * `D` - The deserializer type
-pub fn deserialize<'d, T, D>(d: D) -> Result<T, D::Error>
-where
- T: Encode + Decode,
- D: Deserializer<'d>,
-{
- if d.is_human_readable() {
- use alloc::string::String;
-
- use serde::Deserialize;
-
- let hex_str = String::deserialize(d)?;
- encoding::decode_from_hex(&hex_str)
- .map_err(|_| de::Error::custom("failed to decode hex string"))
- } else {
- // For non-human-readable formats, deserialize from bytes
- struct BytesVisitor<T>(PhantomData<T>);
-
- impl<'de, T> serde::de::Visitor<'de> for BytesVisitor<T>
- where
- T: Encode + Decode,
- {
- type Value = T;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("a byte array")
- }
-
- fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
- where
- E: serde::de::Error,
- {
- encoding::decode_from_slice(v)
- .map_err(|_| serde::de::Error::custom("failed to decode from bytes"))
- }
-
- fn visit_byte_buf<E>(self, v: alloc::vec::Vec<u8>) -> Result<Self::Value, E>
- where
- E: serde::de::Error,
- {
- encoding::decode_from_slice(&v)
- .map_err(|_| serde::de::Error::custom("failed to decode from bytes"))
- }
- }
-
- d.deserialize_bytes(BytesVisitor(PhantomData))
- }
-}
-
-pub mod opt {
- //! `serde` serialize and deserialize `Option<T>`.
- //!
- //! **WARNING:**
- //!
- //! This module is specifically for using `serde` to be able to serialize any object that is
- //! `Encode`/`Decode` if said object is in an `Option`.
- //!
- //! Use with `#[serde(with = "bitcoin_primitives::serde_as_consensus::opt")]`.
-
- use core::fmt;
- use core::marker::PhantomData;
-
- use encoding::{Decode, Encode};
- use serde::{de, Deserializer, Serializer};
-
- #[allow(clippy::ref_option)] // API forced by serde.
- pub fn serialize<T, S>(t: &Option<T>, s: S) -> Result<S::Ok, S::Error>
- where
- T: Encode + Decode,
- S: Serializer,
- {
- struct AsConsensus<'a, T>(&'a T);
-
- impl<T: Encode + Decode> serde::Serialize for AsConsensus<'_, T> {
- fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
- super::serialize(self.0, s)
- }
- }
-
- match *t {
- Some(ref t) => s.serialize_some(&AsConsensus(t)),
- None => s.serialize_none(),
- }
- }
-
- pub fn deserialize<'d, T, D>(d: D) -> Result<Option<T>, D::Error>
- where
- T: Encode + Decode,
- D: Deserializer<'d>,
- {
- struct OptVisitor<X>(PhantomData<X>);
-
- impl<'de, X> de::Visitor<'de> for OptVisitor<X>
- where
- X: Encode + Decode,
- {
- type Value = Option<X>;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- write!(formatter, "an Option<T> where T: encoding::Decode")
- }
-
- fn visit_none<E>(self) -> Result<Self::Value, E>
- where
- E: de::Error,
- {
- Ok(None)
- }
-
- fn visit_some<D>(self, d: D) -> Result<Self::Value, D::Error>
- where
- D: Deserializer<'de>,
- {
- Ok(Some(super::deserialize(d)?))
- }
- }
- d.deserialize_option(OptVisitor::<T>(PhantomData))
- }
-}
-
-pub mod vec {
- //! `serde` serialize and deserialize `Vec<T>`.
- //!
- //! **WARNING:**
- //!
- //! This is not a consensus encoded vector (i.e, with length prefix). This module is
- //! specifically for using `serde` to be able to serialize any object that is
- //! `Encode`/`Decode` if said object is in a `Vec`.
- //!
- //! Use with `#[serde(with = "bitcoin_primitives::serde_as_consensus::vec")]`.
-
- use alloc::vec::Vec;
- use core::fmt;
- use core::marker::PhantomData;
-
- use encoding::{Decode, Encode};
- use serde::{de, Deserializer, Serializer};
-
- pub fn serialize<T, S>(v: &[T], s: S) -> Result<S::Ok, S::Error>
- where
- T: Encode + Decode,
- S: Serializer,
- {
- struct AsConsensus<'a, T>(&'a T);
-
- impl<T: Encode + Decode> serde::Serialize for AsConsensus<'_, T> {
- fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
- super::serialize(self.0, s)
- }
- }
-
- s.collect_seq(v.iter().map(|item| AsConsensus(item)))
- }
-
- pub fn deserialize<'d, T, D>(d: D) -> Result<Vec<T>, D::Error>
- where
- T: Encode + Decode,
- D: Deserializer<'d>,
- {
- struct VecVisitor<X>(PhantomData<X>);
-
- impl<'de, X> de::Visitor<'de> for VecVisitor<X>
- where
- X: Encode + Decode,
- {
- type Value = Vec<X>;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- write!(formatter, "a sequence of consensus-encodable items")
- }
-
- fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
- where
- A: de::SeqAccess<'de>,
- {
- struct Wrap<X>(X);
-
- impl<'de, X> de::Deserialize<'de> for Wrap<X>
- where
- X: Encode + Decode,
- {
- fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
- super::deserialize::<X, D>(d).map(Wrap)
- }
- }
-
- let mut out = Vec::new();
- while let Some(Wrap(item)) = seq.next_element::<Wrap<X>>()? {
- out.push(item);
- }
- Ok(out)
- }
- }
-
- d.deserialize_seq(VecVisitor::<T>(PhantomData))
- }
-}
Why this scored 18/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.