From b5990f2e45d18a0c9ace772eee44756047b3c367 Mon Sep 17 00:00:00 2001 From: Tomas Tauber <2410580+tomtau@users.noreply.github.com> Date: Tue, 17 Aug 2021 10:23:32 +0800 Subject: [PATCH] added Ethermint support (fixes #1267 #1071) - collected changes from https://github.com/informalsystems/ibc-rs/issues/1267#issuecomment-896459781 - EthAccount definition was directly pasted into the proto library (as different chains the same proto definition, but under a different package path) - added a new configuration option that allows specifying the address derivation as well as the proto type of public keys (e.g. "/injective.crypto.v1beta1.ethsecp256k1.PubKey" or "/ethermint.crypto.v1alpha1.ethsecp256k1.PubKey") --- .../features/1267-ethermint-support.md | 4 + Cargo.lock | 16 ++++ config.toml | 15 ++++ proto/src/lib.rs | 8 ++ relayer-cli/src/commands/keys/restore.rs | 2 +- relayer/Cargo.toml | 1 + relayer/src/chain/cosmos.rs | 69 +++++++++------ relayer/src/chain/mock.rs | 3 +- relayer/src/config.rs | 29 +++++++ relayer/src/error.rs | 10 +++ relayer/src/keyring.rs | 86 ++++++++++++++----- relayer/src/keyring/errors.rs | 4 + relayer/src/keyring/pub_key.rs | 6 +- .../config/fixtures/relayer_conf_example.toml | 3 +- 14 files changed, 202 insertions(+), 54 deletions(-) create mode 100644 .changelog/unreleased/features/1267-ethermint-support.md diff --git a/.changelog/unreleased/features/1267-ethermint-support.md b/.changelog/unreleased/features/1267-ethermint-support.md new file mode 100644 index 0000000000..c6a3641c3f --- /dev/null +++ b/.changelog/unreleased/features/1267-ethermint-support.md @@ -0,0 +1,4 @@ +- Added Ethermint support ([#1267] [#1071]) + +[#1267]: https://github.com/informalsystems/ibc-rs/issues/1267 +[#1071]: https://github.com/informalsystems/ibc-rs/issues/1071 diff --git a/Cargo.lock b/Cargo.lock index e110eaa065..8895f2e205 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -553,6 +553,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-bigint" version = "0.2.2" @@ -1422,6 +1428,7 @@ dependencies = [ "test-env-log", "thiserror", "tiny-bip39", + "tiny-keccak", "tokio", "toml", "tonic", @@ -3259,6 +3266,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tiny_http" version = "0.8.2" diff --git a/config.toml b/config.toml index 6e887b5740..e37626ab9e 100644 --- a/config.toml +++ b/config.toml @@ -131,6 +131,20 @@ trusting_period = '14days' # Warning: This is an advanced feature! Modify with caution. trust_threshold = { numerator = '1', denominator = '3' } +# Specify the address type which determines: +# 1) address derivation; +# 2) how to retrieve and decode accounts and pubkeys; +# 3) the message signing method. +# The current configuration options are for Cosmos SDK and Ethermint. +# +# Example configuration for Ethermint: +# +# address_type = { derivation = 'ethermint', proto_type = { pk_type = '/injective.crypto.v1beta1.ethsecp256k1.PubKey' } } +# +# Default: { derivation = 'cosmos' }, i.e. address derivation as in Cosmos SDK +# Warning: This is an advanced feature! Modify with caution. +address_type = { derivation = 'cosmos' } + # This section specifies the filters for policy based relaying. # Default: no policy/ filters # The section is ignored if the global 'filter' option is set to 'false'. @@ -168,3 +182,4 @@ max_tx_size = 2097152 clock_drift = '5s' trusting_period = '14days' trust_threshold = { numerator = '1', denominator = '3' } +address_type = { derivation = 'cosmos' } diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 62903f113a..6475d5869e 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -29,6 +29,14 @@ pub mod cosmos { pub mod auth { pub mod v1beta1 { include!("prost/cosmos.auth.v1beta1.rs"); + /// EthAccount defines an Ethermint account. + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct EthAccount { + #[prost(message, optional, tag = "1")] + pub base_account: ::core::option::Option, + #[prost(bytes = "vec", tag = "2")] + pub code_hash: ::prost::alloc::vec::Vec, + } } } pub mod staking { diff --git a/relayer-cli/src/commands/keys/restore.rs b/relayer-cli/src/commands/keys/restore.rs index ae2f0208c2..12e08ee6c3 100644 --- a/relayer-cli/src/commands/keys/restore.rs +++ b/relayer-cli/src/commands/keys/restore.rs @@ -93,7 +93,7 @@ pub fn restore_key( config: &ChainConfig, ) -> Result> { let mut keyring = KeyRing::new(Store::Test, &config.account_prefix, &config.id)?; - let key_entry = keyring.key_from_mnemonic(mnemonic, hdpath)?; + let key_entry = keyring.key_from_mnemonic(mnemonic, hdpath, &config.address_type)?; keyring.add_key(key_name, key_entry.clone())?; Ok(key_entry) diff --git a/relayer/Cargo.toml b/relayer/Cargo.toml index 74de846e17..fb9954213e 100644 --- a/relayer/Cargo.toml +++ b/relayer/Cargo.toml @@ -49,6 +49,7 @@ bitcoin = { version = "=0.27", features = ["use-serde"] } tiny-bip39 = "0.8.0" hdpath = { version = "0.6.0", features = ["with-bitcoin"] } sha2 = "0.9.3" +tiny-keccak = { version = "2.0.2", features = ["keccak"], default-features = false } ripemd160 = "0.9.1" bech32 = "0.8.1" itertools = "0.10.1" diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index dabcef014a..e893fcffdc 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -50,7 +50,7 @@ use ibc::ics24_host::{ClientUpgradePath, Path, IBC_QUERY_PATH, SDK_UPGRADE_QUERY use ibc::query::{QueryTxHash, QueryTxRequest}; use ibc::signer::Signer; use ibc::Height as ICSHeight; -use ibc_proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest}; +use ibc_proto::cosmos::auth::v1beta1::{BaseAccount, EthAccount, QueryAccountRequest}; use ibc_proto::cosmos::base::tendermint::v1beta1::service_client::ServiceClient; use ibc_proto::cosmos::base::tendermint::v1beta1::GetNodeInfoRequest; use ibc_proto::cosmos::base::v1beta1::Coin; @@ -71,7 +71,7 @@ use ibc_proto::ibc::core::connection::v1::{ QueryClientConnectionsRequest, QueryConnectionsRequest, }; -use crate::config::{ChainConfig, GasPrice}; +use crate::config::{AddressType, ChainConfig, GasPrice}; use crate::error::Error; use crate::event::monitor::{EventMonitor, EventReceiver}; use crate::keyring::{KeyEntry, KeyRing, Store}; @@ -523,15 +523,15 @@ impl CosmosSdkChain { fn account(&mut self) -> Result<&mut BaseAccount, Error> { if self.account == None { let account = self.block_on(query_account(self, self.key()?.account))?; - - debug!( - sequence = %account.sequence, - number = %account.account_number, - "[{}] send_tx: retrieved account", - self.id() - ); - - self.account = Some(account); + if let Some(acc) = account.as_ref() { + debug!( + sequence = %acc.sequence, + number = %acc.account_number, + "[{}] send_tx: retrieved account", + self.id() + ); + } + self.account = account; } Ok(self @@ -555,9 +555,13 @@ impl CosmosSdkChain { fn signer(&self, sequence: u64) -> Result { let (_key, pk_buf) = self.key_and_bytes()?; + let pk_type = match &self.config.address_type { + AddressType::Cosmos => "/cosmos.crypto.secp256k1.PubKey".to_string(), + AddressType::Ethermint { pk_type } => pk_type.clone(), + }; // Create a MsgSend proto Any message let pk_any = Any { - type_url: "/cosmos.crypto.secp256k1.PubKey".to_string(), + type_url: pk_type, value: pk_buf, }; @@ -610,7 +614,11 @@ impl CosmosSdkChain { // Sign doc let signed = self .keybase - .sign_msg(&self.config.key_name, signdoc_buf) + .sign_msg( + &self.config.key_name, + signdoc_buf, + &self.config.address_type, + ) .map_err(Error::key_base)?; Ok(signed) @@ -1888,7 +1896,10 @@ async fn broadcast_tx_sync(chain: &CosmosSdkChain, data: Vec) -> Result Result { +async fn query_account( + chain: &CosmosSdkChain, + address: String, +) -> Result, Error> { let mut client = ibc_proto::cosmos::auth::v1beta1::query_client::QueryClient::connect( chain.grpc_addr.clone(), ) @@ -1898,19 +1909,23 @@ async fn query_account(chain: &CosmosSdkChain, address: String) -> Result Result { diff --git a/relayer/src/chain/mock.rs b/relayer/src/chain/mock.rs index 0bcb430c41..84525de977 100644 --- a/relayer/src/chain/mock.rs +++ b/relayer/src/chain/mock.rs @@ -391,7 +391,7 @@ pub mod test_utils { use ibc::ics24_host::identifier::ChainId; - use crate::config::{ChainConfig, GasPrice, PacketFilter}; + use crate::config::{AddressType, ChainConfig, GasPrice, PacketFilter}; /// Returns a very minimal chain configuration, to be used in initializing `MockChain`s. pub fn get_basic_chain_config(id: &str) -> ChainConfig { @@ -413,6 +413,7 @@ pub mod test_utils { trusting_period: Duration::from_secs(14 * 24 * 60 * 60), // 14 days trust_threshold: Default::default(), packet_filter: PacketFilter::default(), + address_type: AddressType::default(), } } } diff --git a/relayer/src/config.rs b/relayer/src/config.rs index cd91cb90fb..bb54c65c66 100644 --- a/relayer/src/config.rs +++ b/relayer/src/config.rs @@ -262,6 +262,33 @@ impl Default for RestConfig { } } +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde( + rename_all = "lowercase", + tag = "derivation", + content = "proto_type", + deny_unknown_fields +)] +pub enum AddressType { + Cosmos, + Ethermint { pk_type: String }, +} + +impl Default for AddressType { + fn default() -> Self { + AddressType::Cosmos + } +} + +impl fmt::Display for AddressType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AddressType::Cosmos => write!(f, "cosmos"), + AddressType::Ethermint { .. } => write!(f, "ethermint"), + } + } +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct ChainConfig { @@ -291,6 +318,8 @@ pub struct ChainConfig { pub gas_price: GasPrice, #[serde(default)] pub packet_filter: PacketFilter, + #[serde(default)] + pub address_type: AddressType, } /// Attempt to load and parse the TOML config file as a `Config`. diff --git a/relayer/src/error.rs b/relayer/src/error.rs index 4af2417ba5..17fb1a8668 100644 --- a/relayer/src/error.rs +++ b/relayer/src/error.rs @@ -422,6 +422,16 @@ define_error! { format!("Hermes health check failed while verifying the application compatibility for chain {0}:{1}; caused by: {2}", e.chain_id, e.address, e.cause) }, + + UnknownAccountType + { + type_url: String + } + |e| { + format!("Failed to deserialize account of an unknown protobuf type: {0}", + e.type_url) + }, + } } diff --git a/relayer/src/keyring.rs b/relayer/src/keyring.rs index 3ae33576e3..fba32cf0b8 100644 --- a/relayer/src/keyring.rs +++ b/relayer/src/keyring.rs @@ -1,13 +1,9 @@ -use std::collections::HashMap; -use std::ffi::OsStr; -use std::fs::{self, File}; -use std::path::{Path, PathBuf}; - +use crate::config::AddressType; use bech32::{ToBase32, Variant}; use bip39::{Language, Mnemonic, Seed}; use bitcoin::{ network::constants::Network, - secp256k1::Secp256k1, + secp256k1::{Message, Secp256k1, SecretKey}, util::bip32::{DerivationPath, ExtendedPrivKey, ExtendedPubKey}, }; use hdpath::StandardHDPath; @@ -16,6 +12,11 @@ use k256::ecdsa::{signature::Signer, Signature, SigningKey}; use ripemd160::Ripemd160; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::path::{Path, PathBuf}; +use tiny_keccak::{Hasher, Keccak}; use errors::Error; pub use pub_key::EncodedPubKey; @@ -320,6 +321,7 @@ impl KeyRing { &self, mnemonic_words: &str, hd_path: &HDPath, + at: &AddressType, ) -> Result { // Get the private key from the mnemonic let private_key = private_key_from_mnemonic(mnemonic_words, hd_path)?; @@ -328,7 +330,7 @@ impl KeyRing { let public_key = ExtendedPubKey::from_private(&Secp256k1::new(), &private_key); // Get address from the public Key - let address = get_address(public_key); + let address = get_address(public_key, at); // Compute Bech32 account let account = bech32::encode(self.account_prefix(), address.to_base32(), Variant::Bech32) @@ -343,15 +345,33 @@ impl KeyRing { } /// Sign a message - pub fn sign_msg(&self, key_name: &str, msg: Vec) -> Result, Error> { + pub fn sign_msg( + &self, + key_name: &str, + msg: Vec, + address_type: &AddressType, + ) -> Result, Error> { let key = self.get_key(key_name)?; let private_key_bytes = key.private_key.private_key.to_bytes(); - let signing_key = - SigningKey::from_bytes(private_key_bytes.as_slice()).map_err(Error::invalid_key)?; - - let signature: Signature = signing_key.sign(&msg); - Ok(signature.as_ref().to_vec()) + match address_type { + AddressType::Cosmos => { + let signing_key = SigningKey::from_bytes(private_key_bytes.as_slice()) + .map_err(Error::invalid_key)?; + let signature: Signature = signing_key.sign(&msg); + Ok(signature.as_ref().to_vec()) + } + AddressType::Ethermint { .. } => { + let hash = keccak256_hash(msg.as_slice()); + let s = Secp256k1::signing_only(); + // SAFETY: hash is 32 bytes, as expected in `Message::from_slice` -- see `keccak256_hash`, hence `unwrap` + let sign_msg = Message::from_slice(hash.as_slice()).unwrap(); + let key = SecretKey::from_slice(private_key_bytes.as_slice()) + .map_err(Error::invalid_key_raw)?; + let (_, sig_bytes) = s.sign_recoverable(&sign_msg, &key).serialize_compact(); + Ok(sig_bytes.to_vec()) + } + } } pub fn account_prefix(&self) -> &str { @@ -380,19 +400,31 @@ fn private_key_from_mnemonic( } /// Return an address from a Public Key -fn get_address(pk: ExtendedPubKey) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(pk.public_key.to_bytes().as_slice()); +fn get_address(pk: ExtendedPubKey, at: &AddressType) -> Vec { + match at { + AddressType::Cosmos => { + let mut hasher = Sha256::new(); + hasher.update(pk.public_key.to_bytes().as_slice()); + + // Read hash digest over the public key bytes & consume hasher + let pk_hash = hasher.finalize(); + + // Plug the hash result into the next crypto hash function. + let mut rip_hasher = Ripemd160::new(); + rip_hasher.update(pk_hash); + let rip_result = rip_hasher.finalize(); - // Read hash digest over the public key bytes & consume hasher - let pk_hash = hasher.finalize(); + rip_result.to_vec() + } + AddressType::Ethermint { .. } => { + let public_key = pk.public_key.key.serialize_uncompressed(); + debug_assert_eq!(public_key[0], 0x04); - // Plug the hash result into the next crypto hash function. - let mut rip_hasher = Ripemd160::new(); - rip_hasher.update(pk_hash); - let rip_result = rip_hasher.finalize(); + let output = keccak256_hash(&public_key[1..]); - rip_result.to_vec() + output[12..].to_vec() + } + } } fn decode_bech32(input: &str) -> Result, Error> { @@ -415,3 +447,11 @@ fn disk_store_path(folder_name: &str) -> Result { Ok(folder) } + +fn keccak256_hash(bytes: &[u8]) -> Vec { + let mut hasher = Keccak::v256(); + hasher.update(bytes); + let mut resp = vec![0u8; 32]; + hasher.finalize(&mut resp); + resp +} diff --git a/relayer/src/keyring/errors.rs b/relayer/src/keyring/errors.rs index b5abaf5bf8..4e06f876d4 100644 --- a/relayer/src/keyring/errors.rs +++ b/relayer/src/keyring/errors.rs @@ -7,6 +7,10 @@ define_error! { [ TraceError ] |_| { "invalid key: could not build signing key from private key bytes" }, + InvalidKeyRaw + [ TraceError ] + |_| { "invalid key: could not build signing key from private key bytes" }, + KeyNotFound |_| { "key not found" }, diff --git a/relayer/src/keyring/pub_key.rs b/relayer/src/keyring/pub_key.rs index 97ba01bc09..11d0794dcb 100644 --- a/relayer/src/keyring/pub_key.rs +++ b/relayer/src/keyring/pub_key.rs @@ -63,7 +63,11 @@ impl FromStr for EncodedPubKey { proto.tpe ); - if proto.tpe != "/cosmos.crypto.secp256k1.PubKey" { + // Ethermint pubkey types: + // "/ethermint.crypto.v1alpha1.ethsecp256k1.PubKey", "/injective.crypto.v1beta1.ethsecp256k1.PubKey" + if proto.tpe != "/cosmos.crypto.secp256k1.PubKey" + && !proto.tpe.ends_with(".ethsecp256k1.PubKey") + { Err(Error::unsupported_public_key(proto.tpe)) } else { Ok(EncodedPubKey::Proto(proto)) diff --git a/relayer/tests/config/fixtures/relayer_conf_example.toml b/relayer/tests/config/fixtures/relayer_conf_example.toml index bd3d322a39..44d001bcc5 100644 --- a/relayer/tests/config/fixtures/relayer_conf_example.toml +++ b/relayer/tests/config/fixtures/relayer_conf_example.toml @@ -18,6 +18,7 @@ max_tx_size = 1048576 clock_drift = '5s' trusting_period = '14days' trust_threshold = { numerator = '1', denominator = '3' } +address_type = { derivation = 'cosmos' } [[chains]] id = 'chain_B' @@ -32,4 +33,4 @@ gas_price = { price = 0.001, denom = 'stake' } clock_drift = '5s' trusting_period = '14days' trust_threshold = { numerator = '1', denominator = '3' } - +address_type = { derivation = 'ethermint', proto_type = { pk_type = '/injective.crypto.v1beta1.ethsecp256k1.PubKey' } }