Manually implement serde traits for OutPoint
What changed, and why it matters
This commit rewrites how the OutPoint type (a Bitcoin transaction reference made of a transaction ID and output index) is serialized and deserialized with the serde library. It replaces a macro with hand-written code. The change is described by the author as a code cleanup to remove an ugly macro, not as a security fix. There is no direct evidence in the commit or supplied references that this fixes a vulnerability.
No immediate security action required. Treat as a normal refactoring commit. If relying on exact binary serde output for OutPoint, verify compatibility with the new representation (txid as slice with length prefix, vout as little-endian bytes).
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch removes internals::serde_struct_human_string_impl! for OutPoint and manually implements Serialize and Deserialize. For human-readable formats it keeps the existing ‘txid:vout’ string representation. For binary formats it serializes txid as a byte slice and vout as little-endian bytes, and deserializes from either a sequence or a map. The implementation adds duplicate-field and missing-field checks. No security relevance is stated by the vendor, and no CVE or advisory is referenced.
Changed components
primitives/src/transaction.rsOutPoint serde serialization/deserializationInspect captured patch +116 / −2
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index ee90b7c2..ef37c616 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -24,6 +24,8 @@ use hashes::sha256d;
use internals::compact_size;
#[cfg(feature = "hex")]
use internals::write_err;
+#[cfg(feature = "serde")]
+use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "hex")]
use units::parse_int;
@@ -357,8 +359,6 @@ pub struct OutPoint {
/// The index of the referenced output in its transaction's vout.
pub vout: u32,
}
-#[cfg(feature = "serde")]
-internals::serde_struct_human_string_impl!(OutPoint, "an OutPoint", txid, vout);
impl OutPoint {
/// The number of bytes that an outpoint contributes to the size of a transaction.
@@ -419,6 +419,120 @@ fn parse_vout(s: &str) -> Result<u32, ParseOutPointError> {
parse_int::int_from_str(s).map_err(ParseOutPointError::Vout)
}
+#[cfg(feature = "serde")]
+impl Serialize for OutPoint {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ if serializer.is_human_readable() {
+ serializer.collect_str(&self)
+ } else {
+ use crate::serde::ser::SerializeStruct as _;
+
+ let mut state = serializer.serialize_struct("OutPoint", 2)?;
+ // serializing as an array was found in the past to break for some serializers so we use
+ // a slice instead. This causes 8 bytes to be prepended for the length (even though this
+ // is a bit silly because know the length).
+ state.serialize_field("txid", self.txid.as_byte_array().as_slice())?;
+ state.serialize_field("vout", &self.vout.to_le_bytes())?;
+ state.end()
+ }
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for OutPoint {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ if deserializer.is_human_readable() {
+ struct StringVisitor;
+
+ impl<'de> de::Visitor<'de> for StringVisitor {
+ type Value = OutPoint;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a string in format 'txid:vout'")
+ }
+
+ fn visit_str<E>(self, value: &str) -> Result<OutPoint, E>
+ where
+ E: de::Error,
+ {
+ value.parse::<OutPoint>().map_err(de::Error::custom)
+ }
+ }
+
+ deserializer.deserialize_str(StringVisitor)
+ } else {
+ #[derive(Deserialize)]
+ #[serde(field_identifier, rename_all = "lowercase")]
+ enum Field {
+ Txid,
+ Vout,
+ }
+
+ struct OutPointVisitor;
+
+ impl<'de> de::Visitor<'de> for OutPointVisitor {
+ type Value = OutPoint;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("OutPoint struct with fields")
+ }
+
+ fn visit_seq<V>(self, mut seq: V) -> Result<OutPoint, V::Error>
+ where
+ V: de::SeqAccess<'de>,
+ {
+ let txid =
+ seq.next_element()?.ok_or_else(|| de::Error::invalid_length(0, &self))?;
+ let vout =
+ seq.next_element()?.ok_or_else(|| de::Error::invalid_length(1, &self))?;
+ Ok(OutPoint { txid, vout })
+ }
+
+ fn visit_map<V>(self, mut map: V) -> Result<OutPoint, V::Error>
+ where
+ V: de::MapAccess<'de>,
+ {
+ let mut txid = None;
+ let mut vout = None;
+
+ while let Some(key) = map.next_key()? {
+ match key {
+ Field::Txid => {
+ if txid.is_some() {
+ return Err(de::Error::duplicate_field("txid"));
+ }
+ let bytes: [u8; 32] = map.next_value()?;
+ txid = Some(Txid::from_byte_array(bytes));
+ }
+ Field::Vout => {
+ if vout.is_some() {
+ return Err(de::Error::duplicate_field("vout"));
+ }
+ let bytes: [u8; 4] = map.next_value()?;
+ vout = Some(u32::from_le_bytes(bytes));
+ }
+ }
+ }
+
+ let txid = txid.ok_or_else(|| de::Error::missing_field("txid"))?;
+ let vout = vout.ok_or_else(|| de::Error::missing_field("vout"))?;
+
+ Ok(OutPoint { txid, vout })
+ }
+ }
+
+ const FIELDS: &[&str] = &["txid", "vout"];
+ deserializer.deserialize_struct("OutPoint", FIELDS, OutPointVisitor)
+ }
+ }
+}
+
/// An error in parsing an [`OutPoint`].
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
Why this scored 17/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.