Skip to content

Add hex codec traits for Header and Transaction#5381

Merged
apoelstra merged 2 commits intorust-bitcoin:masterfrom
mpbagot:feature/tx-hex
Jan 5, 2026
Merged

Add hex codec traits for Header and Transaction#5381
apoelstra merged 2 commits intorust-bitcoin:masterfrom
mpbagot:feature/tx-hex

Conversation

@mpbagot
Copy link
Copy Markdown
Contributor

@mpbagot mpbagot commented Dec 9, 2025

Currently, the Header and Transction types can be easily encoded into bytes using consensus_encoding, but there exists no way to encode them to hex without using the bitcoin crate's serialize_hex or a manual implementation. Neither such solution is elegant or discoverable.

  • Patch 1 adds FromStr, LowerHex, UpperHex, Display and DisplayHex to Transaction
  • Patch 2 adds FromStr, LowerHex, UpperHex and DisplayHex to Header

Closes #1706, #4807

@github-actions github-actions bot added the C-bitcoin PRs modifying the bitcoin crate label Dec 9, 2025
@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Dec 9, 2025

This PR follows the same pattern as used by[0] and suggested in[1] the hex_conservative docs with a wrapper type around the reference to provide the DisplayHex::Display type. I'm not a huge fan of the end result, so if there are any suggestions on other methods to achieve the same result, I would appreciate them.

[0] https://docs.rs/hex-conservative/0.3.1/src/hex_conservative/display.rs.html#207-250
[1] https://docs.rs/hex-conservative/0.3.1/hex_conservative/display/trait.DisplayHex.html#associatedtype.Display

@apoelstra
Copy link
Copy Markdown
Member

I can't tell whether you're putting hex-conservative 0.3 stuff into the public API. It looks like it because the as_hex and into_iter methods appear in the API files.

Your Display impls also just collect the entire object into a string and then displays it. If this is the approach you want to take then you shouldn't need to use hex-conservative at all.

I'm also not totally confident about implementing Display without FromStr on these types, though I agree that we ought to implement the formatting traits which output hex.

@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Dec 10, 2025

I can't tell whether you're putting hex-conservative 0.3 stuff into the public API

into_iter is used for the fmt_hex_exact! macro in Header, and for the BytesToHexIter in Transaction.
as_hex only exists because of the DisplayHex trait (which is from unstable hex-conservative). I can easily drop that trait implementation entirely, but I specifically included it because you called it out here: #1706 (comment)

Your Display impls also just collect the entire object into a string and then displays it.

I did it that way mainly because it was easy. If we want an alloc-less version which works purely off the BytesToHexIter, then I can do that instead. It will take a bit of care to make it work with formatting arguments though, which is why I went with the current method first.

I'm also not totally confident about implementing Display without FromStr on these types

I can extend this PR to include that trait too if you'd like.

@apoelstra
Copy link
Copy Markdown
Member

I can easily drop that trait implementation entirely, but I specifically included it because you called it out here: #1706 (comment)

Yeah, sorry, I didn't realize that this would (apparently) leak into the public API.

@apoelstra
Copy link
Copy Markdown
Member

I wonder if we want to back up and add some facility to consensus_encoding to do these hex displays. It seems like every consensus object will have pretty-much the same considerations when it comes to hex encoding.

At the very least we could make a wrapper type which is private to primitives and which holds an &T where T: Encodable and implement everything on that. Then questions of "what format args should be respected" and "should we try to avoid allocations" can be answered in one place.

Similarly we should do something for FromStr.

I think that as a first approach we should do this in private structs in primitives, and use those to impl Display and FromStr on Header and Transaction as you propose. I wouldn't worry about allocations or about format args. Then in followup PRs we can improve the wrapper types, and then (maybe) we can promote them to public types in consensus_encoding.

@github-actions github-actions bot removed the C-bitcoin PRs modifying the bitcoin crate label Dec 11, 2025
@github-actions github-actions bot added the C-bitcoin PRs modifying the bitcoin crate label Dec 11, 2025
@mpbagot mpbagot changed the title Add hex display traits for Header and Transaction Add hex codec traits for Header and Transaction Dec 11, 2025
@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Dec 11, 2025

I've tried to implement the private wrapper struct.
While the wrapper struct works fine for the display types, I've introduced a public error for the FromStr result. I assume you don't want to introduce a new error type, so I'd appreciate any guidance and/or suggestions on how to solve this.

I also reverted the change to the Header Display implementation, since this private wrapper struct requires alloc, which the old Display implementation didn't.

@apoelstra
Copy link
Copy Markdown
Member

I've introduced a public error for the FromStr result. I assume you don't want to introduce a new error type, so I'd appreciate any guidance and/or suggestions on how to solve this.

