Replace HexPrimitive decoding with consensus_encoding
What changed, and why it matters
This commit is a routine internal cleanup in the rust-bitcoin library. It removes a duplicate way of turning hex strings into Bitcoin data structures (blocks, block headers, transactions) and replaces it with a single shared helper called decode_from_hex. The actual hex decoding logic stays functionally the same; only the error types and code paths are simplified. There is no indication this fixes a security bug.
No security action required. Treat as normal code-quality refactor; verify downstream consumers do not depend on the removed error types if upgrading.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch deletes the HexPrimitive-based from_str implementations and the ParseBlockError/ParseHeaderError/ParseTransactionError wrapper types in primitives and bitcoin crates. FromStr for Block, Header, and Transaction now delegates directly to encoding::decode_from_hex, returning encoding::FromHexError<…> instead of a wrapper around ParsePrimitiveError. Serde deserialization also switches to decode_from_hex. Tests are updated to match the new error variants. This is a pure refactor with no semantic change to parsing behavior.
Changed components
primitives/src/block.rsprimitives/src/transaction.rsprimitives/src/serde_as_consensus.rsbitcoin/src/blockdata/block.rsbitcoin/src/blockdata/transaction.rsInspect captured patch +26 / −123
diff --git a/bitcoin/src/blockdata/block.rs b/bitcoin/src/blockdata/block.rs
index 996f3620..126f7169 100644
--- a/bitcoin/src/blockdata/block.rs
+++ b/bitcoin/src/blockdata/block.rs
@@ -36,8 +36,8 @@ pub use units::block::{
#[doc(no_inline)]
pub use self::error::{
Bip34Error, BlockDecoderError, BlockHashDecoderError, BlockHeightDecoderError,
- HeaderDecoderError, InvalidBlockError, ParseBlockError, ParseHeaderError,
- TooBigForRelativeHeightError, ValidationError, VersionDecoderError,
+ HeaderDecoderError, InvalidBlockError, TooBigForRelativeHeightError, ValidationError,
+ VersionDecoderError,
};
#[deprecated(since = "TBD", note = "use `BlockHeightInterval` instead")]
@@ -309,7 +309,7 @@ pub mod error {
#[doc(no_inline)]
pub use primitives::block::{
BlockDecoderError, BlockHashDecoderError, HeaderDecoderError, InvalidBlockError,
- ParseBlockError, ParseHeaderError, VersionDecoderError,
+ VersionDecoderError,
};
#[doc(no_inline)]
pub use units::block::{BlockHeightDecoderError, TooBigForRelativeHeightError};
diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index e136700c..07443e82 100644
--- a/bitcoin/src/blockdata/transaction.rs
+++ b/bitcoin/src/blockdata/transaction.rs
@@ -42,8 +42,8 @@ pub use primitives::transaction::{
#[doc(no_inline)]
pub use self::error::{
IndexOutOfBoundsError, InputsIndexError, OutPointDecoderError, OutputsIndexError,
- ParseOutPointError, ParseTransactionError, TransactionDecoderError, TxInDecoderError,
- TxOutDecoderError, VersionDecoderError,
+ ParseOutPointError, TransactionDecoderError, TxInDecoderError, TxOutDecoderError,
+ VersionDecoderError,
};
impl Encodable for Txid {
@@ -1206,7 +1206,7 @@ pub mod error {
#[rustfmt::skip] // Keep public re-exports separate.
#[doc(no_inline)]
pub use primitives::transaction::error::{
- ParseTransactionError, TransactionDecoderError, TxInDecoderError,
+ TransactionDecoderError, TxInDecoderError,
TxOutDecoderError, OutPointDecoderError, ParseOutPointError, VersionDecoderError,
};
diff --git a/primitives/src/block.rs b/primitives/src/block.rs
index 30fd04c3..576a7587 100644
--- a/primitives/src/block.rs
+++ b/primitives/src/block.rs
@@ -36,13 +36,6 @@ use crate::{Transaction, Wtxid};
pub use units::block::{BlockHeight, BlockHeightDecoder, BlockHeightEncoder, BlockHeightInterval, BlockMtp, BlockMtpInterval};
#[rustfmt::skip] // Keep public re-exports separate.
-#[cfg(feature = "hex")]
-#[cfg(feature = "alloc")]
-#[doc(no_inline)]
-pub use self::error::ParseBlockError;
-#[cfg(feature = "hex")]
-#[doc(no_inline)]
-pub use self::error::ParseHeaderError;
#[cfg(feature = "alloc")]
#[doc(no_inline)]
pub use self::error::{BlockDecoderError, InvalidBlockError};
@@ -295,11 +288,9 @@ impl core::str::FromStr for Block<Unchecked>
where
Self: encoding::Decode,
{
- type Err = ParseBlockError;
+ type Err = encoding::FromHexError<BlockDecoderError>;
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- HexPrimitive::from_str(s).map_err(ParseBlockError)
- }
+ fn from_str(s: &str) -> Result<Self, Self::Err> { encoding::decode_from_hex(s) }
}
#[cfg(feature = "alloc")]
@@ -482,11 +473,9 @@ impl Header {
#[cfg(feature = "hex")]
impl core::str::FromStr for Header {
- type Err = ParseHeaderError;
+ type Err = encoding::FromHexError<HeaderDecoderError>;
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- HexPrimitive::from_str(s).map_err(ParseHeaderError)
- }
+ fn from_str(s: &str) -> Result<Self, Self::Err> { encoding::decode_from_hex(s) }
}
#[cfg(feature = "hex")]
@@ -758,12 +747,6 @@ pub mod error {
use internals::write_err;
- #[cfg(feature = "alloc")]
- use super::Block;
- #[cfg(feature = "hex")]
- use super::Header;
- #[cfg(feature = "hex")]
- use crate::hex_codec::ParsePrimitiveError;
use crate::merkle_tree::TxMerkleNodeDecoderError;
use crate::pow::CompactTargetDecoderError;
use crate::time::BlockTimeDecoderError;
@@ -774,34 +757,7 @@ pub mod error {
#[doc(inline)]
pub use crate::hash_types::BlockHashDecoderError;
- /// An error that occurs during parsing of a [`Block`] from a hex string.
- #[cfg(feature = "alloc")]
- #[cfg(feature = "hex")]
- #[derive(Debug, Clone, PartialEq, Eq)]
- pub struct ParseBlockError(pub(super) ParsePrimitiveError<Block>);
-
- #[cfg(feature = "alloc")]
- #[cfg(feature = "hex")]
- impl From<Infallible> for ParseBlockError {
- fn from(never: Infallible) -> Self { match never {} }
- }
-
- #[cfg(feature = "alloc")]
- #[cfg(feature = "hex")]
- impl fmt::Display for ParseBlockError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write_err!(f, "parse block error"; self.0)
- }
- }
-
- #[cfg(feature = "alloc")]
- #[cfg(feature = "hex")]
- #[cfg(feature = "std")]
- impl std::error::Error for ParseBlockError {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.0) }
- }
-
- /// An error consensus decoding a [`Block`].
+ /// An error consensus decoding a [`Block`](super::Block).
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockDecoderError(pub(super) <super::BlockInnerDecoder as encoding::Decoder>::Error);
@@ -871,29 +827,6 @@ pub mod error {
}
}
- /// An error that occurs during parsing of a [`Header`] from a hex string.
- #[cfg(feature = "hex")]
- #[derive(Debug, Clone, PartialEq, Eq)]
- pub struct ParseHeaderError(pub(super) ParsePrimitiveError<Header>);
-
- #[cfg(feature = "hex")]
- impl From<Infallible> for ParseHeaderError {
- fn from(never: Infallible) -> Self { match never {} }
- }
-
- #[cfg(feature = "hex")]
- impl fmt::Display for ParseHeaderError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write_err!(f, "parse header error"; self.0)
- }
- }
-
- #[cfg(feature = "hex")]
- #[cfg(feature = "std")]
- impl std::error::Error for ParseHeaderError {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.0) }
- }
-
/// An error consensus decoding a `Header`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
diff --git a/primitives/src/serde_as_consensus.rs b/primitives/src/serde_as_consensus.rs
index 7d44b662..e159d17d 100644
--- a/primitives/src/serde_as_consensus.rs
+++ b/primitives/src/serde_as_consensus.rs
@@ -39,8 +39,6 @@ use core::marker::PhantomData;
use encoding::{Decode, Encode};
use serde::{de, Deserializer, Serializer};
-use crate::hex_codec::HexPrimitive;
-
/// Serializes a type as a consensus-encoded hex string.
///
/// # Type Parameters
@@ -79,7 +77,8 @@ where
use serde::Deserialize;
let hex_str = String::deserialize(d)?;
- HexPrimitive::<T>::from_str(&hex_str).map_err(de::Error::custom)
+ 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>);
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index 8fd0819e..0099e180 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -16,6 +16,9 @@ use core::{cmp, mem};
#[cfg(feature = "arbitrary")]
use arbitrary::{Arbitrary, Unstructured};
+#[cfg(feature = "hex")]
+#[cfg(feature = "alloc")]
+use encoding::FromHexError;
use encoding::{ArrayEncoder, BytesEncoder, Encoder2};
#[cfg(feature = "alloc")]
use encoding::{
@@ -55,7 +58,7 @@ use crate::{absolute, Amount, ScriptPubKeyBuf, ScriptSigBuf, Sequence, Weight, W
#[cfg(feature = "hex")]
#[cfg(feature = "alloc")]
#[doc(no_inline)]
-pub use self::error::{ParseTransactionError, ParseOutPointError};
+pub use self::error::ParseOutPointError;
#[doc(no_inline)]
pub use self::error::{OutPointDecoderError, VersionDecoderError};
#[cfg(feature = "alloc")]
@@ -241,11 +244,9 @@ impl cmp::Ord for Transaction {
#[cfg(feature = "alloc")]
#[cfg(feature = "hex")]
impl core::str::FromStr for Transaction {
- type Err = ParseTransactionError;
+ type Err = FromHexError<TransactionDecoderError>;
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- HexPrimitive::from_str(s).map_err(ParseTransactionError)
- }
+ fn from_str(s: &str) -> Result<Self, Self::Err> { encoding::decode_from_hex(s) }
}
#[cfg(feature = "alloc")]
@@ -1276,42 +1277,12 @@ pub mod error {
use super::OutPoint;
#[cfg(feature = "hex")]
#[cfg(feature = "alloc")]
- use super::{parse_int, Transaction};
- #[cfg(feature = "hex")]
- #[cfg(feature = "alloc")]
- use crate::hex_codec::ParsePrimitiveError;
+ use super::parse_int;
#[cfg(feature = "alloc")]
use crate::locktime::absolute::LockTimeDecoderError;
#[cfg(feature = "alloc")]
use crate::witness::WitnessDecoderError;
- /// An error that occurs during parsing of a [`Transaction`] from a hex string.
- #[cfg(feature = "alloc")]
- #[cfg(feature = "hex")]
- #[derive(Debug, Clone, PartialEq, Eq)]
- pub struct ParseTransactionError(pub(super) ParsePrimitiveError<Transaction>);
-
- #[cfg(feature = "alloc")]
- #[cfg(feature = "hex")]
- impl From<Infallible> for ParseTransactionError {
- fn from(never: Infallible) -> Self { match never {} }
- }
-
- #[cfg(feature = "alloc")]
- #[cfg(feature = "hex")]
- impl fmt::Display for ParseTransactionError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write_err!(f, "parse transaction error"; self.0)
- }
- }
-
- #[cfg(feature = "alloc")]
- #[cfg(feature = "hex")]
- #[cfg(feature = "std")]
- impl std::error::Error for ParseTransactionError {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.0) }
- }
-
/// An error consensus decoding a `Transaction`.
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -1612,13 +1583,13 @@ mod tests {
#[cfg(feature = "std")]
use std::error::Error as _;
+ #[cfg(feature = "hex")]
+ use encoding::FromHexError;
use encoding::{Decode as _, Decoder as _};
#[cfg(feature = "hex")]
use hex::hex;
use super::*;
- #[cfg(feature = "hex")]
- use crate::hex_codec::ParsePrimitiveError;
const TC_TXID_BYTES: [u8; 32] = [
32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10,
@@ -1758,20 +1729,20 @@ mod tests {
#[test]
#[cfg(feature = "hex")]
fn transaction_from_hex_str_error() {
- // OddLengthString error
+ // OddLength error
let odd = "abc"; // 3 chars, odd length
let err = Transaction::from_str(odd).unwrap_err();
- assert!(matches!(err, ParseTransactionError(ParsePrimitiveError::OddLengthString(..))));
+ assert!(matches!(err, FromHexError::OddLength(..)));
// InvalidChar error
let invalid = "zz";
let err = Transaction::from_str(invalid).unwrap_err();
- assert!(matches!(err, ParseTransactionError(ParsePrimitiveError::InvalidChar(..))));
+ assert!(matches!(err, FromHexError::InvalidChar(..)));
// Decode error
let bad = "deadbeef00"; // arbitrary even-length hex that will fail decoding
let err = Transaction::from_str(bad).unwrap_err();
- assert!(matches!(err, ParseTransactionError(ParsePrimitiveError::Decode(..))));
+ assert!(matches!(err, FromHexError::Decode(..)));
}
#[test]
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.