Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: delegate keypair encoding to cosmrs #2887

Merged
merged 7 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😎

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be a good place to have some quick unit tests if we're uncertain about the encoding / decoding like iiuc you flag in the PR description

/// 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
Loading