Skip to content

Commit d06935e

Browse files
committed
cosmrs: PublicKey JSON serialization support
Adds support for the "Cosmos JSON" `PublicKey` serialization, e.g.: {"@type":"/cosmos.crypto.ed25519.PubKey","key":"sEEsVGkXvyewKLWMJbHVDRkBoerW0IIwmj1rHkabtHU="} Encoding follows Protobub JSON conventions, with binary data encoded as standard (i.e. non-URL safe) Base64. However, note that it's structurally still a bit different from the Protobuf JSON encoding for public keys, and thus entails a custom solution. Also note that there is unfortunately not yet a general solution for Protobuf JSON encoding for `prost::Message`: tokio-rs/prost#75
1 parent bcb8e8a commit d06935e

File tree

4 files changed

+173
-27
lines changed

4 files changed

+173
-27
lines changed

cosmos-sdk-proto/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#![forbid(unsafe_code)]
1616
#![warn(trivial_casts, trivial_numeric_casts, unused_import_braces)]
1717

18+
pub use tendermint_proto as tendermint;
19+
1820
/// The version (commit hash) of the Cosmos SDK used when generating this library.
1921
pub const COSMOS_SDK_VERSION: &str = include_str!("prost/COSMOS_SDK_COMMIT");
2022

cosmrs/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ k256 = { version = "0.9", features = ["ecdsa", "sha256"] }
1818
prost = "0.7"
1919
prost-types = "0.7"
2020
rand_core = { version = "0.6", features = ["std"] }
21+
serde = { version = "1", features = ["serde_derive"] }
22+
serde_json = "1"
2123
subtle-encoding = { version = "0.5", features = ["bech32-preview"] }
2224
tendermint = { version = "0.22", default-features = false, features = ["secp256k1"] }
2325
thiserror = "1"

cosmrs/src/base.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use crate::{proto, Decimal, Error, Result};
44
use eyre::WrapErr;
5+
use serde::{de, de::Error as _, ser, Deserialize, Serialize};
56
use std::{
67
convert::{TryFrom, TryInto},
78
fmt,
@@ -104,6 +105,20 @@ impl From<&AccountId> for tendermint::account::Id {
104105
}
105106
}
106107

