Skip to content

Commit ef2904c

Browse files
committed
Serde: use hex encoding for BitcoinNodeHash when (de)serializer is human-readable
1 parent be0cee3 commit ef2904c

File tree

2 files changed

+124
-13
lines changed

2 files changed

+124
-13
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ bitcoin_hashes = "0.14"
1616
serde = { version = "1.0", features = ["derive"], optional = true }
1717

1818
[dev-dependencies]
19+
postcard = { version = "1.1.3", default-features = false, features = ["alloc"] }
1920
serde = { version = "1.0", features = ["derive"] }
2021
serde_json = "1.0.81"
2122

src/accumulator/node_hash.rs

Lines changed: 123 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use std::ops::Deref;
5252
use std::str::FromStr;
5353

5454
use bitcoin_hashes::hex;
55+
use bitcoin_hashes::hex::DisplayHex;
5556
use bitcoin_hashes::sha256;
5657
use bitcoin_hashes::sha512_256;
5758
use bitcoin_hashes::Hash;
@@ -77,6 +78,51 @@ pub trait AccumulatorHash:
7778
R: std::io::Read;
7879
}
7980

81+
/// (de)serialize as hex if the (de)serializer is human readable.
82+
#[cfg(feature = "with-serde")]
83+
mod serde_hex {
84+
pub fn serialize<S, T>(data: T, serializer: S) -> Result<S::Ok, S::Error>
85+
where
86+
S: serde::Serializer,
87+
T: serde::Serialize + bitcoin_hashes::hex::DisplayHex,
88+
{
89+
if serializer.is_human_readable() {
90+
serializer.collect_str(&format_args!("{:x}", data.as_hex()))
91+
} else {
92+
data.serialize(serializer)
93+
}
94+
}
95+
96+
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
97+
where
98+
D: serde::Deserializer<'de>,
99+
T: serde::Deserialize<'de> + bitcoin_hashes::hex::FromHex,
100+
{
101+
struct HexVisitor<T>(std::marker::PhantomData<T>);
102+
103+
impl<'de, T> serde::de::Visitor<'de> for HexVisitor<T>
104+
where
105+
T: bitcoin_hashes::hex::FromHex,
106+
{
107+
type Value = T;
108+
109+
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
110+
f.write_str("an ASCII hex string")
111+
}
112+
113+
fn visit_str<E: serde::de::Error>(self, data: &str) -> Result<Self::Value, E> {
114+
T::from_hex(data).map_err(serde::de::Error::custom)
115+
}
116+
}
117+
118+
if deserializer.is_human_readable() {
119+
deserializer.deserialize_str(HexVisitor(std::marker::PhantomData))
120+
} else {
121+
T::deserialize(deserializer)
122+
}
123+
}
124+
}
125+
80126
#[derive(Eq, PartialEq, Copy, Clone, Hash, PartialOrd, Ord)]
81127
#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
82128
/// AccumulatorHash is a wrapper around a 32 byte array that represents a hash of a node in the tree.
@@ -94,7 +140,7 @@ pub enum BitcoinNodeHash {
94140
#[default]
95141
Empty,
96142
Placeholder,
97-
Some([u8; 32]),
143+
Some(#[cfg_attr(feature = "with-serde", serde(with = "serde_hex"))] [u8; 32]),
98144
}
99145

100146
#[deprecated(since = "0.4.0", note = "Please use BitcoinNodeHash instead.")]
@@ -114,11 +160,7 @@ impl Deref for BitcoinNodeHash {
114160
impl Display for BitcoinNodeHash {
115161
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
116162
if let BitcoinNodeHash::Some(ref inner) = self {
117-
let mut s = String::new();
118-
for byte in inner.iter() {
119-
s.push_str(&format!("{:02x}", byte));
120-
}
121-
write!(f, "{}", s)
163+
Display::fmt(&inner.as_hex(), f)
122164
} else {
123165
write!(f, "empty")
124166
}
@@ -130,13 +172,7 @@ impl Debug for BitcoinNodeHash {
130172
match self {
131173
BitcoinNodeHash::Empty => write!(f, "empty"),
132174
BitcoinNodeHash::Placeholder => write!(f, "placeholder"),
133-
BitcoinNodeHash::Some(ref inner) => {
134-
let mut s = String::new();
135-
for byte in inner.iter() {
136-
s.push_str(&format!("{:02x}", byte));
137-
}
138-
write!(f, "{}", s)
139-
}
175+
BitcoinNodeHash::Some(ref inner) => Debug::fmt(&inner.as_hex(), f),
140176
}
141177
}
142178
}
@@ -351,4 +387,78 @@ mod test {
351387
.unwrap();
352388
assert_eq!(hash, AccumulatorHash::empty());
353389
}
390+
391+
#[cfg(feature = "with-serde")]
392+
fn test_serde_json_roundtrip(
393+
node_hash: BitcoinNodeHash,
394+
expected_serialized: serde_json::Value,
395+
) -> Result<(), serde_json::Error> {
396+
let serialized = serde_json::to_value(node_hash)?;
397+
assert_eq!(serialized, expected_serialized);
398+
let deserialized = serde_json::from_value(serialized)?;
399+
assert_eq!(node_hash, deserialized);
400+
Ok(())
401+
}
402+
403+
#[cfg(feature = "with-serde")]
404+
#[test]
405+
fn test_serde_human_readable_impls() -> Result<(), serde_json::Error> {
406+
let empty = BitcoinNodeHash::Empty;
407+
let placeholder = BitcoinNodeHash::Placeholder;
408+
let hash = {
409+
let mut bytes = [0u8; 32];
410+
for (idx, byte) in bytes.iter_mut().enumerate() {
411+
*byte = idx as u8;
412+
}
413+
BitcoinNodeHash::Some(bytes)
414+
};
415+
let () = test_serde_json_roundtrip(empty, serde_json::Value::String("Empty".to_owned()))?;
416+
let () = test_serde_json_roundtrip(
417+
placeholder,
418+
serde_json::Value::String("Placeholder".to_owned()),
419+
)?;
420+
let () = test_serde_json_roundtrip(
421+
hash,
422+
serde_json::json!({
423+
"Some": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
424+
}),
425+
)?;
426+
Ok(())
427+
}
428+
429+
#[cfg(feature = "with-serde")]
430+
fn test_postcard_roundtrip(
431+
node_hash: BitcoinNodeHash,
432+
expected_serialized: &[u8],
433+
) -> Result<(), postcard::Error> {
434+
let serialized = postcard::to_allocvec(&node_hash)?;
435+
assert_eq!(&serialized, expected_serialized);
436+
let deserialized = postcard::from_bytes(&serialized)?;
437+
assert_eq!(node_hash, deserialized);
438+
Ok(())
439+
}
440+
441+
#[cfg(feature = "with-serde")]
442+
#[test]
443+
fn test_serde_non_human_readable_impls() -> Result<(), postcard::Error> {
444+
let empty = BitcoinNodeHash::Empty;
445+
let placeholder = BitcoinNodeHash::Placeholder;
446+
let hash = {
447+
let mut bytes = [0u8; 32];
448+
for (idx, byte) in bytes.iter_mut().enumerate() {
449+
*byte = idx as u8;
450+
}
451+
BitcoinNodeHash::Some(bytes)
452+
};
453+
let () = test_postcard_roundtrip(empty, &[0u8])?;
454+
let () = test_postcard_roundtrip(placeholder, &[1u8])?;
455+
let () = test_postcard_roundtrip(hash, &{
456+
let mut bytes = [2u8; 33];
457+
for (idx, byte) in bytes.iter_mut().skip(1).enumerate() {
458+
*byte = idx as u8;
459+
}
460+
bytes
461+
})?;
462+
Ok(())
463+
}
354464
}

0 commit comments

Comments
 (0)