diff --git a/.changelog/unreleased/improvements/1449-allow-base64-apphash.md b/.changelog/unreleased/improvements/1449-allow-base64-apphash.md new file mode 100644 index 000000000..0968b1609 --- /dev/null +++ b/.changelog/unreleased/improvements/1449-allow-base64-apphash.md @@ -0,0 +1,2 @@ +- `[tendermint-rpc]` If `AppHash` fails to decode as hex, try to decode it as base64. + ([\#1449](https://github.com/informalsystems/tendermint-rs/issues/1449)) \ No newline at end of file diff --git a/tendermint/src/hash.rs b/tendermint/src/hash.rs index 8b91e0206..9926a0ab3 100644 --- a/tendermint/src/hash.rs +++ b/tendermint/src/hash.rs @@ -7,7 +7,7 @@ use core::{ use bytes::Bytes; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; -use subtle_encoding::{Encoding, Hex}; +use subtle_encoding::{base64, Encoding, Hex}; use tendermint_proto::Protobuf; use crate::{error::Error, prelude::*}; @@ -253,6 +253,12 @@ impl AppHash { .map_err(Error::subtle_encoding)?; Ok(AppHash(h)) } + + /// Decode a `Hash` from base64-encoded string + pub fn from_base64(s: &str) -> Result { + let h = base64::decode(s).map_err(Error::subtle_encoding)?; + Ok(AppHash(h)) + } } impl AsRef<[u8]> for AppHash { @@ -285,6 +291,52 @@ impl FromStr for AppHash { type Err = Error; fn from_str(s: &str) -> Result { - Self::from_hex_upper(s) + Self::from_hex_upper(s).or_else(|_| Self::from_base64(s)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, serde::Deserialize)] + struct Test { + #[serde(default)] + #[serde(with = "crate::serializers::apphash")] + pub app_hash: AppHash, + } + + #[test] + fn apphash_decode_base64() { + let test = serde_json::from_str::( + r#"{"app_hash":"MfX9f+bYoI8IioRb4YT/8/VhPvtNjgWFgTi4mmMSkBc="}"#, + ) + .unwrap(); + + assert_eq!( + test.app_hash.as_ref(), + &[ + 0x31, 0xF5, 0xFD, 0x7F, 0xE6, 0xD8, 0xA0, 0x8F, 0x08, 0x8A, 0x84, 0x5B, 0xE1, 0x84, + 0xFF, 0xF3, 0xF5, 0x61, 0x3E, 0xFB, 0x4D, 0x8E, 0x05, 0x85, 0x81, 0x38, 0xB8, 0x9A, + 0x63, 0x12, 0x90, 0x17 + ] + ); + } + + #[test] + fn apphash_decode_hex() { + let test = serde_json::from_str::( + r#"{"app_hash":"31F5FD7FE6D8A08F088A845BE184FFF3F5613EFB4D8E05858138B89A63129017"}"#, + ) + .unwrap(); + + assert_eq!( + test.app_hash.as_ref(), + &[ + 0x31, 0xF5, 0xFD, 0x7F, 0xE6, 0xD8, 0xA0, 0x8F, 0x08, 0x8A, 0x84, 0x5B, 0xE1, 0x84, + 0xFF, 0xF3, 0xF5, 0x61, 0x3E, 0xFB, 0x4D, 0x8E, 0x05, 0x85, 0x81, 0x38, 0xB8, 0x9A, + 0x63, 0x12, 0x90, 0x17 + ] + ); } } diff --git a/tendermint/src/serializers/apphash.rs b/tendermint/src/serializers/apphash.rs index d5c34a588..390b0224a 100644 --- a/tendermint/src/serializers/apphash.rs +++ b/tendermint/src/serializers/apphash.rs @@ -1,9 +1,10 @@ //! AppHash serialization with validation +use core::str::FromStr; + use alloc::borrow::Cow; -use serde::{de, ser, Deserialize, Deserializer, Serializer}; -use subtle_encoding::hex; +use serde::{de, Deserialize, Deserializer, Serializer}; use crate::{prelude::*, AppHash}; @@ -13,7 +14,7 @@ where D: Deserializer<'de>, { let hexstring = Option::>::deserialize(deserializer)?.unwrap_or(Cow::Borrowed("")); - AppHash::from_hex_upper(&hexstring).map_err(de::Error::custom) + AppHash::from_str(&hexstring).map_err(de::Error::custom) } /// Serialize from AppHash into hexstring @@ -21,8 +22,6 @@ pub fn serialize(value: &AppHash, serializer: S) -> Result where S: Serializer, { - let hex_bytes = hex::encode_upper(value.as_ref()); - let hex_string = String::from_utf8(hex_bytes).map_err(ser::Error::custom)?; // Serialize as Option for symmetry with deserialize - serializer.serialize_some(&hex_string) + serializer.serialize_some(&value.to_string()) }