108+
impl<'de> Deserialize<'de> for AccountId {
109+
fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
110+
String::deserialize(deserializer)?
111+
.parse()
112+
.map_err(D::Error::custom)
113+
}
114+
}
115+
116+
impl Serialize for AccountId {
117+
fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
118+
self.bech32.serialize(serializer)
119+
}
120+
}
121+
107122
/// Coin defines a token with a denomination and an amount.
108123
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
109124
pub struct Coin {

cosmrs/src/crypto/public_key.rs

Lines changed: 154 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ use crate::{prost_ext::MessageExt, proto, AccountId, Error, Result};
44
use eyre::WrapErr;
55
use prost::Message;
66
use prost_types::Any;
7-
use std::convert::{TryFrom, TryInto};
7+
use serde::{de, de::Error as _, ser, Deserialize, Serialize};
8+
use std::{
9+
convert::{TryFrom, TryInto},
10+
str::FromStr,
11+
};
12+
use subtle_encoding::base64;
813

914
/// Protobuf [`Any`] type URL for Ed25519 public keys
1015
const ED25519_TYPE_URL: &str = "/cosmos.crypto.ed25519.PubKey";
@@ -17,6 +22,16 @@ const SECP256K1_TYPE_URL: &str = "/cosmos.crypto.secp256k1.PubKey";
1722
pub struct PublicKey(tendermint::PublicKey);
1823

1924
impl PublicKey {
25+
/// Parse public key from Cosmos JSON format.
26+
pub fn from_json(s: &str) -> Result<Self> {
27+
Ok(serde_json::from_str::<PublicKey>(s)?)
28+
}
29+
30+
/// Serialize public key as Cosmos JSON.
31+
pub fn to_json(&self) -> String {
32+
serde_json::to_string(&self).expect("JSON serialization error")
33+
}
34+
2035
/// Get the [`AccountId`] for this [`PublicKey`] (if applicable).
2136
pub fn account_id(&self, prefix: &str) -> Result<AccountId> {
2237
match &self.0 {
@@ -28,31 +43,34 @@ impl PublicKey {
2843
}
2944
}
3045

46+
/// Get the type URL for this [`PublicKey`].
47+
pub fn type_url(&self) -> &'static str {
48+
match &self.0 {
49+
tendermint::PublicKey::Ed25519(_) => ED25519_TYPE_URL,
50+
tendermint::PublicKey::Secp256k1(_) => SECP256K1_TYPE_URL,
51+
// `tendermint::PublicKey` is `non_exhaustive`
52+
_ => unreachable!("unknown pubic key type"),
53+
}
54+
}
55+
3156
/// Convert this [`PublicKey`] to a Protobuf [`Any`] type.
3257
pub fn to_any(&self) -> Result<Any> {
33-
match self.0 {
34-
tendermint::PublicKey::Ed25519(_) => {
35-
let proto = proto::cosmos::crypto::secp256k1::PubKey {
36-
key: self.to_bytes(),
37-
};
38-
39-
Ok(Any {
40-
type_url: ED25519_TYPE_URL.to_owned(),
41-
value: proto.to_bytes()?,
42-
})
58+
let value = match self.0 {
59+
tendermint::PublicKey::Ed25519(_) => proto::cosmos::crypto::secp256k1::PubKey {
60+
key: self.to_bytes(),
4361
}
44-
tendermint::PublicKey::Secp256k1(_) => {
45-
let proto = proto::cosmos::crypto::secp256k1::PubKey {
46-
key: self.to_bytes(),
47-
};
48-
49-
Ok(Any {
50-
type_url: SECP256K1_TYPE_URL.to_owned(),
51-
value: proto.to_bytes()?,
52-
})
62+
.to_bytes(),
63+
tendermint::PublicKey::Secp256k1(_) => proto::cosmos::crypto::secp256k1::PubKey {
64+
key: self.to_bytes(),
5365
}
66+
.to_bytes(),
5467
_ => Err(Error::Crypto.into()),
55-
}
68+
}?;
69+
70+
Ok(Any {
71+
type_url: self.type_url().to_owned(),
72+
value,
73+
})
5674
}
5775

5876
/// Serialize this [`PublicKey`] as a byte vector.
@@ -85,16 +103,22 @@ impl TryFrom<&Any> for PublicKey {
85103
type Error = eyre::Report;
86104

87105
fn try_from(any: &Any) -> Result<PublicKey> {
88-
match any.type_url.as_str() {
106+
let tm_key = match any.type_url.as_str() {
107+
ED25519_TYPE_URL => {
108+
let proto = proto::cosmos::crypto::ed25519::PubKey::decode(&*any.value)?;
109+
tendermint::PublicKey::from_raw_ed25519(&proto.key)
110+
}
89111
SECP256K1_TYPE_URL => {
90112
let proto = proto::cosmos::crypto::secp256k1::PubKey::decode(&*any.value)?;
91113
tendermint::PublicKey::from_raw_secp256k1(&proto.key)
92-
.map(Into::into)
93-
.ok_or_else(|| Error::Crypto.into())
94114
}
95-
other => Err(Error::Crypto)
96-
.wrap_err_with(|| format!("invalid type URL for public key: {}", other)),
97-
}
115+
other => {
116+
return Err(Error::Crypto)
117+
.wrap_err_with(|| format!("invalid type URL for public key: {}", other))
118+
}
119+
};
120+
121+
tm_key.map(Into::into).ok_or_else(|| Error::Crypto.into())
98122
}
99123
}
100124

@@ -117,3 +141,106 @@ impl From<PublicKey> for tendermint::PublicKey {
117141
pk.0
118142
}
119143
}
144+
145+
impl FromStr for PublicKey {
146+
type Err = eyre::Report;
147+
148+
fn from_str(s: &str) -> Result<Self> {
149+
Self::from_json(s)
150+
}
151+
}
152+
153+
impl ToString for PublicKey {
154+
fn to_string(&self) -> String {
155+
self.to_json()
156+
}
157+
}
158+
159+
impl<'de> Deserialize<'de> for PublicKey {
160+
fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
161+
PublicKeyJson::deserialize(deserializer)?
162+
.try_into()
163+
.map_err(D::Error::custom)
164+
}
165+
}
166+
167+
impl Serialize for PublicKey {
168+
fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
169+
PublicKeyJson::from(self).serialize(serializer)
170+
}
171+
}
172+
173+
/// Serde encoding type for JSON public keys.
174+
///
175+
/// Uses Protobuf JSON encoding conventions.
176+
#[derive(Deserialize, Serialize)]
177+
struct PublicKeyJson {
178+
/// `@type` field e.g. `/cosmos.crypto.ed25519.PubKey`.
179+
#[serde(rename = "@type")]
180+
type_url: String,
181+
182+
/// Key data: standard Base64 encoded with padding.
183+
key: String,
184+
}
185+
186+
impl From<PublicKey> for PublicKeyJson {
187+
fn from(public_key: PublicKey) -> PublicKeyJson {
188+
PublicKeyJson::from(&public_key)
189+
}
190+
}
191+
192+
impl From<&PublicKey> for PublicKeyJson {
193+
fn from(public_key: &PublicKey) -> PublicKeyJson {
194+
let type_url = public_key.type_url().to_owned();
195+
let key = String::from_utf8(base64::encode(&public_key.to_bytes())).expect("UTF-8 error");
196+
PublicKeyJson { type_url, key }
197+
}
198+
}
199+
200+
impl TryFrom<PublicKeyJson> for PublicKey {
201+
type Error = eyre::Report;
202+
203+
fn try_from(json: PublicKeyJson) -> Result<PublicKey> {
204+
PublicKey::try_from(&json)
205+
}
206+
}
207+
208+
impl TryFrom<&PublicKeyJson> for PublicKey {
209+
type Error = eyre::Report;
210+
211+
fn try_from(json: &PublicKeyJson) -> Result<PublicKey> {
212+
let pk_bytes = base64::decode(&json.key)?;
213+
214+
let tm_key = match json.type_url.as_str() {
215+
ED25519_TYPE_URL => tendermint::PublicKey::from_raw_ed25519(&pk_bytes),
216+
SECP256K1_TYPE_URL => tendermint::PublicKey::from_raw_secp256k1(&pk_bytes),
217+
other => {
218+
return Err(Error::Crypto)
219+
.wrap_err_with(|| format!("invalid public key @type: {}", other))
220+
}
221+
};
222+
223+
tm_key.map(Into::into).ok_or_else(|| Error::Crypto.into())
224+
}
225+
}
226+
227+
#[cfg(test)]
228+
mod tests {
229+
use super::PublicKey;
230+
231+
const EXAMPLE_JSON: &str = "{\"@type\":\"/cosmos.crypto.ed25519.PubKey\",\"key\":\"sEEsVGkXvyewKLWMJbHVDRkBoerW0IIwmj1rHkabtHU=\"}";
232+
233+
#[test]
234+
fn json_round_trip() {
235+
let example_key = EXAMPLE_JSON.parse::<PublicKey>().unwrap();
236+
assert_eq!(example_key.type_url(), "/cosmos.crypto.ed25519.PubKey");
237+
assert_eq!(
238+
example_key.to_bytes().as_slice(),
239+
&[
240+
176, 65, 44, 84, 105, 23, 191, 39, 176, 40, 181, 140, 37, 177, 213, 13, 25, 1, 161,
241+
234, 214, 208, 130, 48, 154, 61, 107, 30, 70, 155, 180, 117
242+
]
243+
);
244+
assert_eq!(EXAMPLE_JSON, example_key.to_string());
245+
}
246+
}

0 commit comments

Comments
 (0)