Merge rust-bitcoin/rust-bitcoin#6880: primitives: Fix OutPoint serde vout endianness
What changed, and why it matters
This commit fixes a serialization bug in how the `OutPoint` type (a Bitcoin transaction output identifier) handles its `vout` number when using non-human-readable serde formats. The code was writing `vout` as fixed little-endian bytes but reading it back as a generic integer, which could corrupt or fail deserialization under formats that use big-endian or variable-length integers. The fix makes the reader match the writer by always decoding four little-endian bytes. A regression test using bincode in big-endian mode was added.
Review whether any persisted serialized `OutPoint` data was produced by affected code paths with non-little-endian serde configurations, and consider whether downstream users relying on bincode big-endian or varint modes need a compatibility note. The fix itself should be backported to maintained release branches.
Security signals we found
Data integrity / roundtrip failure in serialized identifiers
Endianness mismatch between serializer and deserializer
Potential consensus-critical field corruption (OutPoint vout)
Regression test added for non-default serde backend
Evidence from the diff
In primitives/src/transaction.rs, the Deserialize implementation for OutPoint had an asymmetric sequence visitor: serialization used u32::to_le_bytes(), but deserialization called seq.next_element::<u32>(). With serializers whose integer representation differs from little-endian 4-byte fixed width (e.g., bincode configured for big-endian or varints), roundtrips could produce the wrong vout or error out. The patch changes the sequence visitor to first read [u8; 4] and then decode with u32::from_le_bytes, matching the serializer and the existing map visitor. A new test, out_point_serde_big_endian_roundtrip, verifies correct roundtripping with bincode’s big-endian option.
Changed components
primitives/src/transaction.rsOutPoint serde Deserialize sequence visitorbincode and similar non-human-readable serde backendsInspect captured patch +18 / −1
### primitives/src/transaction.rs
@@ -1163,8 +1163,9 @@ impl<'de> Deserialize<'de> for OutPoint {
{
let txid =
seq.next_element()?.ok_or_else(|| de::Error::invalid_length(0, &self))?;
- let vout =
+ let bytes: [u8; 4] =
seq.next_element()?.ok_or_else(|| de::Error::invalid_length(1, &self))?;
+ let vout = u32::from_le_bytes(bytes);
Ok(OutPoint { txid, vout })
}
@@ -2098,6 +2099,22 @@ mod tests {
assert_eq!(got, out_point);
}
+ #[test]
+ #[cfg(feature = "serde")]
+ fn out_point_serde_big_endian_roundtrip() {
+ use bincode::Options as _;
+
+ let out_point = tc_out_point();
+ let encoded =
+ bincode::DefaultOptions::new().with_big_endian().serialize(&out_point).unwrap();
+ let decoded = bincode::DefaultOptions::new()
+ .with_big_endian()
+ .deserialize::<OutPoint>(&encoded)
+ .unwrap();
+
+ assert_eq!(decoded, out_point);
+ }
+
#[cfg(feature = "alloc")]
fn tx_out() -> TxOut { TxOut { amount: Amount::ONE_SAT, script_pubkey: tc_script_pubkey() } }
Why this scored 38/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.