So, I think you will need to introduce a public error type no matter what, because as you observe there are (at least) two cases: a hex decoding error and a Decoder error. But both cases have ugly API questions:

  • for the hex error, do we need to wrap a hex-conservative 0.x error? Hopefully not; hopefully the 1.0 API (which will let us decode arrays and vectors, but not ArrayVecs or slices, ugh) is somehow sufficient for this
  • for the decoding error, do we make the ParsePrimitiveError type generic? In general users hate this. Or do we embed a Box<dyn Error> or something else that's awful. Or do we just return a nested Result?

@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Dec 13, 2025

do we need to wrap a hex-conservative 0.x error?

hex_conservative 1.0 has the same error types (OddLengthStringError and InvalidCharError) that we can, in theory, use. The problem is in converting from the unstable to the stable versions without losing information that could be used in Display. The HexToBytesIter yields unstable error types. We could just use decode_to_vec from stable hex for now, since it's functionally equivalent to what's there and would yield the stable errors for us.

do we make the ParsePrimitiveError type generic?

I specifically made it generic over the Decodable type because I thought that would the most intuitive way to understand it. A ParsePrimitiveError<T>::Decode is an error while decoding an object of type T. Box is less than ideal because it further pushes us into requiring alloc and will need to be removed eventually anyway if we go alloc-less.
The nested Result, I'm confused about. Any chance you could elaborate on that?

@apoelstra
Copy link
Copy Markdown
Member

The nested Result, I'm confused about. Any chance you could elaborate on that?

Rather than having ParsePrimitiveError<T>, we have Result<Result<Object, T>, hex::Error>.

This is ugly and potentially confusing for the user who needs to keep track of which error is which (and there is no .transpose two swap them; there is a .transpose method on Results but it's `Option~-specific because Rust has no monads), but

  • Result has a huge pile of accessory methods .and_then() .map_err() etc etc that our custom type does not
  • Users are used to Result being generic; less so for actual error types

I'm leaning against doing this, but I wanted to remind people that it's an option.

@mpbagot mpbagot marked this pull request as ready for review December 15, 2025 07:44
@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Dec 15, 2025

I've sorted out the use of stable hex errors for the ParsePrimitiveError. I'm still using the generic for Decode though because while it's not nice, I don't personally think the nested Result is much better, and neither of us seem to be confident in a specific solution.

That said, since this is going into 1.0, maybe we should sit on this until we really lock down the error type.

Comment on lines +132 to +135
Self::OddLengthString(odd_err) => write_err!(f, "odd length string"; odd_err),
Self::InvalidChar(char_err) => write_err!(f, "invalid character"; char_err),
// Decoder error types don't have Debug, so we only provide this generic error
Self::Decode(_) => write!(f, "failure decoding hex string into {}", core::any::type_name::<T>()),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Self::OddLengthString(odd_err) => write_err!(f, "odd length string"; odd_err),
Self::InvalidChar(char_err) => write_err!(f, "invalid character"; char_err),
// Decoder error types don't have Debug, so we only provide this generic error
Self::Decode(_) => write!(f, "failure decoding hex string into {}", core::any::type_name::<T>()),
Self::OddLengthString(ref e) => write_err!(f, "odd length string"; e),
Self::InvalidChar(ref e) => write_err!(f, "invalid character"; e),
Self::Decode(ref e) => write_err!(f, "failure decoding hex string into {}", core::any::type_name::<T>(); e),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FTR I'm not 100% sure why this works but it builds.

}

