Skip to content

Commit

Permalink
refactor: delegate keypair encoding to cosmrs (#2887)
Browse files Browse the repository at this point in the history
### Description

Delegates bech32 encoding, decoding, and cosmos address building from a
private key to cosmrs/tendermint upstream crates. The only place where
this wasn't doable was the tests, because of a cyclic dependency - left
a TODO comment there.

As part of this, renamed
`rust/chains/hyperlane-cosmos/src/libs/verify.rs` ->
`rust/chains/hyperlane-cosmos/src/libs/address.rs`

~~**Warning**: We should re-enable e2e and make sure everything still
works locally, since the actual `encode`/`decode` functions that end up
being called _are_ different~~

### Testing
Tested manually EVM <> Duality (both ways)
  • Loading branch information
daniel-savu authored Nov 7, 2023
1 parent 453734a commit 2a7d6d6
Show file tree
Hide file tree
Showing 27 changed files with 256 additions and 266 deletions.
2 changes: 2 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ static_assertions = "1.1"
strum = "0.25.0"
strum_macros = "0.25.2"
tempfile = "3.3"
tendermint = "0.32.2"
thiserror = "1.0"
time = "0.3"
tiny-keccak = "2.0.2"
Expand Down
8 changes: 2 additions & 6 deletions rust/chains/hyperlane-cosmos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ version = { workspace = true }
[dependencies]
async-trait = { workspace = true }
cosmrs = { workspace = true, features = ["cosmwasm", "tokio", "grpc", "rpc"] }
derive-new = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
Expand All @@ -27,12 +28,7 @@ hyper = { workspace = true }
hyper-tls = { workspace = true }
sha256 = { workspace = true }
hex = { workspace = true }
tendermint = { workspace = true, features = ["rust-crypto", "secp256k1"]}
hpl-interface = { version = "0.0.2" }

hyperlane-core = { path = "../../hyperlane-core" }

# These should only be used if it _must_ be used to interop with the inner library,
# all errors exported from a chain crate should be using thiserror or handrolled to
# make error handling easier.
# eyre = "never"
# anyhow = never
11 changes: 8 additions & 3 deletions rust/chains/hyperlane-cosmos/src/aggregation_ism.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::str::FromStr;

use crate::{
address::CosmosAddress,
grpc::{WasmGrpcProvider, WasmProvider},
payloads::aggregate_ism::{ModulesAndThresholdRequest, ModulesAndThresholdResponse},
verify::bech32_decode,
ConnectionConf, CosmosProvider, Signer,
};
use async_trait::async_trait;
Expand Down Expand Up @@ -60,8 +62,11 @@ impl AggregationIsm for CosmosAggregationIsm {
let data = self.provider.wasm_query(payload, None).await?;
let response: ModulesAndThresholdResponse = serde_json::from_slice(&data)?;

let modules: ChainResult<Vec<H256>> =
response.modules.into_iter().map(bech32_decode).collect();
let modules: ChainResult<Vec<H256>> = response
.modules
.into_iter()
.map(|module| CosmosAddress::from_str(&module).map(|ca| ca.digest()))
.collect();

Ok((modules?, response.threshold))
}
Expand Down
6 changes: 6 additions & 0 deletions rust/chains/hyperlane-cosmos/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ pub enum HyperlaneCosmosError {
/// gRPC error
#[error("{0}")]
GrpcError(#[from] tonic::Status),
/// Cosmos error
#[error("{0}")]
CosmosError(#[from] cosmrs::Error),
/// Cosmos error report
#[error("{0}")]
CosmosErrorReport(#[from] cosmrs::ErrorReport),
}

impl From<HyperlaneCosmosError> for ChainCommunicationError {
Expand Down
31 changes: 5 additions & 26 deletions rust/chains/hyperlane-cosmos/src/interchain_security_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ use crate::{
grpc::{WasmGrpcProvider, WasmProvider},
payloads::{
general::EmptyStruct,
ism_routes::{
QueryIsmGeneralRequest, QueryIsmModuleTypeRequest, QueryIsmModuleTypeResponse,
},
ism_routes::{QueryIsmGeneralRequest, QueryIsmModuleTypeRequest},
},
types::IsmType,
ConnectionConf, CosmosProvider, Signer,
};

Expand Down Expand Up @@ -57,19 +56,6 @@ impl HyperlaneChain for CosmosInterchainSecurityModule {
}
}

fn ism_type_to_module_type(ism_type: hpl_interface::ism::ISMType) -> ModuleType {
match ism_type {
hpl_interface::ism::ISMType::Unused => ModuleType::Unused,
hpl_interface::ism::ISMType::Routing => ModuleType::Routing,
hpl_interface::ism::ISMType::Aggregation => ModuleType::Aggregation,
hpl_interface::ism::ISMType::LegacyMultisig => ModuleType::MessageIdMultisig,
hpl_interface::ism::ISMType::MerkleRootMultisig => ModuleType::MerkleRootMultisig,
hpl_interface::ism::ISMType::MessageIdMultisig => ModuleType::MessageIdMultisig,
hpl_interface::ism::ISMType::Null => ModuleType::Null,
hpl_interface::ism::ISMType::CcipRead => ModuleType::CcipRead,
}
}

#[async_trait]
impl InterchainSecurityModule for CosmosInterchainSecurityModule {
/// Returns the module type of the ISM compliant with the corresponding
Expand All @@ -84,16 +70,9 @@ impl InterchainSecurityModule for CosmosInterchainSecurityModule {
.wasm_query(QueryIsmGeneralRequest { ism: query }, None)
.await?;

// Handle both the ISMType response and the ModuleTypeResponse response.
let ismtype_response = serde_json::from_slice::<QueryIsmModuleTypeResponse>(&data);
let moduletye_response =
serde_json::from_slice::<hpl_interface::ism::ModuleTypeResponse>(&data);

Ok(match (ismtype_response, moduletye_response) {
(Ok(v), _) => ism_type_to_module_type(v.typ),
(_, Ok(v)) => ism_type_to_module_type(v.typ),
_ => ModuleType::Null,
})
let module_type_response =
serde_json::from_slice::<hpl_interface::ism::ModuleTypeResponse>(&data)?;
Ok(IsmType(module_type_response.typ).into())
}

/// Dry runs the `verify()` ISM call and returns `Some(gas_estimate)` if the call
Expand Down
1 change: 1 addition & 0 deletions rust/chains/hyperlane-cosmos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod providers;
mod routing_ism;
mod signers;
mod trait_builder;
mod types;
mod utils;
mod validator_announce;

Expand Down
142 changes: 142 additions & 0 deletions rust/chains/hyperlane-cosmos/src/libs/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use std::str::FromStr;

use cosmrs::{
crypto::{secp256k1::SigningKey, PublicKey},
AccountId,
};
use derive_new::new;
use hyperlane_core::{ChainCommunicationError, ChainResult, Error::Overflow, H256};
use tendermint::account::Id as TendermintAccountId;
use tendermint::public_key::PublicKey as TendermintPublicKey;

/// Wrapper around the cosmrs AccountId type that abstracts bech32 encoding
#[derive(new, Debug)]
pub struct CosmosAddress {
/// Bech32 encoded cosmos account
account_id: AccountId,
/// Hex representation (digest) of cosmos account
digest: H256,
}

impl CosmosAddress {
/// Returns a Bitcoin style address: RIPEMD160(SHA256(pubkey))
/// Source: https://github.com/cosmos/cosmos-sdk/blob/177e7f45959215b0b4e85babb7c8264eaceae052/crypto/keys/secp256k1/secp256k1.go#L154
pub fn from_pubkey(pubkey: PublicKey, prefix: &str) -> ChainResult<Self> {
// Get the inner type
let tendermint_pubkey = TendermintPublicKey::from(pubkey);
// Get the RIPEMD160(SHA256(pubkey))
let tendermint_id = TendermintAccountId::from(tendermint_pubkey);
// Bech32 encoding
let account_id = AccountId::new(prefix, tendermint_id.as_bytes())?;
// Hex digest
let digest = Self::bech32_decode(account_id.clone())?;
Ok(CosmosAddress::new(account_id, digest))
}

/// Creates a wrapper arround a cosmrs AccountId from a private key byte array
pub fn from_privkey(priv_key: &[u8], prefix: &str) -> ChainResult<Self> {
let pubkey = SigningKey::from_slice(priv_key)?.public_key();
Self::from_pubkey(pubkey, prefix)
}

/// Creates a wrapper arround a cosmrs AccountId from a H256 digest
///
/// - digest: H256 digest (hex representation of address)
/// - prefix: Bech32 prefix
pub fn from_h256(digest: H256, prefix: &str) -> ChainResult<Self> {
// This is the hex-encoded version of the address
let bytes = digest.as_bytes();
// Bech32 encode it
let account_id = AccountId::new(prefix, bytes)?;
Ok(CosmosAddress::new(account_id, digest))
}

/// Builds a H256 digest from a cosmos AccountId (Bech32 encoding)
fn bech32_decode(account_id: AccountId) -> ChainResult<H256> {
// Temporarily set the digest to a default value as a placeholder.
// Can't implement H256::try_from for AccountId to avoid this.
let cosmos_address = CosmosAddress::new(account_id, Default::default());
H256::try_from(&cosmos_address)
}

/// String representation of a cosmos AccountId
pub fn address(&self) -> String {
self.account_id.to_string()
}

/// H256 digest of the cosmos AccountId
pub fn digest(&self) -> H256 {
self.digest
}
}

impl TryFrom<&CosmosAddress> for H256 {
type Error = ChainCommunicationError;

fn try_from(cosmos_address: &CosmosAddress) -> Result<Self, Self::Error> {
// `to_bytes()` decodes the Bech32 into a hex, represented as a byte vec
let bytes = cosmos_address.account_id.to_bytes();
let h256_len = H256::len_bytes();
let Some(start_point) = h256_len.checked_sub(bytes.len()) else {
// input is too large to fit in a H256
return Err(Overflow.into());
};
let mut empty_hash = H256::default();
let result = empty_hash.as_bytes_mut();
result[start_point..].copy_from_slice(bytes.as_slice());
Ok(H256::from_slice(result))
}
}

impl FromStr for CosmosAddress {
type Err = ChainCommunicationError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let account_id = AccountId::from_str(s)?;
let digest = Self::bech32_decode(account_id.clone())?;
Ok(Self::new(account_id, digest))
}
}

#[cfg(test)]
pub mod test {
use hyperlane_core::utils::hex_or_base58_to_h256;

use super::*;

#[test]
fn test_bech32_decode() {
let addr = "dual1pk99xge6q94qtu3568x3qhp68zzv0mx7za4ct008ks36qhx5tvss3qawfh";
let cosmos_address = CosmosAddress::from_str(addr).unwrap();
assert_eq!(
cosmos_address.digest,
H256::from_str("0d8a53233a016a05f234d1cd105c3a3884c7ecde176b85bde7b423a05cd45b21")
.unwrap()
);
}

#[test]
fn test_bech32_decode_from_cosmos_key() {
let hex_key = "0x5486418967eabc770b0fcb995f7ef6d9a72f7fc195531ef76c5109f44f51af26";
let key = hex_or_base58_to_h256(hex_key).unwrap();
let prefix = "neutron";
let addr = CosmosAddress::from_privkey(key.as_bytes(), prefix)
.expect("Cosmos address creation failed");
assert_eq!(
addr.address(),
"neutron1kknekjxg0ear00dky5ykzs8wwp2gz62z9s6aaj"
);
}

#[test]
fn test_bech32_encode_from_h256() {
let hex_key = "0x1b16866227825a5166eb44031cdcf6568b3e80b52f2806e01b89a34dc90ae616";
let key = hex_or_base58_to_h256(hex_key).unwrap();
let prefix = "dual";
let addr = CosmosAddress::from_h256(key, prefix).expect("Cosmos address creation failed");
assert_eq!(
addr.address(),
"dual1rvtgvc38sfd9zehtgsp3eh8k269naq949u5qdcqm3x35mjg2uctqfdn3yq"
);
}
}
28 changes: 0 additions & 28 deletions rust/chains/hyperlane-cosmos/src/libs/binary.rs

This file was deleted.

5 changes: 1 addition & 4 deletions rust/chains/hyperlane-cosmos/src/libs/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
/// This module contains all the verification variables the libraries used by the Hyperlane Cosmos chain.
pub mod verify;

/// This module contains all the Binary variables used by the Hyperlane Cosmos chain.
pub mod binary;
pub mod address;
Loading

0 comments on commit 2a7d6d6

Please sign in to comment.