consensus_encoding: Add hex encoding helpers
What changed, and why it matters
This commit adds new convenience functions that let users convert Bitcoin consensus data into hexadecimal text strings. It is a pure feature addition: it does not change existing behavior, fix a bug, or alter any security-critical logic. There is no indication this commit addresses a vulnerability.
No security action required. Review as a normal feature addition; consider whether the new optional `hex` dependency and feature gating align with downstream packaging expectations.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces encode_to_hex and drain_to_hex helpers in the bitcoin-consensus-encoding crate. They wrap the existing EncoderByteIter with hex-conservative 1.1’s BytesToHexIter to produce a String. The functions are gated behind both the alloc and hex feature flags, re-exported in lib.rs, and covered by unit tests. No existing APIs are modified.
Changed components
consensus_encoding/src/encode/mod.rsconsensus_encoding/src/lib.rsconsensus_encoding/Cargo.tomlconsensus_encoding/tests/encode.rsInspect captured patch +64 / −2
diff --git a/consensus_encoding/Cargo.toml b/consensus_encoding/Cargo.toml
index 126d4f1a..c9a99d08 100644
--- a/consensus_encoding/Cargo.toml
+++ b/consensus_encoding/Cargo.toml
@@ -16,12 +16,14 @@ exclude = ["api", "tests", "contrib"]
[features]
default = ["std"]
-std = ["alloc", "internals/std"]
-alloc = ["internals/alloc"]
+std = ["alloc", "internals/std", "hex?/std"]
+alloc = ["internals/alloc", "hex?/alloc"]
[dependencies]
internals = { package = "bitcoin-internals", path = "../internals", version = "0.5.0" }
+hex = { package = "hex-conservative", version = "1.1.0", default-features = false, features = [], optional = true }
+
[dev-dependencies]
hex = { package = "hex-conservative", version = "1.1.0" }
diff --git a/consensus_encoding/src/encode/mod.rs b/consensus_encoding/src/encode/mod.rs
index 08d8ccba..58b6cf64 100644
--- a/consensus_encoding/src/encode/mod.rs
+++ b/consensus_encoding/src/encode/mod.rs
@@ -2,6 +2,9 @@
//! Consensus Encoding Traits
+#[cfg(feature = "alloc")]
+#[cfg(feature = "hex")]
+use alloc::string::String;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
@@ -339,6 +342,28 @@ where
vec
}
+/// Encodes an object into a hex string.
+#[cfg(feature = "alloc")]
+#[cfg(feature = "hex")]
+pub fn encode_to_hex<T>(object: &T, case: hex::Case) -> String
+where
+ T: Encode + ?Sized,
+{
+ drain_to_hex(object.encoder(), case)
+}
+
+/// Drains the output of an [`Encoder`] into a hex string.
+#[cfg(feature = "alloc")]
+#[cfg(feature = "hex")]
+pub fn drain_to_hex<T>(encoder: T, case: hex::Case) -> String
+where
+ T: Encoder,
+{
+ let iter = EncoderByteIter::new(encoder);
+ let hex_iter = hex::BytesToHexIter::new(iter, case);
+ hex_iter.flatten().map(char::from).collect()
+}
+
/// Encodes an object to a standard I/O writer.
///
/// # Performance
diff --git a/consensus_encoding/src/lib.rs b/consensus_encoding/src/lib.rs
index e9043293..e807a6dd 100644
--- a/consensus_encoding/src/lib.rs
+++ b/consensus_encoding/src/lib.rs
@@ -47,11 +47,14 @@
//! * [`drain_to_writer`]: Drain an encoder to a stdlib writer.
//! * [`encode_to_vec`]: Encode to the heap.
//! * [`drain_to_vec`]: Drain an encoder to the heap.
+//! * [`encode_to_hex`]: Encode to a hex string.
+//! * [`drain_to_hex`]: Drain an encoder to a hex string.
//!
//! # Feature Flags
//!
//! * `std` - Enables std lib I/O driver functions and `std::error::Error` impls (implies `alloc`).
//! * `alloc` - Enables [`encode_to_vec`], `Vec`-based decoders, and allocation-based helpers.
+//! * `hex` - Enables [`encode_to_hex`] and [`drain_to_hex`] (also requires `alloc`).
#![no_std]
// Coding conventions.
@@ -97,6 +100,10 @@ pub use self::encode::{
check_encode, check_encoder, Encode, Encoder, EncoderByteIter, EncoderStatus, ExactSizeEncoder,
};
#[cfg(feature = "alloc")]
+#[cfg(feature = "hex")]
+#[doc(inline)]
+pub use self::encode::{drain_to_hex, encode_to_hex};
+#[cfg(feature = "alloc")]
#[doc(inline)]
pub use self::encode::{drain_to_vec, encode_to_vec};
#[cfg(feature = "std")]
diff --git a/consensus_encoding/tests/encode.rs b/consensus_encoding/tests/encode.rs
index 1a40d85c..17d484ca 100644
--- a/consensus_encoding/tests/encode.rs
+++ b/consensus_encoding/tests/encode.rs
@@ -90,6 +90,24 @@ fn encode_vec_empty_data() {
assert!(result.is_empty());
}
+#[test]
+#[cfg(all(feature = "alloc", feature = "hex"))]
+fn encode_hex() {
+ let data = TestData(0xDEAD_BEEF);
+ let hex = bitcoin_consensus_encoding::encode_to_hex(&data, hex::Case::Lower);
+ assert_eq!(hex, "efbeadde");
+ let hex = bitcoin_consensus_encoding::encode_to_hex(&data, hex::Case::Upper);
+ assert_eq!(hex, "EFBEADDE");
+}
+
+#[test]
+#[cfg(all(feature = "alloc", feature = "hex"))]
+fn encode_hex_empty_data() {
+ let data = EmptyData;
+ let hex = bitcoin_consensus_encoding::encode_to_hex(&data, hex::Case::Lower);
+ assert!(hex.is_empty());
+}
+
#[test]
#[cfg(feature = "std")]
fn encode_std_writer_empty_data() {
@@ -475,3 +493,13 @@ fn check_encoder_detects_error_byte_offset() {
);
check_encoder(&mut encoder, &[0x01, 0x02, 0x03]);
}
+
+#[test]
+#[cfg(all(feature = "alloc", feature = "hex"))]
+fn drain_hex_multi_chunk() {
+ let enc1 = ArrayEncoder::without_length_prefix([0xDE_u8, 0xAD]);
+ let enc2 = ArrayEncoder::without_length_prefix([0xBE_u8, 0xEF]);
+ let encoder = Encoder2::new(enc1, enc2);
+ let hex = bitcoin_consensus_encoding::drain_to_hex(encoder, hex::Case::Lower);
+ assert_eq!(hex, "deadbeef");
+}
Why this scored 16/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.