#[cfg(all(feature = "hex", feature = "alloc"))]
impl<T: Decodable> From<hex::DecodeVariableLengthBytesError> for ParsePrimitiveError<T> {
Copy link
Copy Markdown
Member

@tcharding tcharding Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to put DecodeVariableLengthBytesError in the public API why have the two variants in ParsePrimitiveError? Why not just have a single HexDecode(DecodeVariableLengthBytesError) (or some other name)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was mainly trying to reduce the error depth so that users won't have to descend into the error types as far to figure out the problem. If a wrapper is preferred though, then I can do that.

Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Firstly, hats off for working this out this far. My gut feeling though is this is too much complexity for primitives. I have very low confidence that we would get this 100% correct. Its only going to be used for Transaction and Header, right? If so I rekon a brain-dead simple implementation would be better even if it requires code duplication.

@tcharding
Copy link
Copy Markdown
Member

tcharding commented Dec 16, 2025

And sorry for being so late to the party. Since I'm so late, @apoelstra please override me if you think I'm wrong.

@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Dec 16, 2025

If so I rekon a brain-dead simple implementation would be better even if it requires code duplication.

I had something like that initially, but @apoelstra suggested something like the private struct that we have now, specifically so that we have a single point at which to make decisions about the encoding style. The main problem that I can see is the error type. Even if we do manual implementations on Transaction or Header, we'd still need to have an error type like the ParsePrimitiveError for the FromStr implementations.

@tcharding
Copy link
Copy Markdown
Member

hmm, I had a hunch that might have happened. What about we do the same trick we did with hash_types::generic then define two separate error types (one for Transaction, one for Header)? This is an off-the-top-of-my-head idea and may be a wild goose chase. I'd suggest either waiting for @apoelstra to weigh in or only do the work with the that expectation.

@apoelstra
Copy link
Copy Markdown
Member

Maybe we can do the generic error type, but keep it private, then expose specific opaque error types for Header and Transaction that wrap it?

@github-actions github-actions bot removed the C-bitcoin PRs modifying the bitcoin crate label Dec 18, 2025
@github-actions github-actions bot added the C-bitcoin PRs modifying the bitcoin crate label Dec 18, 2025
@mpbagot mpbagot force-pushed the feature/tx-hex branch 2 times, most recently from abb9bb5 to 38fc6b5 Compare December 19, 2025 11:20
Copy link
Copy Markdown
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 38fc6b5; successfully ran local tests; this is great. thanks for dealing with all the boilerplate

@nyonson
Copy link
Copy Markdown
Contributor

nyonson commented Dec 20, 2025

fwiw ack 38fc6b5, I learned a bunch following along on this one.


/// Writes an Encodable object to the given formatter in the requested case.
#[inline]
fn hex_write_tx_with_case<T: Encodable + Decodable>(obj: &HexPrimitive<T>, f: &mut fmt::Formatter, case: Case) -> fmt::Result {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for net mentioning it last review; this has _tx_ in the function name but writes a HexPrimitive. Perhaps just drop the tx` bit. Can be done as a follow up since this is private.

/// Provides default implementations for `Display`, `Debug`, `LowerHex`, and `UpperHex`.
/// Also provides [`Self::from_str`] for parsing a string to a `T`.
/// This can be used to implement hex display traits for any encodable types.
pub struct HexPrimitive<'a, T: Encodable + Decodable>(pub &'a T);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub struct HexPrimitive<'a, T: Encodable + Decodable>(pub &'a T);
pub(crate) struct HexPrimitive<'a, T: Encodable + Decodable>(pub &'a T);

Not that it really matters but just for clarity.

tcharding
tcharding previously approved these changes Dec 30, 2025
Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 38fc6b5

@tcharding
Copy link
Copy Markdown
Member

Hot. Patch 1 made patch 2 a blast to review.

@tcharding
Copy link
Copy Markdown
Member

My vote goes for merge and follow up (if you feel like following up that is).

@apoelstra
Copy link
Copy Markdown
Member

Sounds good! Can one of you file an issue to deal with the allocations in both encoding and decoding? They should be unnecessary, though undoubtedly they'll both be annoying to deal with (encoding may require finally adding encoding to hex-conservative, while decoding will involve a "decode hex to a buffer then pass to push_decode" loop).

Everything is encapsulated well so that users will not need to know or care about the change.

apoelstra
apoelstra previously approved these changes Jan 2, 2026
Copy link
Copy Markdown
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 38fc6b5; successfully ran local tests

@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Jan 2, 2026

Rebased on master to fix conflicts, and changed the unaddressed comments from Tobin:

  • pub(crate) for HexPrimitive
  • hex_write_tx_with_case to hex_write_with_case
  • Debug for ParsePrimitiveError Nevermind on this one. It does not build correctly.

Currently, the Transaction type can be easily encoded into bytes using
consensus_encoding, but there exists no way to encode it to hex without
using the bitcoin crate's serialize_hex or a manual implementation.
Neither such solution is elegant or discoverable.
In implementing traits to hex, FromStr should also be introduced to
decode from a hex string.

Implement FromStr, Display, LowerHex, UpperHex and DisplayHex for
Transactions.
Currently, the Header type can be easily encoded into bytes using
consensus_encoding, but there exists no way to encode it to hex without
using the bitcoin crate's serialize_hex or a manual implementation.
Neither such solution is elegant or discoverable.
In implementing traits to hex, FromStr should also be introduced to
decode from a hex string.

Implement FromStr, LowerHex, UpperHex and DisplayHex for Header.
Copy link
Copy Markdown
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 17531ce; successfully ran local tests

Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 17531ce

@apoelstra apoelstra merged commit 6c71d09 into rust-bitcoin:master Jan 5, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-bitcoin PRs modifying the bitcoin crate C-primitives

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Easy way to print hex for raw tx hex?

4 participants