From 66bc49e1bb77e4ce940f067e28e3060c3d840d97 Mon Sep 17 00:00:00 2001 From: yjamin Date: Fri, 24 Jan 2025 10:58:23 +0100 Subject: [PATCH 1/4] feat: cosmos native --- rust/main/Cargo.lock | 65 +++ rust/main/Cargo.toml | 15 +- .../chains/hyperlane-cosmos-native/Cargo.toml | 51 ++ .../hyperlane-cosmos-native/src/error.rs | 94 +++ .../hyperlane-cosmos-native/src/indexers.rs | 11 + .../src/indexers/delivery.rs | 104 ++++ .../src/indexers/dispatch.rs | 121 ++++ .../src/indexers/gas_paymaster.rs | 168 ++++++ .../src/indexers/indexer.rs | 239 ++++++++ .../src/indexers/tree_insertion.rs | 122 ++++ .../chains/hyperlane-cosmos-native/src/ism.rs | 143 +++++ .../chains/hyperlane-cosmos-native/src/lib.rs | 24 + .../src/libs/account.rs | 94 +++ .../src/libs/account/tests.rs | 74 +++ .../src/libs/address.rs | 167 ++++++ .../hyperlane-cosmos-native/src/libs/mod.rs | 8 + .../hyperlane-cosmos-native/src/mailbox.rs | 163 ++++++ .../src/merkle_tree_hook.rs | 136 +++++ .../hyperlane-cosmos-native/src/providers.rs | 9 + .../src/providers/cosmos.rs | 535 ++++++++++++++++++ .../src/providers/grpc.rs | 419 ++++++++++++++ .../src/providers/rest.rs | 330 +++++++++++ .../hyperlane-cosmos-native/src/signers.rs | 53 ++ .../src/trait_builder.rs | 160 ++++++ .../src/validator_announce.rs | 118 ++++ rust/main/config/testnet_config.json | 263 ++++++++- rust/main/hyperlane-base/Cargo.toml | 1 + .../src/contract_sync/cursors/mod.rs | 4 + .../cursors/sequence_aware/forward.rs | 2 +- .../hyperlane-base/src/settings/chains.rs | 99 ++++ rust/main/hyperlane-base/src/settings/mod.rs | 1 + .../src/settings/parser/connection_parser.rs | 96 ++++ .../hyperlane-base/src/settings/signers.rs | 26 + .../src/traits/checkpoint_syncer.rs | 3 +- rust/main/hyperlane-core/src/chain.rs | 23 +- 35 files changed, 3917 insertions(+), 24 deletions(-) create mode 100644 rust/main/chains/hyperlane-cosmos-native/Cargo.toml create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/error.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/indexers.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/indexers/indexer.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/ism.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/lib.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/libs/account.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/libs/account/tests.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/libs/address.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/libs/mod.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/merkle_tree_hook.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/providers.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/providers/rest.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/signers.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs diff --git a/rust/main/Cargo.lock b/rust/main/Cargo.lock index 110954d2f7..2fef0b728a 100644 --- a/rust/main/Cargo.lock +++ b/rust/main/Cargo.lock @@ -4427,6 +4427,7 @@ dependencies = [ "futures-util", "hyperlane-core", "hyperlane-cosmos", + "hyperlane-cosmos-native", "hyperlane-ethereum", "hyperlane-fuel", "hyperlane-sealevel", @@ -4543,6 +4544,47 @@ dependencies = [ "url", ] +[[package]] +name = "hyperlane-cosmos-native" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.21.7", + "bech32 0.9.1", + "cosmrs", + "cosmwasm-std 2.1.3", + "crypto", + "derive-new", + "futures", + "hex 0.4.3", + "http 0.2.12", + "hyper", + "hyper-tls", + "hyperlane-core", + "hyperlane-cosmwasm-interface", + "injective-protobuf", + "injective-std", + "itertools 0.12.1", + "once_cell", + "prost 0.13.4", + "protobuf", + "reqwest", + "ripemd", + "serde", + "serde_json", + "sha2 0.10.8", + "sha256", + "tendermint", + "tendermint-rpc", + "thiserror", + "time", + "tokio", + "tonic 0.9.2", + "tracing", + "tracing-futures", + "url", +] + [[package]] name = "hyperlane-cosmwasm-interface" version = "0.0.6-rc6" @@ -6560,6 +6602,16 @@ dependencies = [ "prost-derive 0.12.6", ] +[[package]] +name = "prost" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +dependencies = [ + "bytes", + "prost-derive 0.13.4", +] + [[package]] name = "prost-derive" version = "0.11.9" @@ -6586,6 +6638,19 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "prost-derive" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", +] + [[package]] name = "prost-types" version = "0.11.9" diff --git a/rust/main/Cargo.toml b/rust/main/Cargo.toml index 6a0b7cbb35..6797135b1c 100644 --- a/rust/main/Cargo.toml +++ b/rust/main/Cargo.toml @@ -4,6 +4,7 @@ members = [ "agents/scraper", "agents/validator", "chains/hyperlane-cosmos", + "chains/hyperlane-cosmos-native", "chains/hyperlane-ethereum", "chains/hyperlane-fuel", "chains/hyperlane-sealevel", @@ -40,7 +41,6 @@ bincode = "1.3" borsh = "0.9" bs58 = "0.5.0" bytes = "1" -clap = "4" chrono = "*" color-eyre = "0.6" config = "0.13.3" @@ -55,7 +55,6 @@ cosmrs = { version = "0.14", default-features = false, features = [ cosmwasm-std = "*" crunchy = "0.2" ctrlc = "3.2" -curve25519-dalek = { version = "~3.2", features = ["serde"] } derive-new = "0.5" derive_builder = "0.12" derive_more = "0.99" @@ -67,7 +66,6 @@ fuels = "0.65.0" fuels-code-gen = "0.65.0" futures = "0.3" futures-util = "0.3" -generic-array = { version = "0.14", features = ["serde", "more_lengths"] } # Required for WASM support https://docs.rs/getrandom/latest/getrandom/#webassembly-support bech32 = "0.9.1" elliptic-curve = "0.13.8" @@ -96,7 +94,6 @@ num-traits = "0.2" once_cell = "1.18.0" parking_lot = "0.12" paste = "1.0" -pretty_env_logger = "0.5.0" primitive-types = "=0.12.1" prometheus = "0.13" protobuf = "*" @@ -104,7 +101,6 @@ rand = "0.8.5" regex = "1.5" reqwest = "0.11" ripemd = "0.1.3" -rlp = "=0.5.2" rocksdb = "0.21.0" sea-orm = { version = "0.11.1", features = [ "sqlx-postgres", @@ -117,10 +113,7 @@ sea-orm-migration = { version = "0.11.1", features = [ "sqlx-postgres", "runtime-tokio-native-tls", ] } -semver = "1.0" serde = { version = "1.0", features = ["derive"] } -serde_bytes = "0.11" -serde_derive = "1.0" serde_json = "1.0" sha2 = { version = "0.10.6", default-features = false } sha256 = "1.1.4" @@ -160,15 +153,9 @@ ya-gcp = { version = "0.11.3", features = ["storage"] } ## TODO: remove this cosmwasm-schema = "1.2.7" -[profile.release.package.access-control] -overflow-checks = true - [profile.release.package.account-utils] overflow-checks = true -[profile.release.package.ecdsa-signature] -overflow-checks = true - [profile.release.package.hyperlane-sealevel-interchain-security-module-interface] overflow-checks = true diff --git a/rust/main/chains/hyperlane-cosmos-native/Cargo.toml b/rust/main/chains/hyperlane-cosmos-native/Cargo.toml new file mode 100644 index 0000000000..7d4faa4aa5 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/Cargo.toml @@ -0,0 +1,51 @@ + +[package] +name = "hyperlane-cosmos-native" +documentation = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license-file = { workspace = true } +publish = { workspace = true } +version = { workspace = true } + +[dependencies] +async-trait = { workspace = true } +base64 = { workspace = true } +bech32 = { workspace = true } +cosmrs = { workspace = true, features = ["cosmwasm", "tokio", "grpc", "rpc"] } +cosmwasm-std = { workspace = true } +crypto = { path = "../../utils/crypto" } +derive-new = { workspace = true } +futures = { workspace = true } +hex = { workspace = true } +http = { workspace = true } +hyperlane-core = { path = "../../hyperlane-core", features = ["async"] } +hyperlane-cosmwasm-interface.workspace = true +hyper = { workspace = true } +hyper-tls = { workspace = true } +injective-protobuf = { workspace = true } +injective-std = { workspace = true } +itertools = { workspace = true } +once_cell = { workspace = true } +protobuf = { workspace = true } +ripemd = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +sha256 = { workspace = true } +tendermint = { workspace = true, features = ["rust-crypto", "secp256k1"] } +tendermint-rpc = { workspace = true } +time = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +reqwest = { workspace = true, features = ["json"] } +tonic = { workspace = true, features = [ + "transport", + "tls", + "tls-roots", + "tls-roots-common", +] } +tracing = { workspace = true } +tracing-futures = { workspace = true } +url = { workspace = true } +prost = "0.13.4" diff --git a/rust/main/chains/hyperlane-cosmos-native/src/error.rs b/rust/main/chains/hyperlane-cosmos-native/src/error.rs new file mode 100644 index 0000000000..1905c722c5 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/error.rs @@ -0,0 +1,94 @@ +use std::fmt::Debug; + +use cosmrs::proto::prost; + +use crypto::PublicKeyError; +use hyperlane_core::ChainCommunicationError; + +/// Errors from the crates specific to the hyperlane-cosmos +/// implementation. +/// This error can then be converted into the broader error type +/// in hyperlane-core using the `From` trait impl +#[derive(Debug, thiserror::Error)] +pub enum HyperlaneCosmosError { + /// base64 error + #[error("{0}")] + Base64(#[from] base64::DecodeError), + /// bech32 error + #[error("{0}")] + Bech32(#[from] bech32::Error), + /// 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), + #[error("{0}")] + /// Cosmrs Tendermint Error + CosmrsTendermintError(#[from] cosmrs::tendermint::Error), + #[error("{0}")] + /// CosmWasm Error + CosmWasmError(#[from] cosmwasm_std::StdError), + /// Tonic error + #[error("{0}")] + Tonic(#[from] tonic::transport::Error), + /// Tonic codegen error + #[error("{0}")] + TonicGenError(#[from] tonic::codegen::StdError), + /// Tendermint RPC Error + #[error(transparent)] + TendermintError(#[from] tendermint_rpc::error::Error), + /// Prost error + #[error("{0}")] + Prost(#[from] prost::DecodeError), + /// Protobuf error + #[error("{0}")] + Protobuf(#[from] protobuf::ProtobufError), + /// Fallback providers failed + #[error("Fallback providers failed. (Errors: {0:?})")] + FallbackProvidersFailed(Vec), + /// Public key error + #[error("{0}")] + PublicKeyError(String), + /// Address error + #[error("{0}")] + AddressError(String), + /// Signer info error + #[error("{0}")] + SignerInfoError(String), + /// Serde error + #[error("{0}")] + SerdeError(#[from] serde_json::Error), + /// Empty error + #[error("{0}")] + UnparsableEmptyField(String), + /// Parsing error + #[error("{0}")] + ParsingFailed(String), + /// Parsing attempt failed + #[error("Parsing attempt failed. (Errors: {0:?})")] + ParsingAttemptsFailed(Vec), + #[error("{0}")] + ReqwestError(reqwest::Error), +} + +impl From for ChainCommunicationError { + fn from(value: HyperlaneCosmosError) -> Self { + ChainCommunicationError::from_other(value) + } +} + +impl From for HyperlaneCosmosError { + fn from(value: PublicKeyError) -> Self { + HyperlaneCosmosError::PublicKeyError(value.to_string()) + } +} + +impl From for HyperlaneCosmosError { + fn from(value: reqwest::Error) -> Self { + HyperlaneCosmosError::ReqwestError(value) + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers.rs new file mode 100644 index 0000000000..13a902d397 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers.rs @@ -0,0 +1,11 @@ +mod delivery; +mod dispatch; +mod gas_paymaster; +mod indexer; +mod tree_insertion; + +pub use delivery::*; +pub use dispatch::*; +pub use gas_paymaster::*; +pub use indexer::*; +pub use tree_insertion::*; diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs new file mode 100644 index 0000000000..0b10e3848f --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs @@ -0,0 +1,104 @@ +use std::ops::RangeInclusive; +use std::{io::Cursor, sync::Arc}; + +use ::futures::future; +use async_trait::async_trait; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use cosmrs::{tx::Raw, Any, Tx}; +use once_cell::sync::Lazy; +use prost::Message; +use tendermint::abci::EventAttribute; +use tokio::{sync::futures, task::JoinHandle}; +use tracing::{instrument, warn}; + +use hyperlane_core::{ + rpc_clients::BlockNumberGetter, utils, ChainCommunicationError, ChainResult, ContractLocator, + Decode, HyperlaneContract, HyperlaneMessage, HyperlaneProvider, Indexed, Indexer, LogMeta, + SequenceAwareIndexer, H256, H512, +}; + +use crate::{ + ConnectionConf, CosmosNativeMailbox, CosmosNativeProvider, HyperlaneCosmosError, + MsgProcessMessage, Signer, +}; + +use super::{EventIndexer, ParsedEvent}; + +/// delivery indexer to check if a message was delivered +#[derive(Debug, Clone)] +pub struct CosmosNativeDeliveryIndexer { + indexer: EventIndexer, +} + +impl CosmosNativeDeliveryIndexer { + pub fn new(conf: ConnectionConf, locator: ContractLocator) -> ChainResult { + let provider = CosmosNativeProvider::new(locator.domain.clone(), conf, locator, None)?; + Ok(CosmosNativeDeliveryIndexer { + indexer: EventIndexer::new( + "hyperlane.mailbox.v1.Process".to_string(), + Arc::new(provider), + ), + }) + } + + #[instrument(err)] + fn delivery_parser(attrs: &Vec) -> ChainResult> { + let mut message_id: Option = None; + let mut contract_address: Option = None; + + for attribute in attrs { + let value = attribute.value.replace("\"", ""); + match attribute.key.as_str() { + "message_id" => { + message_id = Some(value.parse()?); + } + "origin_mailbox_id" => { + contract_address = Some(value.parse()?); + } + _ => continue, + } + } + + let contract_address = contract_address + .ok_or_else(|| ChainCommunicationError::from_other_str("missing contract_address"))?; + let message_id = message_id + .ok_or_else(|| ChainCommunicationError::from_other_str("missing message_id"))?; + + Ok(ParsedEvent::new(contract_address, message_id)) + } +} + +#[async_trait] +impl Indexer for CosmosNativeDeliveryIndexer { + #[instrument(err, skip(self))] + #[allow(clippy::blocks_in_conditions)] // TODO: `rustc` 1.80.1 clippy issue + async fn fetch_logs_in_range( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { + self.indexer + .fetch_logs_in_range(range, Self::delivery_parser) + .await + } + + async fn get_finalized_block_number(&self) -> ChainResult { + self.indexer.get_finalized_block_number().await + } + + async fn fetch_logs_by_tx_hash( + &self, + tx_hash: H512, + ) -> ChainResult, LogMeta)>> { + self.indexer + .fetch_logs_by_tx_hash(tx_hash, Self::delivery_parser) + .await + } +} + +#[async_trait] +impl SequenceAwareIndexer for CosmosNativeDeliveryIndexer { + async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { + let tip = Indexer::::get_finalized_block_number(&self).await?; + Ok((None, tip)) + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs new file mode 100644 index 0000000000..64c9fd7c1a --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs @@ -0,0 +1,121 @@ +use std::ops::RangeInclusive; +use std::{io::Cursor, sync::Arc}; + +use ::futures::future; +use async_trait::async_trait; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use cosmrs::{tx::Raw, Any, Tx}; +use hyperlane_core::ReorgPeriod; +use once_cell::sync::Lazy; +use prost::Message; +use tendermint::abci::EventAttribute; +use tokio::{sync::futures, task::JoinHandle}; +use tracing::{instrument, warn}; + +use hyperlane_core::{ + rpc_clients::BlockNumberGetter, utils, ChainCommunicationError, ChainResult, ContractLocator, + Decode, HyperlaneContract, HyperlaneMessage, HyperlaneProvider, Indexed, Indexer, LogMeta, + SequenceAwareIndexer, H256, H512, +}; + +use crate::{ + ConnectionConf, CosmosNativeMailbox, CosmosNativeProvider, HyperlaneCosmosError, + MsgProcessMessage, Signer, +}; + +use super::{EventIndexer, ParsedEvent}; + +/// Dispatch indexer to check if a new hyperlane message was dispatched +#[derive(Debug, Clone)] +pub struct CosmosNativeDispatchIndexer { + indexer: EventIndexer, + provider: Arc, + address: H256, +} + +impl CosmosNativeDispatchIndexer { + pub fn new(conf: ConnectionConf, locator: ContractLocator) -> ChainResult { + let provider = + CosmosNativeProvider::new(locator.domain.clone(), conf, locator.clone(), None)?; + let provider = Arc::new(provider); + + Ok(CosmosNativeDispatchIndexer { + indexer: EventIndexer::new( + "hyperlane.mailbox.v1.Dispatch".to_string(), + provider.clone(), + ), + provider, + address: locator.address, + }) + } + + #[instrument(err)] + fn dispatch_parser(attrs: &Vec) -> ChainResult> { + let mut message: Option = None; + let mut contract_address: Option = None; + + for attribute in attrs { + let value = attribute.value.replace("\"", ""); + let value = value.trim_start_matches("0x"); + match attribute.key.as_str() { + "message" => { + let mut reader = Cursor::new(hex::decode(value)?); + message = Some(HyperlaneMessage::read_from(&mut reader)?); + } + "origin_mailbox_id" => { + contract_address = Some(value.parse()?); + } + _ => {} + } + } + + let contract_address = contract_address + .ok_or_else(|| ChainCommunicationError::from_other_str("missing contract_address"))?; + let message = + message.ok_or_else(|| ChainCommunicationError::from_other_str("missing message"))?; + + Ok(ParsedEvent::new(contract_address, message)) + } +} + +#[async_trait] +impl Indexer for CosmosNativeDispatchIndexer { + #[instrument(err, skip(self))] + #[allow(clippy::blocks_in_conditions)] // TODO: `rustc` 1.80.1 clippy issue + async fn fetch_logs_in_range( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { + self.indexer + .fetch_logs_in_range(range, Self::dispatch_parser) + .await + } + + async fn get_finalized_block_number(&self) -> ChainResult { + self.indexer.get_finalized_block_number().await + } + + async fn fetch_logs_by_tx_hash( + &self, + tx_hash: H512, + ) -> ChainResult, LogMeta)>> { + self.indexer + .fetch_logs_by_tx_hash(tx_hash, Self::dispatch_parser) + .await + } +} + +#[async_trait] +impl SequenceAwareIndexer for CosmosNativeDispatchIndexer { + #[instrument(err, skip(self), ret)] + #[allow(clippy::blocks_in_conditions)] // TODO: `rustc` 1.80.1 clippy issue + async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { + let tip = Indexer::::get_finalized_block_number(&self).await?; + let sequence = self + .provider + .rest() + .leaf_count_at_height(self.address, tip) + .await?; + Ok((Some(sequence), tip)) + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs new file mode 100644 index 0000000000..f0fd32ad8f --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs @@ -0,0 +1,168 @@ +use std::ops::RangeInclusive; +use std::{io::Cursor, sync::Arc}; + +use ::futures::future; +use async_trait::async_trait; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use cosmrs::{tx::Raw, Any, Tx}; +use hyperlane_core::{ + HyperlaneChain, HyperlaneDomain, InterchainGasPaymaster, InterchainGasPayment, U256, +}; +use itertools::Itertools; +use once_cell::sync::Lazy; +use prost::Message; +use tendermint::abci::EventAttribute; +use tokio::{sync::futures, task::JoinHandle}; +use tracing::{instrument, warn}; + +use hyperlane_core::{ + rpc_clients::BlockNumberGetter, utils, ChainCommunicationError, ChainResult, ContractLocator, + Decode, HyperlaneContract, HyperlaneMessage, HyperlaneProvider, Indexed, Indexer, LogMeta, + SequenceAwareIndexer, H256, H512, +}; + +use crate::{ + ConnectionConf, CosmosNativeMailbox, CosmosNativeProvider, HyperlaneCosmosError, + MsgProcessMessage, Signer, +}; + +use super::{EventIndexer, ParsedEvent}; + +/// delivery indexer to check if a message was delivered +#[derive(Debug, Clone)] +pub struct CosmosNativeGasPaymaster { + indexer: EventIndexer, + address: H256, + domain: HyperlaneDomain, + provider: CosmosNativeProvider, +} + +impl InterchainGasPaymaster for CosmosNativeGasPaymaster {} + +impl CosmosNativeGasPaymaster { + pub fn new(conf: ConnectionConf, locator: ContractLocator) -> ChainResult { + let provider = + CosmosNativeProvider::new(locator.domain.clone(), conf.clone(), locator.clone(), None)?; + Ok(CosmosNativeGasPaymaster { + indexer: EventIndexer::new( + "hyperlane.mailbox.v1.GasPayment".to_string(), + Arc::new(provider), + ), + address: locator.address.clone(), + domain: locator.domain.clone(), + provider: CosmosNativeProvider::new(locator.domain.clone(), conf, locator, None)?, + }) + } + + #[instrument(err)] + fn gas_payment_parser( + attrs: &Vec, + ) -> ChainResult> { + let mut message_id: Option = None; + let mut igp_id: Option = None; + let mut gas_amount: Option = None; + let mut payment: Option = None; + let mut destination: Option = None; + + for attribute in attrs { + let value = attribute.value.replace("\"", ""); + match attribute.key.as_str() { + "igp_id" => igp_id = Some(value.parse()?), + "message_id" => message_id = Some(value.parse()?), + "gas_amount" => gas_amount = Some(U256::from_dec_str(&value)?), + "payment" => payment = Some(U256::from_dec_str(&value)?), + "destination" => destination = Some(value.parse()?), + _ => continue, + } + } + + let message_id = message_id + .ok_or_else(|| ChainCommunicationError::from_other_str("missing message_id"))?; + let igp_id = + igp_id.ok_or_else(|| ChainCommunicationError::from_other_str("missing igp_id"))?; + let gas_amount = gas_amount + .ok_or_else(|| ChainCommunicationError::from_other_str("missing gas_amount"))?; + let payment = + payment.ok_or_else(|| ChainCommunicationError::from_other_str("missing payment"))?; + let destination = destination + .ok_or_else(|| ChainCommunicationError::from_other_str("missing destination"))?; + + Ok(ParsedEvent::new( + igp_id, + InterchainGasPayment { + destination, + message_id, + payment, + gas_amount, + }, + )) + } +} + +impl HyperlaneChain for CosmosNativeGasPaymaster { + // Return the domain + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + // A provider for the chain + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +impl HyperlaneContract for CosmosNativeGasPaymaster { + // Return the address of this contract + fn address(&self) -> H256 { + self.address + } +} + +#[async_trait] +impl Indexer for CosmosNativeGasPaymaster { + async fn fetch_logs_in_range( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { + let result = self + .indexer + .fetch_logs_in_range(range, Self::gas_payment_parser) + .await + .map(|logs| { + logs.into_iter() + .filter(|payment| payment.1.address == self.address) + .collect() + }); + result + } + + async fn get_finalized_block_number(&self) -> ChainResult { + self.indexer.get_finalized_block_number().await + } + + async fn fetch_logs_by_tx_hash( + &self, + tx_hash: H512, + ) -> ChainResult, LogMeta)>> { + let result = self + .indexer + .fetch_logs_by_tx_hash(tx_hash, Self::gas_payment_parser) + .await + .map(|logs| { + let result = logs + .into_iter() + .filter(|payment| payment.1.address == self.address) + .collect(); + result + }); + result + } +} + +#[async_trait] +impl SequenceAwareIndexer for CosmosNativeGasPaymaster { + async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { + let tip = Indexer::::get_finalized_block_number(&self).await?; + Ok((None, tip)) + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/indexer.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/indexer.rs new file mode 100644 index 0000000000..273a36eb55 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/indexer.rs @@ -0,0 +1,239 @@ +use crate::{error, CosmosNativeProvider}; +use futures::future; +use hyperlane_core::rpc_clients::BlockNumberGetter; +use hyperlane_core::{ + ChainCommunicationError, ChainResult, HyperlaneProvider, Indexed, LogMeta, H256, H512, U256, +}; +use itertools::Itertools; +use std::fmt::Debug; +use std::ops::RangeInclusive; +use std::sync::Arc; +use tendermint::abci::{Event, EventAttribute}; +use tendermint::hash::Algorithm; +use tendermint::Hash; +use tendermint_rpc::endpoint::block::Response as BlockResponse; +use tendermint_rpc::endpoint::block_results::{self, Response as BlockResultsResponse}; +use tendermint_rpc::endpoint::tx; +use tokio::task::JoinHandle; +use tracing::{debug, error, event, trace, warn, Level}; + +#[derive(Debug, Eq, PartialEq)] +/// An event parsed from the RPC response. +pub struct ParsedEvent { + contract_address: H256, + event: T, +} + +impl ParsedEvent { + /// Create a new ParsedEvent. + pub fn new(contract_address: H256, event: T) -> Self { + Self { + contract_address, + event, + } + } + + /// Get the inner event + pub fn inner(self) -> T { + self.event + } +} + +#[derive(Debug, Clone)] +pub struct EventIndexer { + target_type: String, + provider: Arc, +} + +pub type Parser = for<'a> fn(&'a Vec) -> ChainResult>; + +impl EventIndexer { + pub fn new(target_type: String, provider: Arc) -> EventIndexer { + EventIndexer { + target_type: target_type, + provider: provider, + } + } + + pub async fn get_finalized_block_number(&self) -> ChainResult { + let result = self.provider.grpc().get_block_number().await?; + Ok(result as u32) + } + + pub async fn fetch_logs_by_tx_hash( + &self, + tx_hash: H512, + parser: Parser, + ) -> ChainResult, LogMeta)>> + where + T: PartialEq + 'static, + Indexed: From, + { + let tx_response = self.provider.get_tx(&tx_hash).await?; + let block_height = tx_response.height; + let block = self + .provider + .get_block_by_height(block_height.into()) + .await?; + + let result: Vec<_> = self + .handle_tx(tx_response, block.hash, parser) + .map(|(value, logs)| (value.into(), logs)) + .collect(); + Ok(result) + } + + pub async fn fetch_logs_in_range( + &self, + range: RangeInclusive, + parser: Parser, + ) -> ChainResult, LogMeta)>> + where + T: PartialEq + Debug + 'static + Send + Sync, + Indexed: From, + { + let futures: Vec<_> = range + .map(|block_height| { + let clone = self.clone(); + tokio::spawn(async move { + let logs = Self::get_logs_in_block(&clone, block_height, parser).await; + (logs, block_height) + }) + }) + .collect(); + + let result = future::join_all(futures) + .await + .into_iter() + .flatten() + .map(|(logs, block_number)| { + if let Err(err) = &logs { + warn!(?err, "error"); + warn!(?err, ?block_number, "Failed to fetch logs for block"); + } + logs + }) + // Propagate errors from any of the queries. This will cause the entire range to be retried, + // including successful ones, but we don't have a way to handle partial failures in a range for now. + // This is also why cosmos indexing should be run with small chunks (currently set to 5). + .collect::, _>>()? + .into_iter() + .flatten() + .map(|(log, meta)| (log.into(), meta)) + .collect(); + Ok(result) + } + + async fn get_logs_in_block( + &self, + block_height: u32, + parser: Parser, + ) -> ChainResult> + where + T: PartialEq + Debug + 'static, + { + let block = self.provider.get_block(block_height).await?; + let block_results = self.provider.get_block_results(block_height).await?; + let result = self.handle_txs(block, block_results, parser); + Ok(result) + } + + // Iterate through all txs, filter out failed txs, find target events + // in successful txs, and parse them. + fn handle_txs( + &self, + block: BlockResponse, + block_results: BlockResultsResponse, + parser: Parser, + ) -> Vec<(T, LogMeta)> + where + T: PartialEq + Debug + 'static, + { + let Some(tx_results) = block_results.txs_results else { + return vec![]; + }; + + let tx_hashes: Vec = block + .clone() + .block + .data + .into_iter() + .filter_map(|tx| hex::decode(sha256::digest(tx.as_slice())).ok()) + .filter_map(|hash| Hash::from_bytes(Algorithm::Sha256, hash.as_slice()).ok()) + .collect(); + + tx_results + .into_iter() + .enumerate() + .filter_map(move |(idx, tx)| { + let Some(tx_hash) = tx_hashes.get(idx) else { + debug!(?tx, "No tx hash found for tx"); + return None; + }; + if tx.code.is_err() { + debug!(?tx_hash, "Not indexing failed transaction"); + return None; + } + + // We construct a simplified structure `tx::Response` here so that we can + // reuse `handle_tx` method below. + let tx_response = tx::Response { + hash: *tx_hash, + height: block_results.height, + index: idx as u32, + tx_result: tx, + tx: vec![], + proof: None, + }; + + let block_hash = H256::from_slice(block.block_id.hash.as_bytes()); + + Some(self.handle_tx(tx_response, block_hash, parser)) + }) + .flatten() + .collect() + } + + // Iter through all events in the tx, looking for any target events + // made by the contract we are indexing. + fn handle_tx( + &self, + tx: tx::Response, + block_hash: H256, + parser: Parser, + ) -> impl Iterator + '_ + where + T: PartialEq + 'static, + { + let tx_events = tx.tx_result.events; + let tx_hash = tx.hash; + let tx_index = tx.index; + let block_height = tx.height; + + tx_events.into_iter().enumerate().filter_map(move |(log_idx, event)| { + + if event.kind.as_str() != self.target_type { + return None; + } + + parser(&event.attributes) + .map_err(|err| { + // This can happen if we attempt to parse an event that just happens + // to have the same name but a different structure. + println!("Failed to parse event attributes: {}", err); + trace!(?err, tx_hash=?tx_hash, log_idx, ?event, "Failed to parse event attributes"); + }) + .ok() + .and_then(|parsed_event| { + Some((parsed_event.event, LogMeta { + address: parsed_event.contract_address, + block_number: block_height.value(), + block_hash, + transaction_id: H256::from_slice(tx_hash.as_bytes()).into(), + transaction_index: tx_index as u64, + log_index: U256::from(log_idx), + })) + }) + }) + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs new file mode 100644 index 0000000000..ba1ef19888 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs @@ -0,0 +1,122 @@ +use std::ops::RangeInclusive; +use std::{io::Cursor, sync::Arc}; + +use ::futures::future; +use async_trait::async_trait; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use cosmrs::{tx::Raw, Any, Tx}; +use hyperlane_core::MerkleTreeInsertion; +use once_cell::sync::Lazy; +use prost::Message; +use tendermint::abci::EventAttribute; +use tokio::{sync::futures, task::JoinHandle}; +use tracing::{instrument, warn}; + +use hyperlane_core::{ + rpc_clients::BlockNumberGetter, utils, ChainCommunicationError, ChainResult, ContractLocator, + Decode, HyperlaneContract, HyperlaneMessage, HyperlaneProvider, Indexed, Indexer, LogMeta, + SequenceAwareIndexer, H256, H512, +}; + +use crate::{ + ConnectionConf, CosmosNativeMailbox, CosmosNativeProvider, HyperlaneCosmosError, Signer, +}; + +use super::{EventIndexer, ParsedEvent}; + +/// delivery indexer to check if a message was delivered +#[derive(Debug, Clone)] +pub struct CosmosNativeTreeInsertionIndexer { + indexer: EventIndexer, + provider: Arc, + address: H256, +} + +impl CosmosNativeTreeInsertionIndexer { + pub fn new(conf: ConnectionConf, locator: ContractLocator) -> ChainResult { + let provider = + CosmosNativeProvider::new(locator.domain.clone(), conf, locator.clone(), None)?; + let provider = Arc::new(provider); + Ok(CosmosNativeTreeInsertionIndexer { + indexer: EventIndexer::new( + "hyperlane.mailbox.v1.InsertedIntoTree".to_string(), + provider.clone(), + ), + provider, + address: locator.address, + }) + } + + #[instrument(err)] + fn tree_insertion_parser( + attrs: &Vec, + ) -> ChainResult> { + let mut message_id: Option = None; + let mut leaf_index: Option = None; + let mut contract_address: Option = None; + + for attribute in attrs { + let value = attribute.value.replace("\"", ""); + match attribute.key.as_str() { + "message_id" => { + message_id = Some(value.parse()?); + } + "mailbox_id" => { + contract_address = Some(value.parse()?); + } + "index" => leaf_index = Some(value.parse()?), + _ => continue, + } + } + + let contract_address = contract_address + .ok_or_else(|| ChainCommunicationError::from_other_str("missing contract_address"))?; + let message_id = message_id + .ok_or_else(|| ChainCommunicationError::from_other_str("missing message_id"))?; + let leaf_index = leaf_index + .ok_or_else(|| ChainCommunicationError::from_other_str("missing leafindex"))?; + let insertion = MerkleTreeInsertion::new(leaf_index, message_id); + + Ok(ParsedEvent::new(contract_address, insertion)) + } +} + +#[async_trait] +impl Indexer for CosmosNativeTreeInsertionIndexer { + #[instrument(err, skip(self))] + #[allow(clippy::blocks_in_conditions)] // TODO: `rustc` 1.80.1 clippy issue + async fn fetch_logs_in_range( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { + self.indexer + .fetch_logs_in_range(range, Self::tree_insertion_parser) + .await + } + + async fn get_finalized_block_number(&self) -> ChainResult { + self.indexer.get_finalized_block_number().await + } + + async fn fetch_logs_by_tx_hash( + &self, + tx_hash: H512, + ) -> ChainResult, LogMeta)>> { + self.indexer + .fetch_logs_by_tx_hash(tx_hash, Self::tree_insertion_parser) + .await + } +} + +#[async_trait] +impl SequenceAwareIndexer for CosmosNativeTreeInsertionIndexer { + async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { + let tip = Indexer::::get_finalized_block_number(&self).await?; + let sequence = self + .provider + .rest() + .leaf_count_at_height(self.address, tip) + .await?; + Ok((Some(sequence), tip)) + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/ism.rs b/rust/main/chains/hyperlane-cosmos-native/src/ism.rs new file mode 100644 index 0000000000..56ab3a5e75 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/ism.rs @@ -0,0 +1,143 @@ +use core::panic; +use std::str::FromStr; + +use hyperlane_core::{ + ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, + HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, InterchainSecurityModule, ModuleType, + MultisigIsm, ReorgPeriod, H160, H256, U256, +}; +use tonic::async_trait; + +use crate::{ConnectionConf, CosmosNativeProvider, Signer, ISM}; + +#[derive(Debug)] +pub struct CosmosNativeIsm { + /// The domain of the ISM contract. + domain: HyperlaneDomain, + /// The address of the ISM contract. + address: H256, + /// The provider for the ISM contract. + provider: Box, +} + +/// The Cosmos Interchain Security Module Implementation. +impl CosmosNativeIsm { + /// Creates a new Cosmos Interchain Security Module. + pub fn new(conf: &ConnectionConf, locator: ContractLocator) -> ChainResult { + let provider = + CosmosNativeProvider::new(locator.domain.clone(), conf.clone(), locator.clone(), None)?; + + Ok(Self { + domain: locator.domain.clone(), + address: locator.address, + provider: Box::new(provider), + }) + } + + async fn get_ism(&self) -> ChainResult> { + let isms = self.provider.rest().isms(ReorgPeriod::None).await?; + + for ism in isms { + match ism.clone() { + ISM::NoOpISM { id, .. } if id.parse::()? == self.address => { + return Ok(Some(ism)) + } + ISM::MultiSigISM { id, .. } if id.parse::()? == self.address => { + return Ok(Some(ism)) + } + _ => {} + } + } + + Ok(None) + } +} + +impl HyperlaneContract for CosmosNativeIsm { + /// Return the address of this contract + fn address(&self) -> H256 { + self.address.clone() + } +} + +impl HyperlaneChain for CosmosNativeIsm { + /// Return the Domain + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + /// A provider for the chain + fn provider(&self) -> Box { + self.provider.clone() + } +} + +/// Interface for the InterchainSecurityModule chain contract. Allows abstraction over +/// different chains +#[async_trait] +impl InterchainSecurityModule for CosmosNativeIsm { + /// Returns the module type of the ISM compliant with the corresponding + /// metadata offchain fetching and onchain formatting standard. + async fn module_type(&self) -> ChainResult { + let isms = self.provider.rest().isms(ReorgPeriod::None).await?; + for ism in isms { + match ism { + ISM::NoOpISM { id, .. } if id.parse::()? == self.address => { + return Ok(ModuleType::Null) + } + ISM::MultiSigISM { id, .. } if id.parse::()? == self.address => { + return Ok(ModuleType::MerkleRootMultisig) + } + _ => {} + } + } + Err(ChainCommunicationError::from_other_str( + "cannot convert ism to contract type", + )) + } + + /// Dry runs the `verify()` ISM call and returns `Some(gas_estimate)` if the call + /// succeeds. + async fn dry_run_verify( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + ) -> ChainResult> { + // TODO: is only relevant for aggeration isms -> cosmos native does not support them yet + Ok(Some(1.into())) + } +} + +/// Interface for the MultisigIsm chain contract. Allows abstraction over +/// different chains +#[async_trait] +impl MultisigIsm for CosmosNativeIsm { + /// Returns the validator and threshold needed to verify message + async fn validators_and_threshold( + &self, + message: &HyperlaneMessage, + ) -> ChainResult<(Vec, u8)> { + let ism = self.get_ism().await?.ok_or_else(|| { + ChainCommunicationError::from_other_str("ism contract does not exists on chain") + })?; + + match ism { + ISM::MultiSigISM { + id, + creator, + ism_type, + multi_sig, + } => { + let validators = multi_sig + .validator_pub_keys + .iter() + .map(|v| H160::from_str(v).map(H256::from)) + .collect::, _>>()?; + Ok((validators, multi_sig.threshold as u8)) + } + _ => Err(ChainCommunicationError::from_other_str( + "ISM address is not a MultiSigISM", + )), + } + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/lib.rs b/rust/main/chains/hyperlane-cosmos-native/src/lib.rs new file mode 100644 index 0000000000..414616b675 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/lib.rs @@ -0,0 +1,24 @@ +//! Implementation of hyperlane for cosmos. + +#![forbid(unsafe_code)] +// #![warn(missing_docs)] +// TODO: Remove once we start filling things in +#![allow(unused_variables)] +#![allow(unused_imports)] // TODO: `rustc` 1.80.1 clippy issue + +mod error; +mod indexers; +mod ism; +mod libs; +mod mailbox; +mod merkle_tree_hook; +mod providers; +mod signers; +mod trait_builder; +mod validator_announce; + +pub use { + self::error::*, self::indexers::*, self::ism::*, self::libs::*, self::mailbox::*, + self::merkle_tree_hook::*, self::providers::*, self::signers::*, self::trait_builder::*, + self::validator_announce::*, +}; diff --git a/rust/main/chains/hyperlane-cosmos-native/src/libs/account.rs b/rust/main/chains/hyperlane-cosmos-native/src/libs/account.rs new file mode 100644 index 0000000000..d29afd1d03 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/libs/account.rs @@ -0,0 +1,94 @@ +use cosmrs::{crypto::PublicKey, AccountId}; +use hyperlane_cosmwasm_interface::types::keccak256_hash; +use tendermint::account::Id as TendermintAccountId; +use tendermint::public_key::PublicKey as TendermintPublicKey; + +use crypto::decompress_public_key; +use hyperlane_core::Error::Overflow; +use hyperlane_core::{AccountAddressType, ChainCommunicationError, ChainResult, H256}; + +use crate::HyperlaneCosmosError; + +pub(crate) struct CosmosAccountId<'a> { + account_id: &'a AccountId, +} + +impl<'a> CosmosAccountId<'a> { + pub fn new(account_id: &'a AccountId) -> Self { + Self { account_id } + } + + /// Calculate AccountId from public key depending on provided prefix + pub fn account_id_from_pubkey( + pub_key: PublicKey, + prefix: &str, + account_address_type: &AccountAddressType, + ) -> ChainResult { + match account_address_type { + AccountAddressType::Bitcoin => Self::bitcoin_style(pub_key, prefix), + AccountAddressType::Ethereum => Self::ethereum_style(pub_key, prefix), + } + } + + /// Returns a Bitcoin style address: RIPEMD160(SHA256(pubkey)) + /// Source: `` + fn bitcoin_style(pub_key: PublicKey, prefix: &str) -> ChainResult { + // Get the inner type + let tendermint_pub_key = TendermintPublicKey::from(pub_key); + // Get the RIPEMD160(SHA256(pub_key)) + let tendermint_id = TendermintAccountId::from(tendermint_pub_key); + // Bech32 encoding + let account_id = AccountId::new(prefix, tendermint_id.as_bytes()) + .map_err(Into::::into)?; + + Ok(account_id) + } + + /// Returns an Ethereum style address: KECCAK256(pubkey)[20] + /// Parameter `pub_key` is a compressed public key. + fn ethereum_style(pub_key: PublicKey, prefix: &str) -> ChainResult { + let decompressed_public_key = decompress_public_key(&pub_key.to_bytes()) + .map_err(Into::::into)?; + + let hash = keccak256_hash(&decompressed_public_key[1..]); + + let mut bytes = [0u8; 20]; + bytes.copy_from_slice(&hash.as_slice()[12..]); + + let account_id = + AccountId::new(prefix, bytes.as_slice()).map_err(Into::::into)?; + + Ok(account_id) + } +} + +impl TryFrom<&CosmosAccountId<'_>> for H256 { + type Error = HyperlaneCosmosError; + + /// Builds a H256 digest from a cosmos AccountId (Bech32 encoding) + fn try_from(account_id: &CosmosAccountId) -> Result { + let bytes = account_id.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 + let msg = "account address is too large to fit it a H256"; + return Err(HyperlaneCosmosError::AddressError(msg.to_owned())); + }; + 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 TryFrom> for H256 { + type Error = HyperlaneCosmosError; + + /// Builds a H256 digest from a cosmos AccountId (Bech32 encoding) + fn try_from(account_id: CosmosAccountId) -> Result { + (&account_id).try_into() + } +} + +#[cfg(test)] +mod tests; diff --git a/rust/main/chains/hyperlane-cosmos-native/src/libs/account/tests.rs b/rust/main/chains/hyperlane-cosmos-native/src/libs/account/tests.rs new file mode 100644 index 0000000000..0ba8f73d74 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/libs/account/tests.rs @@ -0,0 +1,74 @@ +use cosmrs::crypto::PublicKey; +use cosmwasm_std::HexBinary; + +use crypto::decompress_public_key; +use hyperlane_core::AccountAddressType; +use AccountAddressType::{Bitcoin, Ethereum}; + +use crate::CosmosAccountId; + +const COMPRESSED_PUBLIC_KEY: &str = + "02962d010010b6eec66846322704181570d89e28236796579c535d2e44d20931f4"; +const INJECTIVE_ADDRESS: &str = "inj1m6ada382hfuxvuke4h9p4uswhn2qcca7mlg0dr"; +const NEUTRON_ADDRESS: &str = "neutron1mydju5alsmhnfsawy0j4lyns70l7qukgdgy45w"; + +#[test] +fn test_account_id() { + // given + let pub_key = compressed_public_key(); + + // when + let neutron_account_id = + CosmosAccountId::account_id_from_pubkey(pub_key, "neutron", &Bitcoin).unwrap(); + let injective_account_id = + CosmosAccountId::account_id_from_pubkey(pub_key, "inj", &Ethereum).unwrap(); + + // then + assert_eq!(neutron_account_id.as_ref(), NEUTRON_ADDRESS); + assert_eq!(injective_account_id.as_ref(), INJECTIVE_ADDRESS); +} + +#[test] +fn test_bitcoin_style() { + // given + let compressed = compressed_public_key(); + let decompressed = decompressed_public_key(); + + // when + let from_compressed = CosmosAccountId::bitcoin_style(compressed, "neutron").unwrap(); + let from_decompressed = CosmosAccountId::bitcoin_style(decompressed, "neutron").unwrap(); + + // then + assert_eq!(from_compressed.as_ref(), NEUTRON_ADDRESS); + assert_eq!(from_decompressed.as_ref(), NEUTRON_ADDRESS); +} + +#[test] +fn test_ethereum_style() { + // given + let compressed = compressed_public_key(); + let decompressed = decompressed_public_key(); + + // when + let from_compressed = CosmosAccountId::ethereum_style(compressed, "inj").unwrap(); + let from_decompressed = CosmosAccountId::ethereum_style(decompressed, "inj").unwrap(); + + // then + assert_eq!(from_compressed.as_ref(), INJECTIVE_ADDRESS); + assert_eq!(from_decompressed.as_ref(), INJECTIVE_ADDRESS); +} + +fn compressed_public_key() -> PublicKey { + let hex = hex::decode(COMPRESSED_PUBLIC_KEY).unwrap(); + let tendermint = tendermint::PublicKey::from_raw_secp256k1(&hex).unwrap(); + let pub_key = PublicKey::from(tendermint); + pub_key +} + +fn decompressed_public_key() -> PublicKey { + let hex = hex::decode(COMPRESSED_PUBLIC_KEY).unwrap(); + let decompressed = decompress_public_key(&hex).unwrap(); + let tendermint = tendermint::PublicKey::from_raw_secp256k1(&decompressed).unwrap(); + let pub_key = PublicKey::from(tendermint); + pub_key +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/libs/address.rs b/rust/main/chains/hyperlane-cosmos-native/src/libs/address.rs new file mode 100644 index 0000000000..e538e1c3b0 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/libs/address.rs @@ -0,0 +1,167 @@ +use std::str::FromStr; + +use cosmrs::{ + crypto::{secp256k1::SigningKey, PublicKey}, + AccountId, +}; +use derive_new::new; +use hyperlane_core::{ + AccountAddressType, ChainCommunicationError, ChainResult, Error::Overflow, H256, +}; + +use crate::{CosmosAccountId, HyperlaneCosmosError}; + +/// Wrapper around the cosmrs AccountId type that abstracts bech32 encoding +#[derive(new, Debug, Clone)] +pub struct CosmosAddress { + /// Bech32 encoded cosmos account + account_id: AccountId, + /// Hex representation (digest) of cosmos account + digest: H256, +} + +impl CosmosAddress { + /// Creates a wrapper around a cosmrs AccountId from a private key byte array + pub fn from_privkey( + priv_key: &[u8], + prefix: &str, + account_address_type: &AccountAddressType, + ) -> ChainResult { + let pubkey = SigningKey::from_slice(priv_key) + .map_err(Into::::into)? + .public_key(); + Self::from_pubkey(pubkey, prefix, account_address_type) + } + + /// Returns an account address calculated from Bech32 encoding + pub fn from_account_id(account_id: AccountId) -> ChainResult { + // Hex digest + let digest = H256::try_from(&CosmosAccountId::new(&account_id))?; + Ok(CosmosAddress::new(account_id, digest)) + } + + /// Creates a wrapper around a cosmrs AccountId from a H256 digest + /// + /// - digest: H256 digest (hex representation of address) + /// - prefix: Bech32 prefix + /// - byte_count: Number of bytes to truncate the digest to. Cosmos addresses can sometimes + /// be less than 32 bytes, so this helps to serialize it in bech32 with the appropriate + /// length. + pub fn from_h256(digest: H256, prefix: &str, byte_count: usize) -> ChainResult { + // This is the hex-encoded version of the address + let untruncated_bytes = digest.as_bytes(); + + if byte_count > untruncated_bytes.len() { + return Err(Overflow.into()); + } + + let remainder_bytes_start = untruncated_bytes.len() - byte_count; + // Left-truncate the digest to the desired length + let bytes = &untruncated_bytes[remainder_bytes_start..]; + + // Bech32 encode it + let account_id = + AccountId::new(prefix, bytes).map_err(Into::::into)?; + Ok(CosmosAddress::new(account_id, digest)) + } + + /// 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 + } + + /// Calculates an account address depending on prefix and account address type + fn from_pubkey( + pubkey: PublicKey, + prefix: &str, + account_address_type: &AccountAddressType, + ) -> ChainResult { + let account_id = + CosmosAccountId::account_id_from_pubkey(pubkey, prefix, account_address_type)?; + Self::from_account_id(account_id) + } +} + +impl TryFrom<&CosmosAddress> for H256 { + type Error = ChainCommunicationError; + + fn try_from(cosmos_address: &CosmosAddress) -> Result { + H256::try_from(CosmosAccountId::new(&cosmos_address.account_id)) + .map_err(Into::::into) + } +} + +impl FromStr for CosmosAddress { + type Err = ChainCommunicationError; + + fn from_str(s: &str) -> Result { + let account_id = AccountId::from_str(s).map_err(Into::::into)?; + let digest = CosmosAccountId::new(&account_id).try_into()?; + 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, &AccountAddressType::Bitcoin) + .expect("Cosmos address creation failed"); + assert_eq!( + addr.address(), + "neutron1kknekjxg0ear00dky5ykzs8wwp2gz62z9s6aaj" + ); + + // Create an address with the same digest & explicitly set the byte count to 20, + // which should have the same result as the above. + let digest = addr.digest(); + let addr2 = + CosmosAddress::from_h256(digest, prefix, 20).expect("Cosmos address creation failed"); + assert_eq!(addr.address(), addr2.address()); + } + + #[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, 32).expect("Cosmos address creation failed"); + assert_eq!( + addr.address(), + "dual1rvtgvc38sfd9zehtgsp3eh8k269naq949u5qdcqm3x35mjg2uctqfdn3yq" + ); + + // Last 20 bytes only, which is 0x1cdcf6568b3e80b52f2806e01b89a34dc90ae616 + let addr = + CosmosAddress::from_h256(key, prefix, 20).expect("Cosmos address creation failed"); + assert_eq!( + addr.address(), + "dual1rnw0v45t86qt2tegqmsphzdrfhys4esk9ktul7" + ); + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/libs/mod.rs b/rust/main/chains/hyperlane-cosmos-native/src/libs/mod.rs new file mode 100644 index 0000000000..0ee55478e0 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/libs/mod.rs @@ -0,0 +1,8 @@ +pub(crate) use account::CosmosAccountId; +pub(crate) use address::CosmosAddress; + +/// This module contains conversions from Cosmos AccountId to H56 +mod account; + +/// This module contains all the verification variables the libraries used by the Hyperlane Cosmos chain. +mod address; diff --git a/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs b/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs new file mode 100644 index 0000000000..83a07e917d --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs @@ -0,0 +1,163 @@ +use cosmrs::Any; +use hex::ToHex; +use hyperlane_core::{ + rpc_clients::BlockNumberGetter, ChainResult, ContractLocator, HyperlaneChain, + HyperlaneContract, HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, Mailbox, + RawHyperlaneMessage, ReorgPeriod, TxCostEstimate, TxOutcome, H256, U256, +}; +use prost::Message; +use tonic::async_trait; + +use crate::{ConnectionConf, CosmosNativeProvider, MsgProcessMessage, Signer}; + +#[derive(Debug, Clone)] +pub struct CosmosNativeMailbox { + provider: CosmosNativeProvider, + domain: HyperlaneDomain, + address: H256, + signer: Option, +} + +impl CosmosNativeMailbox { + /// new cosmos native mailbox instance + pub fn new( + conf: ConnectionConf, + locator: ContractLocator, + signer: Option, + ) -> ChainResult { + Ok(CosmosNativeMailbox { + provider: CosmosNativeProvider::new( + locator.domain.clone(), + conf.clone(), + locator.clone(), + signer.clone(), + )?, + signer, + address: locator.address.clone(), + domain: locator.domain.clone(), + }) + } + + fn encode_hyperlane_message(&self, message: &HyperlaneMessage, metadata: &[u8]) -> Any { + let mailbox_id: String = self.address.encode_hex(); + let message = hex::encode(RawHyperlaneMessage::from(message)); + let metadata = hex::encode(metadata); + let signer = self + .signer + .as_ref() + .map_or("".to_string(), |signer| signer.address.clone()); + let process = MsgProcessMessage { + mailbox_id: "0x".to_string() + &mailbox_id, + metadata, + message, + relayer: signer, + }; + Any { + type_url: "/hyperlane.mailbox.v1.MsgProcessMessage".to_string(), + value: process.encode_to_vec(), + } + } +} + +impl HyperlaneChain for CosmosNativeMailbox { + #[doc = " Return the domain"] + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + #[doc = " A provider for the chain"] + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +impl HyperlaneContract for CosmosNativeMailbox { + #[doc = " Return the address of this contract."] + fn address(&self) -> H256 { + self.address + } +} + +#[async_trait] +impl Mailbox for CosmosNativeMailbox { + /// Gets the current leaf count of the merkle tree + /// + /// - `reorg_period` is how far behind the current block to query, if not specified + /// it will query at the latest block. + async fn count(&self, reorg_period: &ReorgPeriod) -> ChainResult { + self.provider + .rest() + .leaf_count(self.address, reorg_period.clone()) + .await + } + + /// Fetch the status of a message + async fn delivered(&self, id: H256) -> ChainResult { + self.provider.rest().delivered(id).await + } + + /// Fetch the current default interchain security module value + async fn default_ism(&self) -> ChainResult { + let mailbox = self + .provider + .rest() + .mailbox(self.address, ReorgPeriod::None) + .await?; + let default_ism: H256 = mailbox.default_ism.parse()?; + return Ok(default_ism); + } + + /// Get the recipient ism address + async fn recipient_ism(&self, recipient: H256) -> ChainResult { + self.provider.rest().recipient_ism(recipient).await + } + + /// Process a message with a proof against the provided signed checkpoint + async fn process( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + tx_gas_limit: Option, + ) -> ChainResult { + let any_encoded = self.encode_hyperlane_message(message, metadata); + let gas_limit = match tx_gas_limit { + Some(gas) => Some(gas.as_u64()), + None => None, + }; + + let response = self + .provider + .grpc() + .send(vec![any_encoded], gas_limit) + .await?; + + Ok(TxOutcome { + transaction_id: H256::from_slice(hex::decode(response.txhash)?.as_slice()).into(), + executed: response.code == 0, + gas_used: U256::from(response.gas_used), + gas_price: U256::one().try_into()?, + }) + } + + /// Estimate transaction costs to process a message. + async fn process_estimate_costs( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + ) -> ChainResult { + let hex_string = hex::encode(metadata); + let any_encoded = self.encode_hyperlane_message(message, metadata); + let gas_limit = self.provider.grpc().estimate_gas(vec![any_encoded]).await?; + Ok(TxCostEstimate { + gas_limit: gas_limit.into(), + gas_price: self.provider.grpc().gas_price(), + l2_gas_limit: None, + }) + } + + /// Get the calldata for a transaction to process a message with a proof + /// against the provided signed checkpoint + fn process_calldata(&self, message: &HyperlaneMessage, metadata: &[u8]) -> Vec { + todo!() // TODO: check if we really don't need that + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/merkle_tree_hook.rs b/rust/main/chains/hyperlane-cosmos-native/src/merkle_tree_hook.rs new file mode 100644 index 0000000000..ddd8e1b3f7 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/merkle_tree_hook.rs @@ -0,0 +1,136 @@ +use std::fmt::Debug; + +use async_trait::async_trait; +use base64::Engine; +use itertools::Itertools; +use tracing::instrument; + +use hyperlane_core::accumulator::incremental::IncrementalMerkle; +use hyperlane_core::{ + ChainCommunicationError, ChainResult, Checkpoint, ContractLocator, HyperlaneChain, + HyperlaneContract, HyperlaneDomain, HyperlaneProvider, MerkleTreeHook, ReorgPeriod, H256, +}; + +use crate::{ConnectionConf, CosmosNativeProvider, HyperlaneCosmosError, Signer}; + +#[derive(Debug, Clone)] +/// A reference to a MerkleTreeHook contract on some Cosmos chain +pub struct CosmosMerkleTreeHook { + /// Domain + domain: HyperlaneDomain, + /// Contract address + address: H256, + /// Provider + provider: CosmosNativeProvider, +} + +impl CosmosMerkleTreeHook { + /// create new Cosmos MerkleTreeHook agent + pub fn new( + conf: ConnectionConf, + locator: ContractLocator, + signer: Option, + ) -> ChainResult { + let provider = CosmosNativeProvider::new( + locator.domain.clone(), + conf.clone(), + locator.clone(), + signer, + )?; + + Ok(Self { + domain: locator.domain.clone(), + address: locator.address, + provider, + }) + } +} + +impl HyperlaneContract for CosmosMerkleTreeHook { + fn address(&self) -> H256 { + self.address + } +} + +impl HyperlaneChain for CosmosMerkleTreeHook { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +#[async_trait] +impl MerkleTreeHook for CosmosMerkleTreeHook { + /// Return the incremental merkle tree in storage + #[instrument(level = "debug", err, ret, skip(self))] + async fn tree(&self, reorg_period: &ReorgPeriod) -> ChainResult { + let mailbox = self + .provider + .rest() + .mailbox(self.address, reorg_period.clone()) + .await?; + + let branch = mailbox + .tree + .branch + .iter() + .map(|hash| { + let result = base64::prelude::BASE64_STANDARD.decode(hash); + match result { + Ok(vec) => Ok(H256::from_slice(&vec)), + Err(e) => Err(e), + } + }) + .filter_map(|hash| hash.ok()) + .collect_vec(); + + if branch.len() < mailbox.tree.branch.len() { + return Err(ChainCommunicationError::CustomError( + "Failed to parse incremental merkle tree".to_string(), + )); + } + let branch = branch.as_slice(); + let branch: [H256; 32] = match branch.try_into() { + Ok(ba) => ba, + Err(_) => { + return Err(ChainCommunicationError::CustomError( + "Failed to convert incremental tree. expected branch length of 32".to_string(), + )) + } + }; + Ok(IncrementalMerkle { + branch, + count: mailbox.tree.count, + }) + } + + /// Gets the current leaf count of the merkle tree + async fn count(&self, reorg_period: &ReorgPeriod) -> ChainResult { + self.provider + .rest() + .leaf_count(self.address, reorg_period.clone()) + .await + } + + #[instrument(level = "debug", err, ret, skip(self))] + async fn latest_checkpoint(&self, reorg_period: &ReorgPeriod) -> ChainResult { + let response = self + .provider + .rest() + .latest_checkpoint(self.address, reorg_period.clone()) + .await?; + let root = base64::prelude::BASE64_STANDARD + .decode(response.root) + .map_err(Into::::into)?; + let root = H256::from_slice(&root); + Ok(Checkpoint { + merkle_tree_hook_address: self.address, + mailbox_domain: self.domain.id(), + root, + index: response.count, + }) + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers.rs new file mode 100644 index 0000000000..0b651fe00d --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers.rs @@ -0,0 +1,9 @@ +mod cosmos; +mod grpc; +mod rest; + +pub use cosmos::{ + CosmosNativeProvider, MsgAnnounceValidator, MsgProcessMessage, MsgRemoteTransfer, +}; +pub use grpc::*; +pub use rest::*; diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs new file mode 100644 index 0000000000..41079317f6 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs @@ -0,0 +1,535 @@ +use std::io::Cursor; + +use cosmrs::{ + crypto::PublicKey, + proto::{cosmos::base::abci::v1beta1::TxResponse, tendermint::types::Block}, + tx::{SequenceNumber, SignerInfo, SignerPublicKey}, + AccountId, Any, Coin, Tx, +}; + +use super::{grpc::GrpcProvider, rest::RestProvider, CosmosFallbackProvider}; +use crate::{ + ConnectionConf, CosmosAccountId, CosmosAddress, CosmosAmount, HyperlaneCosmosError, Signer, +}; +use hyperlane_core::{ + h512_to_bytes, + rpc_clients::{BlockNumberGetter, FallbackProvider}, + utils::{self, to_atto}, + AccountAddressType, BlockInfo, ChainCommunicationError, ChainInfo, ChainResult, + ContractLocator, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, HyperlaneProviderError, + LogMeta, ModuleType, TxnInfo, TxnReceiptInfo, H256, H512, U256, +}; +use itertools::Itertools; +use prost::Message; +use reqwest::Error; +use serde::{de::DeserializeOwned, Deserialize}; +use tendermint::{hash::Algorithm, Hash}; +use tendermint_rpc::{ + client::CompatMode, + endpoint::{block, block_results, tx}, + Client, HttpClient, +}; +use time::OffsetDateTime; +use tonic::async_trait; +use tracing::{debug, trace, warn}; + +// proto structs for encoding and decoding transactions +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MsgProcessMessage { + #[prost(string, tag = "1")] + pub mailbox_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub relayer: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub metadata: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub message: ::prost::alloc::string::String, +} + +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MsgAnnounceValidator { + #[prost(string, tag = "1")] + pub validator: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub storage_location: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub signature: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub mailbox_id: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub creator: ::prost::alloc::string::String, +} + +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MsgRemoteTransfer { + #[prost(string, tag = "1")] + pub sender: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub token_id: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub recipient: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub amount: ::prost::alloc::string::String, +} + +#[derive(Debug, Clone)] +struct CosmosHttpClient { + client: HttpClient, +} + +#[derive(Debug, Clone)] +pub struct CosmosNativeProvider { + connection_conf: ConnectionConf, + provider: CosmosFallbackProvider, + grpc: GrpcProvider, + rest: RestProvider, + domain: HyperlaneDomain, +} + +impl CosmosNativeProvider { + #[doc = "Create a new Cosmos Provider instance"] + pub fn new( + domain: HyperlaneDomain, + conf: ConnectionConf, + locator: ContractLocator, + signer: Option, + ) -> ChainResult { + let clients = conf + .get_rpc_urls() + .iter() + .map(|url| { + tendermint_rpc::Url::try_from(url.to_owned()) + .map_err(ChainCommunicationError::from_other) + .and_then(|url| { + tendermint_rpc::HttpClientUrl::try_from(url) + .map_err(ChainCommunicationError::from_other) + }) + .and_then(|url| { + HttpClient::builder(url) + .compat_mode(CompatMode::latest()) + .build() + .map_err(ChainCommunicationError::from_other) + }) + .map(|client| CosmosHttpClient { client }) + }) + .collect::, _>>()?; + + let providers = FallbackProvider::new(clients); + let client = CosmosFallbackProvider::new(providers); + + let gas_price = CosmosAmount::try_from(conf.get_minimum_gas_price().clone())?; + let grpc_provider = GrpcProvider::new( + domain.clone(), + conf.clone(), + gas_price.clone(), + locator, + signer, + )?; + + let rest = RestProvider::new(conf.get_api_urls().iter().map(|url| url.to_string())); + + Ok(CosmosNativeProvider { + domain, + connection_conf: conf, + provider: client, + grpc: grpc_provider, + rest, + }) + } + + // extract the contract address from the tx + fn contract(tx: &Tx) -> ChainResult { + // check for all transfer messages + let remote_transfers: Vec = tx + .body + .messages + .iter() + .filter(|a| a.type_url == "/hyperlane.warp.v1.MsgRemoteTransfer") + .cloned() + .collect(); + + if remote_transfers.len() > 1 { + let msg = "transaction contains multiple execution messages"; + Err(HyperlaneCosmosError::ParsingFailed(msg.to_owned()))? + } + + let msg = &remote_transfers[0]; + let result = MsgRemoteTransfer::decode(Cursor::new(msg.value.to_vec())).map_err(|err| { + HyperlaneCosmosError::ParsingFailed(format!( + "Can't parse any to MsgRemoteTransfer. {msg:?}" + )) + })?; + + let recipient = result.recipient; + let recipient: H256 = recipient.parse()?; + Ok(recipient) + } + + fn search_payer_in_signer_infos( + &self, + signer_infos: &[SignerInfo], + payer: &AccountId, + ) -> ChainResult<(AccountId, SequenceNumber)> { + signer_infos + .iter() + .map(|si| self.convert_signer_info_into_account_id_and_nonce(si)) + // After the following we have a single Ok entry and, possibly, many Err entries + .filter_ok(|(a, s)| payer == a) + // If we have Ok entry, use it since it is the payer, if not, use the first entry with error + .find_or_first(|r| match r { + Ok((a, s)) => payer == a, + Err(e) => false, + }) + // If there were not any signer info with non-empty public key or no signers for the transaction, + // we get None here + .unwrap_or_else(|| Err(ChainCommunicationError::from_other_str("no signer info"))) + } + + fn convert_signer_info_into_account_id_and_nonce( + &self, + signer_info: &SignerInfo, + ) -> ChainResult<(AccountId, SequenceNumber)> { + let signer_public_key = signer_info.public_key.clone().ok_or_else(|| { + HyperlaneCosmosError::PublicKeyError("no public key for default signer".to_owned()) + })?; + + let (key, account_address_type) = self.normalize_public_key(signer_public_key)?; + let public_key = PublicKey::try_from(key)?; + + let account_id = CosmosAccountId::account_id_from_pubkey( + public_key, + &self.connection_conf.get_bech32_prefix(), + &account_address_type, + )?; + + Ok((account_id, signer_info.sequence)) + } + + fn normalize_public_key( + &self, + signer_public_key: SignerPublicKey, + ) -> ChainResult<(SignerPublicKey, AccountAddressType)> { + let public_key_and_account_address_type = match signer_public_key { + SignerPublicKey::Single(pk) => (SignerPublicKey::from(pk), AccountAddressType::Bitcoin), + SignerPublicKey::LegacyAminoMultisig(pk) => { + (SignerPublicKey::from(pk), AccountAddressType::Bitcoin) + } + SignerPublicKey::Any(pk) => { + if pk.type_url != PublicKey::ED25519_TYPE_URL + && pk.type_url != PublicKey::SECP256K1_TYPE_URL + { + let msg = format!( + "can only normalize public keys with a known TYPE_URL: {}, {}", + PublicKey::ED25519_TYPE_URL, + PublicKey::SECP256K1_TYPE_URL, + ); + warn!(pk.type_url, msg); + Err(HyperlaneCosmosError::PublicKeyError(msg.to_owned()))? + } + + let (pub_key, account_address_type) = + (PublicKey::try_from(pk)?, AccountAddressType::Bitcoin); + + (SignerPublicKey::Single(pub_key), account_address_type) + } + }; + + Ok(public_key_and_account_address_type) + } + + /// Calculates the sender and the nonce for the transaction. + /// We use `payer` of the fees as the sender of the transaction, and we search for `payer` + /// signature information to find the nonce. + /// If `payer` is not specified, we use the account which signed the transaction first, as + /// the sender. + pub fn sender_and_nonce(&self, tx: &Tx) -> ChainResult<(H256, SequenceNumber)> { + let (sender, nonce) = tx + .auth_info + .fee + .payer + .as_ref() + .map(|payer| self.search_payer_in_signer_infos(&tx.auth_info.signer_infos, payer)) + .map_or_else( + || { + #[allow(clippy::get_first)] // TODO: `rustc` 1.80.1 clippy issue + let signer_info = tx.auth_info.signer_infos.get(0).ok_or_else(|| { + HyperlaneCosmosError::SignerInfoError( + "no signer info in default signer".to_owned(), + ) + })?; + self.convert_signer_info_into_account_id_and_nonce(signer_info) + }, + |p| p, + ) + .map(|(a, n)| CosmosAddress::from_account_id(a).map(|a| (a.digest(), n)))??; + Ok((sender, nonce)) + } + + /// Reports if transaction contains fees expressed in unsupported denominations + /// The only denomination we support at the moment is the one we express gas minimum price + /// in the configuration of a chain. If fees contain an entry in a different denomination, + /// we report it in the logs. + fn report_unsupported_denominations(&self, tx: &Tx, tx_hash: &H256) -> ChainResult<()> { + let supported_denomination = self.connection_conf.get_minimum_gas_price().denom; + let unsupported_denominations = tx + .auth_info + .fee + .amount + .iter() + .filter(|c| c.denom.as_ref() != supported_denomination) + .map(|c| c.denom.as_ref()) + .fold("".to_string(), |acc, denom| acc + ", " + denom); + + if !unsupported_denominations.is_empty() { + let msg = "transaction contains fees in unsupported denominations, manual intervention is required"; + warn!( + ?tx_hash, + ?supported_denomination, + ?unsupported_denominations, + msg, + ); + Err(ChainCommunicationError::CustomError(msg.to_owned()))? + } + + Ok(()) + } + + /// Converts fees to a common denomination if necessary. + /// + /// If fees are expressed in an unsupported denomination, they will be ignored. + fn convert_fee(&self, coin: &Coin) -> ChainResult { + let native_token = self.connection_conf.get_native_token(); + + if coin.denom.as_ref() != native_token.denom { + return Ok(U256::zero()); + } + + let amount_in_native_denom = U256::from(coin.amount); + + to_atto(amount_in_native_denom, native_token.decimals).ok_or( + ChainCommunicationError::CustomError("Overflow in calculating fees".to_owned()), + ) + } + + fn calculate_gas_price(&self, hash: &H256, tx: &Tx) -> ChainResult { + // TODO support multiple denominations for amount + let supported = self.report_unsupported_denominations(tx, hash); + if supported.is_err() { + return Ok(U256::max_value()); + } + + let gas_limit = U256::from(tx.auth_info.fee.gas_limit); + let fee = tx + .auth_info + .fee + .amount + .iter() + .map(|c| self.convert_fee(c)) + .fold_ok(U256::zero(), |acc, v| acc + v)?; + + if fee < gas_limit { + warn!(tx_hash = ?hash, ?fee, ?gas_limit, "calculated fee is less than gas limit. it will result in zero gas price"); + } + + Ok(fee / gas_limit) + } + + pub fn grpc(&self) -> &GrpcProvider { + &self.grpc + } + + pub fn rest(&self) -> &RestProvider { + &self.rest + } + + pub async fn get_tx(&self, hash: &H512) -> ChainResult { + let hash: H256 = H256::from_slice(&h512_to_bytes(hash)); + + let tendermint_hash = Hash::from_bytes(Algorithm::Sha256, hash.as_bytes()) + .expect("transaction hash should be of correct size"); + + let response = + self.provider + .call(|client| { + let future = async move { + client.client.tx(tendermint_hash, false).await.map_err(|e| { + ChainCommunicationError::from(HyperlaneCosmosError::from(e)) + }) + }; + Box::pin(future) + }) + .await?; + + let received_hash = H256::from_slice(response.hash.as_bytes()); + if received_hash != hash { + return Err(ChainCommunicationError::from_other_str(&format!( + "received incorrect transaction, expected hash: {:?}, received hash: {:?}", + hash, received_hash, + ))); + } + + Ok(response) + } + + pub async fn get_block(&self, height: u32) -> ChainResult { + let response = + self.provider + .call(|client| { + let future = async move { + client.client.block(height).await.map_err(|e| { + ChainCommunicationError::from(HyperlaneCosmosError::from(e)) + }) + }; + Box::pin(future) + }) + .await?; + + Ok(response) + } + + pub async fn get_block_results(&self, height: u32) -> ChainResult { + let response = + self.provider + .call(|client| { + let future = async move { + client.client.block_results(height).await.map_err(|e| { + ChainCommunicationError::from(HyperlaneCosmosError::from(e)) + }) + }; + Box::pin(future) + }) + .await?; + + Ok(response) + } +} + +impl HyperlaneChain for CosmosNativeProvider { + #[doc = " Return the domain"] + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + #[doc = " A provider for the chain"] + fn provider(&self) -> Box { + Box::new(self.clone()) + } +} + +#[async_trait] +impl HyperlaneProvider for CosmosNativeProvider { + async fn get_block_by_height(&self, height: u64) -> ChainResult { + let response = + self.provider + .call(|client| { + let future = async move { + client.client.block(height as u32).await.map_err(|e| { + ChainCommunicationError::from(HyperlaneCosmosError::from(e)) + }) + }; + Box::pin(future) + }) + .await?; + + let block = response.block; + let block_height = block.header.height.value(); + + if block_height != height { + Err(HyperlaneProviderError::IncorrectBlockByHeight( + height, + block_height, + ))? + } + + let hash = H256::from_slice(response.block_id.hash.as_bytes()); + let time: OffsetDateTime = block.header.time.into(); + + let block_info = BlockInfo { + hash: hash.to_owned(), + timestamp: time.unix_timestamp() as u64, + number: block_height, + }; + + Ok(block_info) + } + + async fn get_txn_by_hash(&self, hash: &H512) -> ChainResult { + let hash: H256 = H256::from_slice(&h512_to_bytes(hash)); + + let tendermint_hash = Hash::from_bytes(Algorithm::Sha256, hash.as_bytes()) + .expect("transaction hash should be of correct size"); + + let response = + self.provider + .call(|client| { + let future = async move { + client.client.tx(tendermint_hash, false).await.map_err(|e| { + ChainCommunicationError::from(HyperlaneCosmosError::from(e)) + }) + }; + Box::pin(future) + }) + .await?; + + let received_hash = H256::from_slice(response.hash.as_bytes()); + + if received_hash != hash { + return Err(ChainCommunicationError::from_other_str(&format!( + "received incorrect transaction, expected hash: {:?}, received hash: {:?}", + hash, received_hash, + ))); + } + + let tx = Tx::from_bytes(&response.tx)?; + + let contract = Self::contract(&tx)?; + let (sender, nonce) = self.sender_and_nonce(&tx)?; + let gas_price = self.calculate_gas_price(&hash, &tx)?; + + let tx_info = TxnInfo { + hash: hash.into(), + gas_limit: U256::from(response.tx_result.gas_wanted), + max_priority_fee_per_gas: None, + max_fee_per_gas: None, + gas_price: Some(gas_price), + nonce, + sender, + recipient: Some(contract), + receipt: Some(TxnReceiptInfo { + gas_used: U256::from(response.tx_result.gas_used), + cumulative_gas_used: U256::from(response.tx_result.gas_used), + effective_gas_price: Some(gas_price), + }), + raw_input_data: None, + }; + + Ok(tx_info) + } + + async fn is_contract(&self, address: &H256) -> ChainResult { + // TODO: check if the address is a recipient + return Ok(true); + } + + async fn get_balance(&self, address: String) -> ChainResult { + self.grpc + .get_balance(address, self.connection_conf.get_canonical_asset()) + .await + } + + async fn get_chain_metrics(&self) -> ChainResult> { + return Ok(None); + } +} + +#[async_trait] +impl BlockNumberGetter for CosmosHttpClient { + async fn get_block_number(&self) -> Result { + let block = self + .client + .latest_block() + .await + .map_err(Into::::into)?; + Ok(block.block.header.height.value()) + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs new file mode 100644 index 0000000000..a5f9aa96d1 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs @@ -0,0 +1,419 @@ +use std::{ + fmt::{Debug, Formatter}, + ops::Deref, +}; + +use async_trait::async_trait; +use base64::Engine; +use cosmrs::{ + proto::{ + cosmos::{ + auth::v1beta1::{ + query_client::QueryClient as QueryAccountClient, BaseAccount, QueryAccountRequest, + }, + bank::v1beta1::{query_client::QueryClient as QueryBalanceClient, QueryBalanceRequest}, + base::{ + abci::v1beta1::TxResponse, + tendermint::v1beta1::{service_client::ServiceClient, GetLatestBlockRequest}, + }, + tx::v1beta1::{ + service_client::ServiceClient as TxServiceClient, BroadcastMode, + BroadcastTxRequest, SimulateRequest, TxRaw, + }, + }, + cosmwasm::wasm::v1::{ + query_client::QueryClient as WasmQueryClient, ContractInfo, MsgExecuteContract, + QueryContractInfoRequest, QuerySmartContractStateRequest, + }, + prost::{self, Message}, + }, + tx::{self, Fee, MessageExt, SignDoc, SignerInfo}, + Any, Coin, +}; +use derive_new::new; +use protobuf::Message as _; +use serde::Serialize; +use tonic::{ + transport::{Channel, Endpoint}, + GrpcMethod, IntoRequest, +}; +use tracing::{debug, instrument}; +use url::Url; + +use hyperlane_core::{ + rpc_clients::{BlockNumberGetter, FallbackProvider}, + ChainCommunicationError, ChainResult, ContractLocator, FixedPointNumber, HyperlaneDomain, U256, +}; + +use crate::CosmosAmount; +use crate::HyperlaneCosmosError; +use crate::{ConnectionConf, CosmosAddress, Signer}; + +/// A multiplier applied to a simulated transaction's gas usage to +/// calculate the estimated gas. +const GAS_ESTIMATE_MULTIPLIER: f64 = 2.0; // TODO: this has to be adjusted accordingly and per chain +/// The number of blocks in the future in which a transaction will +/// be valid for. +const TIMEOUT_BLOCKS: u64 = 1000; + +#[derive(new, Clone)] +pub struct CosmosFallbackProvider { + fallback_provider: FallbackProvider, +} + +impl Deref for CosmosFallbackProvider { + type Target = FallbackProvider; + + fn deref(&self) -> &Self::Target { + &self.fallback_provider + } +} + +impl Debug for CosmosFallbackProvider +where + C: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.fallback_provider.fmt(f) + } +} + +#[derive(Debug, Clone, new)] +struct CosmosChannel { + channel: Channel, + /// The url that this channel is connected to. + /// Not explicitly used, but useful for debugging. + _url: Url, +} + +#[async_trait] +impl BlockNumberGetter for CosmosChannel { + async fn get_block_number(&self) -> Result { + let mut client = ServiceClient::new(self.channel.clone()); + let request = tonic::Request::new(GetLatestBlockRequest {}); + + let response = client + .get_latest_block(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + let height = response + .block + .ok_or_else(|| ChainCommunicationError::from_other_str("block not present"))? + .header + .ok_or_else(|| ChainCommunicationError::from_other_str("header not present"))? + .height; + + Ok(height as u64) + } +} + +#[derive(Debug, Clone)] +/// CosmWasm GRPC provider. +pub struct GrpcProvider { + /// Connection configuration. + conf: ConnectionConf, + /// Signer for transactions. + signer: Option, + /// GRPC Channel that can be cheaply cloned. + /// See `` + provider: CosmosFallbackProvider, + gas_price: CosmosAmount, +} + +impl GrpcProvider { + /// Create new CosmWasm GRPC Provider. + pub fn new( + domain: HyperlaneDomain, + conf: ConnectionConf, + gas_price: CosmosAmount, + locator: ContractLocator, + signer: Option, + ) -> ChainResult { + // get all the configured grpc urls and convert them to a Vec + let channels: Result, _> = conf + .get_grpc_urls() + .into_iter() + .map(|url| { + Endpoint::new(url.to_string()) + .map(|e| CosmosChannel::new(e.connect_lazy(), url)) + .map_err(Into::::into) + }) + .collect(); + let mut builder = FallbackProvider::builder(); + builder = builder.add_providers(channels?); + let fallback_provider = builder.build(); + let provider = CosmosFallbackProvider::new(fallback_provider); + + let contract_address = CosmosAddress::from_h256( + locator.address, + &conf.get_bech32_prefix(), + conf.get_contract_address_bytes(), + )?; + + Ok(Self { + conf, + signer, + provider, + gas_price, + }) + } + + /// Gets a signer, or returns an error if one is not available. + fn get_signer(&self) -> ChainResult<&Signer> { + self.signer + .as_ref() + .ok_or(ChainCommunicationError::SignerUnavailable) + } + + /// Get the gas price + pub fn gas_price(&self) -> FixedPointNumber { + self.gas_price.amount.clone() + } + + /// Generates an unsigned SignDoc for a transaction and the Coin amount + /// required to pay for tx fees. + async fn generate_unsigned_sign_doc_and_fee( + &self, + msgs: Vec, + gas_limit: u64, + ) -> ChainResult<(SignDoc, Coin)> { + // As this function is only used for estimating gas or sending transactions, + // we can reasonably expect to have a signer. + let signer = self.get_signer()?; + let account_info = self.account_query(signer.address.clone()).await?; + let current_height = self.latest_block_height().await?; + let timeout_height = current_height + TIMEOUT_BLOCKS; + + let tx_body = tx::Body::new( + msgs, + String::default(), + TryInto::::try_into(timeout_height) + .map_err(ChainCommunicationError::from_other)?, + ); + let signer_info = SignerInfo::single_direct(Some(signer.public_key), account_info.sequence); + + let amount: u128 = (FixedPointNumber::from(gas_limit) * self.gas_price()) + .ceil_to_integer() + .try_into()?; + let fee_coin = Coin::new( + // The fee to pay is the gas limit * the gas price + amount, + self.conf.get_canonical_asset().as_str(), + ) + .map_err(Into::::into)?; + let auth_info = + signer_info.auth_info(Fee::from_amount_and_gas(fee_coin.clone(), gas_limit)); + + let chain_id = self + .conf + .get_chain_id() + .parse() + .map_err(Into::::into)?; + + Ok(( + SignDoc::new(&tx_body, &auth_info, &chain_id, account_info.account_number) + .map_err(Into::::into)?, + fee_coin, + )) + } + + /// Generates a raw signed transaction including `msgs`, estimating gas if a limit is not provided, + /// and the Coin amount required to pay for tx fees. + async fn generate_raw_signed_tx_and_fee( + &self, + msgs: Vec, + gas_limit: Option, + ) -> ChainResult<(Vec, Coin)> { + let gas_limit = if let Some(l) = gas_limit { + l + } else { + self.estimate_gas(msgs.clone()).await? + }; + + let (sign_doc, fee) = self + .generate_unsigned_sign_doc_and_fee(msgs, gas_limit) + .await?; + + let signer = self.get_signer()?; + let tx_signed = sign_doc + .sign(&signer.signing_key()?) + .map_err(Into::::into)?; + Ok(( + tx_signed + .to_bytes() + .map_err(Into::::into)?, + fee, + )) + } + + /// send a transaction + pub async fn send( + &self, + msgs: Vec, + gas_limit: Option, + ) -> ChainResult { + let gas_limit = if let Some(l) = gas_limit { + l + } else { + self.estimate_gas(msgs.clone()).await? + }; + + let (tx_bytes, _) = self + .generate_raw_signed_tx_and_fee(msgs, Some(gas_limit)) + .await?; + let tx_response = self + .provider + .call(move |provider| { + let tx_bytes_clone = tx_bytes.clone(); + let future = async move { + let mut client = TxServiceClient::new(provider.channel.clone()); + let request = tonic::Request::new(BroadcastTxRequest { + tx_bytes: tx_bytes_clone, + mode: BroadcastMode::Sync as i32, + }); + + let tx_response = client + .broadcast_tx(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner() + .tx_response + .ok_or_else(|| { + ChainCommunicationError::from_other_str("tx_response not present") + })?; + Ok(tx_response) + }; + Box::pin(future) + }) + .await?; + Ok(tx_response) + } + + /// Estimates gas for a transaction containing `msgs`. + pub async fn estimate_gas(&self, msgs: Vec) -> ChainResult { + // Get a sign doc with 0 gas, because we plan to simulate + let (sign_doc, _) = self.generate_unsigned_sign_doc_and_fee(msgs, 0).await?; + + let raw_tx = TxRaw { + body_bytes: sign_doc.body_bytes, + auth_info_bytes: sign_doc.auth_info_bytes, + // The poorly documented trick to simulating a tx without a valid signature is to just pass + // in a single empty signature. Taken from cosmjs: + // https://github.com/cosmos/cosmjs/blob/44893af824f0712d1f406a8daa9fcae335422235/packages/stargate/src/modules/tx/queries.ts#L67 + signatures: vec![vec![]], + }; + let tx_bytes = raw_tx + .to_bytes() + .map_err(ChainCommunicationError::from_other)?; + let gas_used = self + .provider + .call(move |provider| { + let tx_bytes_clone = tx_bytes.clone(); + let future = async move { + let mut client = TxServiceClient::new(provider.channel.clone()); + #[allow(deprecated)] + let sim_req = tonic::Request::new(SimulateRequest { + tx: None, + tx_bytes: tx_bytes_clone, + }); + let gas_used = client + .simulate(sim_req) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner() + .gas_info + .ok_or_else(|| { + ChainCommunicationError::from_other_str("gas info not present") + })? + .gas_used; + + Ok(gas_used) + }; + Box::pin(future) + }) + .await?; + + let gas_estimate = (gas_used as f64 * GAS_ESTIMATE_MULTIPLIER) as u64; + + Ok(gas_estimate) + } + + /// Fetches balance for a given `address` and `denom` + pub async fn get_balance(&self, address: String, denom: String) -> ChainResult { + let response = self + .provider + .call(move |provider| { + let address = address.clone(); + let denom = denom.clone(); + let future = async move { + let mut client = QueryBalanceClient::new(provider.channel.clone()); + let balance_request = + tonic::Request::new(QueryBalanceRequest { address, denom }); + let response = client + .balance(balance_request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + Ok(response) + }; + Box::pin(future) + }) + .await?; + + let balance = response + .balance + .ok_or_else(|| ChainCommunicationError::from_other_str("account not present"))?; + + Ok(U256::from_dec_str(&balance.amount)?) + } + + /// Queries an account. + pub async fn account_query(&self, account: String) -> ChainResult { + let response = self + .provider + .call(move |provider| { + let address = account.clone(); + let future = async move { + let mut client = QueryAccountClient::new(provider.channel.clone()); + let request = tonic::Request::new(QueryAccountRequest { address }); + let response = client + .account(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + Ok(response) + }; + Box::pin(future) + }) + .await?; + + let account = BaseAccount::decode( + response + .account + .ok_or_else(|| ChainCommunicationError::from_other_str("account not present"))? + .value + .as_slice(), + ) + .map_err(Into::::into)?; + Ok(account) + } + + async fn latest_block_height(&self) -> ChainResult { + let height = self + .provider + .call(move |provider| { + let future = async move { provider.get_block_number().await }; + Box::pin(future) + }) + .await?; + Ok(height) + } +} + +#[async_trait] +impl BlockNumberGetter for GrpcProvider { + async fn get_block_number(&self) -> Result { + self.latest_block_height().await + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/rest.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/rest.rs new file mode 100644 index 0000000000..fe6d4a399f --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers/rest.rs @@ -0,0 +1,330 @@ +use std::cmp::max; + +use hyperlane_core::{ + rpc_clients::{BlockNumberGetter, FallbackProvider}, + utils, ChainCommunicationError, ChainResult, ReorgPeriod, H160, H256, +}; +use reqwest::Error; +use serde::{de::DeserializeOwned, Deserialize, Deserializer}; +use tonic::async_trait; + +use crate::HyperlaneCosmosError; + +use super::CosmosFallbackProvider; + +#[derive(Debug, Clone)] +struct RestClient { + url: String, +} + +#[derive(Debug, Clone)] +pub struct RestProvider { + clients: CosmosFallbackProvider, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct IncrementalTree { + pub branch: Vec, // base64 encoded + pub count: usize, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct Mailbox { + pub id: String, + pub creator: String, + pub message_sent: usize, + pub message_received: usize, + pub default_ism: String, + pub tree: IncrementalTree, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct MultiSig { + pub validator_pub_keys: Vec, + pub threshold: usize, +} + +#[derive(serde::Deserialize, Clone, Debug)] +#[serde(untagged)] // this is needed because the ISM can be either NoOpISM or MultiSigISM +pub enum ISM { + MultiSigISM { + id: String, + creator: String, + ism_type: usize, + multi_sig: MultiSig, + }, + NoOpISM { + id: String, + ism_type: usize, + creator: String, + }, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct MailboxesResponse { + mailboxes: Vec, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct MailboxResponse { + mailbox: Mailbox, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct ISMResponse { + isms: Vec, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct WarpRoute { + pub id: String, + pub creator: String, + pub token_type: String, + pub origin_mailbox: String, + pub origin_denom: String, + pub receiver_domain: usize, + pub receiver_contract: String, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct WarpRoutesResponse { + pub tokens: Vec, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct CountResponse { + pub count: u32, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct DeliveredResponse { + pub delivered: bool, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct NodeStatus { + #[serde(deserialize_with = "string_to_number")] + pub earliest_store_height: usize, + #[serde(deserialize_with = "string_to_number")] + pub height: usize, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct RecipientIsmResponse { + ism_id: String, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct LatestCheckpointResponse { + pub root: String, // encoded base64 string + pub count: u32, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct ValidatorStorageLocationsResponse { + storage_locations: Vec, +} + +#[async_trait] +impl BlockNumberGetter for RestClient { + async fn get_block_number(&self) -> Result { + let url = self.url.to_owned() + "cosmos/base/node/v1beta1/status"; + let response = reqwest::get(url.clone()) + .await + .map_err(Into::::into)?; + let result: Result = response.json().await; + match result { + Ok(result) => Ok(result.height as u64), + Err(err) => Err(HyperlaneCosmosError::ParsingFailed(format!( + "Failed to parse response for: {:?} {:?}", + url, err + )) + .into()), + } + } +} + +impl RestProvider { + #[doc = "todo"] + pub fn new(urls: impl IntoIterator) -> RestProvider { + let provider = FallbackProvider::new(urls.into_iter().map(|url| RestClient { url })); + RestProvider { + clients: CosmosFallbackProvider::new(provider), + } + } + + async fn get(&self, path: &str, reorg_period: ReorgPeriod) -> ChainResult + where + T: DeserializeOwned, + { + self.clients + .call(move |client| { + let reorg_period = reorg_period.clone(); + let path = path.to_owned(); + let future = async move { + let final_url = client.url.to_string() + "hyperlane/" + &path; + let request = reqwest::Client::new(); + let response = match reorg_period { + ReorgPeriod::None => request + .get(final_url.clone()) + .send() + .await + .map_err(Into::::into)?, + ReorgPeriod::Blocks(non_zero) => { + let remote_height = client.get_block_number().await?; + if non_zero.get() as u64 > remote_height { + return Err(ChainCommunicationError::InvalidRequest { + msg: "reorg period can not be greater than block height." + .to_string(), + }); + } + let delta = remote_height - non_zero.get() as u64; + request + .get(final_url.clone()) + .header("x-cosmos-block-height", delta) + .send() + .await + .map_err(Into::::into)? + } + ReorgPeriod::Tag(_) => todo!(), + }; + + let result: Result = response.json().await; + match result { + Ok(result) => Ok(result), + Err(err) => Err(HyperlaneCosmosError::ParsingFailed(format!( + "Failed to parse response for: {:?} {:?}", + final_url, err + )) + .into()), + } + }; + Box::pin(future) + }) + .await + } + + /// list of all mailboxes deployed + pub async fn mailboxes(&self, reorg_period: ReorgPeriod) -> ChainResult> { + let mailboxes: MailboxesResponse = self.get("mailbox/v1/mailboxes", reorg_period).await?; + Ok(mailboxes.mailboxes) + } + + /// list of all mailboxes deployed + pub async fn mailbox(&self, id: H256, reorg_period: ReorgPeriod) -> ChainResult { + let mailboxes: MailboxResponse = self + .get(&format!("mailbox/v1/mailboxes/{id:?}"), reorg_period) + .await?; + Ok(mailboxes.mailbox) + } + + /// list of all isms + pub async fn isms(&self, reorg_period: ReorgPeriod) -> ChainResult> { + let isms: ISMResponse = self.get("ism/v1/isms", reorg_period).await?; + Ok(isms.isms) + } + + /// list of all warp routes + pub async fn warp_tokens(&self, reorg_period: ReorgPeriod) -> ChainResult> { + let warp: WarpRoutesResponse = self.get("warp/v1/tokens", reorg_period).await?; + Ok(warp.tokens) + } + + /// returns the current leaf count for mailbox + pub async fn leaf_count(&self, mailbox: H256, reorg_period: ReorgPeriod) -> ChainResult { + let leafs: CountResponse = self + .get(&format!("mailbox/v1/tree/count/{mailbox:?}"), reorg_period) + .await?; + Ok(leafs.count) + } + + /// returns the current leaf count for mailbox + pub async fn leaf_count_at_height(&self, mailbox: H256, height: u32) -> ChainResult { + self.clients + .call(move |client| { + let mailbox = mailbox.clone(); + let future = async move { + let final_url = + &format!("{}/hyperlane/mailbox/v1/tree/count/{mailbox:?}", client.url); + let client = reqwest::Client::new(); + let response = client + .get(final_url.clone()) + .header("x-cosmos-block-height", height) + .send() + .await + .map_err(Into::::into)?; + + let result: Result = response.json().await; + match result { + Ok(result) => Ok(result.count), + Err(err) => Err(HyperlaneCosmosError::ParsingFailed(format!( + "Failed to parse response for: {:?} {:?} height:{height}", + final_url, err + )) + .into()), + } + }; + Box::pin(future) + }) + .await + } + + /// returns if the message id has been delivered + pub async fn delivered(&self, message_id: H256) -> ChainResult { + let response: DeliveredResponse = self + .get( + &format!("mailbox/v1/delivered/{message_id:?}"), + ReorgPeriod::None, + ) + .await?; + Ok(response.delivered) + } + + /// returns the latest checkpoint + pub async fn latest_checkpoint( + &self, + mailbox: H256, + height: ReorgPeriod, + ) -> ChainResult { + let response: LatestCheckpointResponse = self + .get( + &format!("mailbox/v1/tree/latest_checkpoint/{mailbox:?}"), + height, + ) + .await?; + Ok(response) + } + + /// returns the recipient ism + pub async fn recipient_ism(&self, recipient: H256) -> ChainResult { + let response: RecipientIsmResponse = self + .get( + &format!("mailbox/v1/recipient_ism/{recipient:?}"), + ReorgPeriod::None, + ) + .await?; + utils::hex_or_base58_to_h256(&response.ism_id).map_err(|e| { + HyperlaneCosmosError::AddressError("invalid recipient ism address".to_string()).into() + }) + } + + /// mailbox/v1/announced_storage_locations/{validator_address} + pub async fn validator_storage_locations(&self, validator: H256) -> ChainResult> { + let validator = H160::from(validator); + let response: ValidatorStorageLocationsResponse = self + .get( + &format!("mailbox/v1/announced_storage_locations/{validator:?}"), + ReorgPeriod::None, + ) + .await?; + Ok(response.storage_locations) + } +} + +fn string_to_number<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + s.parse::().map_err(serde::de::Error::custom) +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/signers.rs b/rust/main/chains/hyperlane-cosmos-native/src/signers.rs new file mode 100644 index 0000000000..1cd0dd8ec4 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/signers.rs @@ -0,0 +1,53 @@ +use cosmrs::crypto::{secp256k1::SigningKey, PublicKey}; +use hyperlane_core::{AccountAddressType, ChainResult}; + +use crate::{CosmosAddress, HyperlaneCosmosError}; + +#[derive(Clone, Debug)] +/// Signer for cosmos chain +pub struct Signer { + /// public key + pub public_key: PublicKey, + /// precomputed address, because computing it is a fallible operation + /// and we want to avoid returning `Result` + pub address: String, + /// address prefix + pub prefix: String, + private_key: Vec, +} + +impl Signer { + /// create new signer + /// + /// # Arguments + /// * `private_key` - private key for signer + /// * `prefix` - prefix for signer address + /// * `account_address_type` - the type of account address used for signer + pub fn new( + private_key: Vec, + prefix: String, + account_address_type: &AccountAddressType, + ) -> ChainResult { + let address = + CosmosAddress::from_privkey(&private_key, &prefix, account_address_type)?.address(); + let signing_key = Self::build_signing_key(&private_key)?; + let public_key = signing_key.public_key(); + Ok(Self { + public_key, + private_key, + address, + prefix, + }) + } + + /// Build a SigningKey from a private key. This cannot be + /// precompiled and stored in `Signer`, because `SigningKey` is not `Sync`. + pub fn signing_key(&self) -> ChainResult { + Self::build_signing_key(&self.private_key) + } + + fn build_signing_key(private_key: &Vec) -> ChainResult { + Ok(SigningKey::from_slice(private_key.as_slice()) + .map_err(Into::::into)?) + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs b/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs new file mode 100644 index 0000000000..4e4da175b3 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs @@ -0,0 +1,160 @@ +use std::str::FromStr; + +use derive_new::new; +use url::Url; + +use hyperlane_core::{ + config::OperationBatchConfig, ChainCommunicationError, FixedPointNumber, NativeToken, +}; + +/// Cosmos connection configuration +#[derive(Debug, Clone)] +pub struct ConnectionConf { + /// API urls to connect to + api_urls: Vec, + /// The GRPC urls to connect to + grpc_urls: Vec, + /// The RPC url to connect to + rpc_urls: Vec, + /// The chain ID + chain_id: String, + /// The human readable address prefix for the chains using bech32. + bech32_prefix: String, + /// Canonical Assets Denom + canonical_asset: String, + /// The gas price set by the cosmos-sdk validator. Note that this represents the + /// minimum price set by the validator. + /// More details here: https://docs.cosmos.network/main/learn/beginner/gas-fees#antehandler + gas_price: RawCosmosAmount, + /// The number of bytes used to represent a contract address. + /// Cosmos address lengths are sometimes less than 32 bytes, so this helps to serialize it in + /// bech32 with the appropriate length. + contract_address_bytes: usize, + /// Operation batching configuration + pub operation_batch: OperationBatchConfig, + /// Native Token + native_token: NativeToken, +} + +/// Untyped cosmos amount +#[derive(serde::Serialize, serde::Deserialize, new, Clone, Debug)] +pub struct RawCosmosAmount { + /// Coin denom (e.g. `untrn`) + pub denom: String, + /// Amount in the given denom + pub amount: String, +} + +/// Typed cosmos amount +#[derive(Clone, Debug)] +pub struct CosmosAmount { + /// Coin denom (e.g. `untrn`) + pub denom: String, + /// Amount in the given denom + pub amount: FixedPointNumber, +} + +impl TryFrom for CosmosAmount { + type Error = ChainCommunicationError; + fn try_from(raw: RawCosmosAmount) -> Result { + Ok(Self { + denom: raw.denom, + amount: FixedPointNumber::from_str(&raw.amount)?, + }) + } +} + +/// An error type when parsing a connection configuration. +#[derive(thiserror::Error, Debug)] +pub enum ConnectionConfError { + /// Missing `rpc_url` for connection configuration + #[error("Missing `rpc_url` for connection configuration")] + MissingConnectionRpcUrl, + /// Missing `grpc_url` for connection configuration + #[error("Missing `grpc_url` for connection configuration")] + MissingConnectionGrpcUrl, + /// Missing `chainId` for connection configuration + #[error("Missing `chainId` for connection configuration")] + MissingChainId, + /// Missing `prefix` for connection configuration + #[error("Missing `prefix` for connection configuration")] + MissingPrefix, + /// Invalid `url` for connection configuration + #[error("Invalid `url` for connection configuration: `{0}` ({1})")] + InvalidConnectionUrl(String, url::ParseError), +} + +impl ConnectionConf { + /// Get the GRPC url + pub fn get_grpc_urls(&self) -> Vec { + self.grpc_urls.clone() + } + + /// Get the RPC urls + pub fn get_rpc_urls(&self) -> Vec { + self.rpc_urls.clone() + } + + /// Get the chain ID + pub fn get_chain_id(&self) -> String { + self.chain_id.clone() + } + + /// Get the bech32 prefix + pub fn get_bech32_prefix(&self) -> String { + self.bech32_prefix.clone() + } + + /// Get the asset + pub fn get_canonical_asset(&self) -> String { + self.canonical_asset.clone() + } + + /// Get the minimum gas price + pub fn get_minimum_gas_price(&self) -> RawCosmosAmount { + self.gas_price.clone() + } + + /// Get the native token + pub fn get_native_token(&self) -> &NativeToken { + &self.native_token + } + + /// Get the number of bytes used to represent a contract address + pub fn get_contract_address_bytes(&self) -> usize { + self.contract_address_bytes + } + + /// Get api urls + pub fn get_api_urls(&self) -> Vec { + self.api_urls.clone() + } + + /// Create a new connection configuration + #[allow(clippy::too_many_arguments)] + pub fn new( + grpc_urls: Vec, + rpc_urls: Vec, + api_urls: Vec, + chain_id: String, + bech32_prefix: String, + canonical_asset: String, + minimum_gas_price: RawCosmosAmount, + contract_address_bytes: usize, + operation_batch: OperationBatchConfig, + native_token: NativeToken, + ) -> Self { + Self { + api_urls, + grpc_urls, + rpc_urls, + chain_id, + bech32_prefix, + canonical_asset, + gas_price: minimum_gas_price, + contract_address_bytes, + operation_batch, + native_token, + } + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs b/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs new file mode 100644 index 0000000000..3ca3f3664f --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs @@ -0,0 +1,118 @@ +use std::hash::Hash; + +use async_trait::async_trait; + +use cosmrs::{proto::cosmos::base::abci::v1beta1::TxResponse, Any}; +use hyperlane_core::{ + Announcement, ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, + HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Signable, SignedType, TxOutcome, + ValidatorAnnounce, H160, H256, U256, +}; +use prost::Message; + +use crate::{signers::Signer, ConnectionConf, CosmosNativeProvider, MsgAnnounceValidator}; + +/// A reference to a ValidatorAnnounce contract on some Cosmos chain +#[derive(Debug)] +pub struct CosmosNativeValidatorAnnounce { + domain: HyperlaneDomain, + address: H256, + provider: CosmosNativeProvider, + signer: Option, +} + +impl CosmosNativeValidatorAnnounce { + /// create a new instance of CosmosValidatorAnnounce + pub fn new( + conf: ConnectionConf, + locator: ContractLocator, + signer: Option, + ) -> ChainResult { + let provider = CosmosNativeProvider::new( + locator.domain.clone(), + conf.clone(), + locator.clone(), + signer.clone(), + )?; + + Ok(Self { + domain: locator.domain.clone(), + address: locator.address, + provider, + signer, + }) + } +} + +impl HyperlaneContract for CosmosNativeValidatorAnnounce { + fn address(&self) -> H256 { + self.address + } +} + +impl HyperlaneChain for CosmosNativeValidatorAnnounce { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +#[async_trait] +impl ValidatorAnnounce for CosmosNativeValidatorAnnounce { + async fn get_announced_storage_locations( + &self, + validators: &[H256], + ) -> ChainResult>> { + let mut validator_locations = vec![]; + for validator in validators { + let locations = self + .provider + .rest() + .validator_storage_locations(validator.clone()) + .await; + if let Ok(locations) = locations { + validator_locations.push(locations); + } else { + validator_locations.push(vec![]) + } + } + Ok(validator_locations) + } + + async fn announce(&self, announcement: SignedType) -> ChainResult { + let signer = self + .signer + .as_ref() + .map_or("".to_string(), |signer| signer.address.clone()); + let announce = MsgAnnounceValidator { + validator: hex::encode(announcement.value.validator.as_bytes()), + storage_location: announcement.value.storage_location.clone(), + signature: hex::encode(announcement.signature.to_vec()), + mailbox_id: hex::encode(announcement.value.mailbox_address.clone()), + creator: signer, + }; + + let any_msg = Any { + type_url: "/hyperlane.mailbox.v1.MsgAnnounceValidator".to_string(), + value: announce.encode_to_vec(), + }; + + let response = self.provider.grpc().send(vec![any_msg], None).await; + let response = response?; + Ok(TxOutcome { + transaction_id: H256::from_slice(hex::decode(response.txhash)?.as_slice()).into(), + executed: response.code == 0, + gas_used: U256::from(response.gas_used), + gas_price: U256::one().try_into()?, + }) + } + + async fn announce_tokens_needed(&self, announcement: SignedType) -> Option { + // TODO: check user balance. For now, just try announcing and + // allow the announce attempt to fail if there are not enough tokens. + Some(0u64.into()) + } +} diff --git a/rust/main/config/testnet_config.json b/rust/main/config/testnet_config.json index e5e01dcaf8..f0643f718d 100644 --- a/rust/main/config/testnet_config.json +++ b/rust/main/config/testnet_config.json @@ -1513,7 +1513,7 @@ "protocol": "ethereum", "rpcUrls": [ { - "http": "http://127.0.0.1:8545" + "http": "http://0.0.0.0:8545" } ], "aggregationHook": "0x7F54A0734c5B443E5B04cc26B54bb8ecE0455785", @@ -2164,6 +2164,267 @@ "index": { "from": 86008 } + }, + "cosmostestnative1": { + "bech32Prefix": "kyve", + "blockExplorers": [], + "blocks": { + "confirmations": 2, + "estimateBlockTime": 7, + "reorgPeriod": 5 + }, + "canonicalAsset": "tkyve", + "chainId": "kyve-local", + "contractAddressBytes": 20, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Cosmos Native 1", + "domainId": 75898671, + "gasCurrencyCoinGeckoId": "kyve-network", + "gasPrice": { + "amount": "0.02", + "denom": "tkyve" + }, + "grpcUrls": [ + { + "http": "http://127.0.0.1:9090" + } + ], + "interchainGasPaymaster": "0x5b97fe8b2b8b2ef118e6540ce2ee38b68ee9d1250d43ac53febef300300c345c", + "mailbox": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", + "merkleTreeHook": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", + "name": "cosmostestnative1", + "nativeToken": { + "decimals": 6, + "denom": "tkyve", + "name": "KYVE", + "symbol": "KYVE" + }, + "index": { + "from": 2, + "chunk": 10 + }, + "protocol": "cosmosnative", + "apiUrls": [ + { + "http": "http://127.0.0.1:1317" + } + ], + "rpcUrls": [ + { + "http": "http://127.0.0.1:26657" + } + ], + "slip44": 118, + "validatorAnnounce": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", + "technicalStack": "other", + "signer": { + "key": "0x33913dd43a5d5764f7a23da212a8664fc4f5eedc68db35f3eb4a5c4f046b5b51", + "prefix": "kyve", + "type": "cosmosKey" + }, + "isTestnet": true + }, + "cosmostestnative2": { + "bech32Prefix": "kyve", + "blockExplorers": [], + "blocks": { + "confirmations": 2, + "estimateBlockTime": 7, + "reorgPeriod": 5 + }, + "canonicalAsset": "tkyve", + "chainId": "kyve-local", + "contractAddressBytes": 20, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "Cosmos Native 2", + "domainId": 75898670, + "gasCurrencyCoinGeckoId": "kyve-network", + "gasPrice": { + "amount": "0.02", + "denom": "tkyve" + }, + "grpcUrls": [ + { + "http": "http://127.0.0.1:9090" + } + ], + "interchainGasPaymaster": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", + "mailbox": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", + "merkleTreeHook": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", + "name": "cosmostestnative2", + "nativeToken": { + "decimals": 6, + "denom": "tkyve", + "name": "KYVE", + "symbol": "KYVE" + }, + "protocol": "cosmosnative", + "apiUrls": [ + { + "http": "http://127.0.0.1:1317" + } + ], + "rpcUrls": [ + { + "http": "http://127.0.0.1:26657" + } + ], + "index": { + "from": 2, + "chunk": 10 + }, + "slip44": 118, + "validatorAnnounce": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", + "technicalStack": "other", + "signer": { + "key": "0x33913dd43a5d5764f7a23da212a8664fc4f5eedc68db35f3eb4a5c4f046b5b51", + "prefix": "kyve", + "type": "cosmosKey" + }, + "isTestnet": true + }, + "kyvealpha": { + "bech32Prefix": "kyve", + "blockExplorers": [], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 5, + "reorgPeriod": 1 + }, + "canonicalAsset": "ukyve", + "chainId": "kyve-alpha", + "contractAddressBytes": 20, + "deployer": { + "name": "Abacus Works", + "url": "https://www.hyperlane.xyz" + }, + "displayName": "KYVEAlpha", + "domainId": 75898669, + "gasCurrencyCoinGeckoId": "kyve-network", + "gasPrice": { + "amount": "0.02", + "denom": "ukyve" + }, + "grpcUrls": [ + { + "http": "https://grpc.alpha.kyve.network" + } + ], + "interchainGasPaymaster": "0x5b97fe8b2b8b2ef118e6540ce2ee38b68ee9d1250d43ac53febef300300c345c", + "mailbox": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", + "merkleTreeHook": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", + "name": "kyvealpha", + "nativeToken": { + "decimals": 6, + "denom": "ukyve", + "name": "KYVE", + "symbol": "KYVE" + }, + "protocol": "cosmosnative", + "apiUrls": [ + { + "http": "http://3.75.39.208:1318" + } + ], + "rpcUrls": [ + { + "http": "http://3.75.39.208:26670" + } + ], + "index": { + "from": 9525962 + }, + "slip44": 118, + "validatorAnnounce": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", + "technicalStack": "other", + "signer": { + "key": "0x33913dd43a5d5764f7a23da212a8664fc4f5eedc68db35f3eb4a5c4f046b5b51", + "prefix": "kyve", + "type": "cosmosKey" + }, + "isTestnet": true + }, + "moonbasealpha": { + "blockExplorers": [ + { + "apiUrl": "https://api-moonbase.moonscan.io/api", + "family": "etherscan", + "name": "MoonScan", + "url": "https://moonbase.moonscan.io" + } + ], + "blocks": { + "confirmations": 1, + "estimateBlockTime": 12, + "reorgPeriod": 1 + }, + "chainId": 1287, + "displayName": "Moonbase Alpha", + "displayNameShort": "Moonbase", + "domainId": 1287, + "isTestnet": true, + "index": { + "from": 10463874, + "chunk": 999 + }, + "name": "moonbasealpha", + "nativeToken": { + "decimals": 18, + "name": "DEV", + "symbol": "DEV" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "https://rpc.api.moonbase.moonbeam.network" + } + ], + "merkleTreeHook": "0xF43AF3c413ba00f3BD933aa3061FA8db86e5b057", + "validatorAnnounce": "0xDbe1f8D6DD161d1309247665E50a742949d419c1", + "interchainGasPaymaster": "0x046fBeDf08f313FF1134069F45c0991dd730b25a", + "mailbox": "0x046fBeDf08f313FF1134069F45c0991dd730b25a" + }, + "local1": { + "chainId": 31337, + "displayName": "Local1", + "domainId": 31337, + "isTestnet": true, + "name": "local1", + "nativeToken": { + "decimals": 18, + "name": "Ether", + "symbol": "ETH" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "http://localhost:8545" + } + ], + "staticMerkleRootMultisigIsmFactory": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "staticMessageIdMultisigIsmFactory": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "staticAggregationIsmFactory": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "staticAggregationHookFactory": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "domainRoutingIsmFactory": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "staticMerkleRootWeightedMultisigIsmFactory": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "staticMessageIdWeightedMultisigIsmFactory": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "proxyAdmin": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "mailbox": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "interchainAccountRouter": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", + "interchainAccountIsm": "0x9A676e781A523b5d0C0e43731313A708CB607508", + "validatorAnnounce": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", + "testRecipient": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", + "merkleTreeHook": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", + "interchainGasPaymaster": "0x0000000000000000000000000000000000000000", + "index": { + "from": 0 + } } }, "defaultRpcConsensusType": "fallback" diff --git a/rust/main/hyperlane-base/Cargo.toml b/rust/main/hyperlane-base/Cargo.toml index ef25d99405..3e441ab26e 100644 --- a/rust/main/hyperlane-base/Cargo.toml +++ b/rust/main/hyperlane-base/Cargo.toml @@ -52,6 +52,7 @@ hyperlane-ethereum = { path = "../chains/hyperlane-ethereum" } hyperlane-fuel = { path = "../chains/hyperlane-fuel" } hyperlane-sealevel = { path = "../chains/hyperlane-sealevel" } hyperlane-cosmos = { path = "../chains/hyperlane-cosmos" } +hyperlane-cosmos-native = { path = "../chains/hyperlane-cosmos-native" } hyperlane-test = { path = "../hyperlane-test" } # dependency version is determined by etheres diff --git a/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs b/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs index 563d0fcc74..5a2d9e40fb 100644 --- a/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs +++ b/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs @@ -33,6 +33,7 @@ impl Indexable for HyperlaneMessage { HyperlaneDomainProtocol::Fuel => todo!(), HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::SequenceAware, + HyperlaneDomainProtocol::CosmosNative => CursorType::SequenceAware, } } @@ -49,6 +50,7 @@ impl Indexable for InterchainGasPayment { HyperlaneDomainProtocol::Fuel => todo!(), HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::RateLimited, + HyperlaneDomainProtocol::CosmosNative => CursorType::RateLimited, } } } @@ -60,6 +62,7 @@ impl Indexable for MerkleTreeInsertion { HyperlaneDomainProtocol::Fuel => todo!(), HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::SequenceAware, + HyperlaneDomainProtocol::CosmosNative => CursorType::SequenceAware, } } } @@ -71,6 +74,7 @@ impl Indexable for Delivery { HyperlaneDomainProtocol::Fuel => todo!(), HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::RateLimited, + HyperlaneDomainProtocol::CosmosNative => CursorType::SequenceAware, } } } diff --git a/rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs b/rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs index 967f4e6053..47ddeb02f6 100644 --- a/rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs +++ b/rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs @@ -92,7 +92,7 @@ impl ForwardSequenceAwareSyncCursor { /// If there are no logs to index, returns `None`. /// If there are logs to index, returns the range of logs, either by sequence or block number /// depending on the mode. - #[instrument(ret)] + // #[instrument(ret)] pub async fn get_next_range(&mut self) -> Result>> { // Skip any already indexed logs. self.skip_indexed().await?; diff --git a/rust/main/hyperlane-base/src/settings/chains.rs b/rust/main/hyperlane-base/src/settings/chains.rs index 33600418bb..1fde7f3d78 100644 --- a/rust/main/hyperlane-base/src/settings/chains.rs +++ b/rust/main/hyperlane-base/src/settings/chains.rs @@ -14,6 +14,7 @@ use hyperlane_core::{ SequenceAwareIndexer, ValidatorAnnounce, H256, }; use hyperlane_cosmos as h_cosmos; +use hyperlane_cosmos_native as h_cosmos_native; use hyperlane_ethereum::{ self as h_eth, BuildableWithProvider, EthereumInterchainGasPaymasterAbi, EthereumMailboxAbi, EthereumReorgPeriod, EthereumValidatorAnnounceAbi, @@ -137,6 +138,8 @@ pub enum ChainConnectionConf { Sealevel(h_sealevel::ConnectionConf), /// Cosmos configuration. Cosmos(h_cosmos::ConnectionConf), + /// Cosmos native configuration + CosmosNative(h_cosmos_native::ConnectionConf), } impl ChainConnectionConf { @@ -147,6 +150,7 @@ impl ChainConnectionConf { Self::Fuel(_) => HyperlaneDomainProtocol::Fuel, Self::Sealevel(_) => HyperlaneDomainProtocol::Sealevel, Self::Cosmos(_) => HyperlaneDomainProtocol::Cosmos, + Self::CosmosNative(_) => HyperlaneDomainProtocol::CosmosNative, } } @@ -217,6 +221,15 @@ impl ChainConf { )?; Ok(Box::new(provider) as Box) } + ChainConnectionConf::CosmosNative(connection_conf) => { + let provider = h_cosmos_native::CosmosNativeProvider::new( + locator.domain.clone(), + connection_conf.clone(), + locator.clone(), + None, + )?; + Ok(Box::new(provider) as Box) + } } .context(ctx) } @@ -250,6 +263,16 @@ impl ChainConf { .map(|m| Box::new(m) as Box) .map_err(Into::into) } + ChainConnectionConf::CosmosNative(conf) => { + let signer = self.cosmos_native_signer().await.context(ctx)?; + h_cosmos_native::CosmosNativeMailbox::new( + conf.clone(), + locator.clone(), + signer.clone(), + ) + .map(|m| Box::new(m) as Box) + .map_err(Into::into) + } } .context(ctx) } @@ -280,6 +303,16 @@ impl ChainConf { let hook = h_cosmos::CosmosMerkleTreeHook::new(conf.clone(), locator.clone(), signer)?; + Ok(Box::new(hook) as Box) + } + ChainConnectionConf::CosmosNative(conf) => { + let signer = self.cosmos_native_signer().await.context(ctx)?; + let hook = h_cosmos_native::CosmosMerkleTreeHook::new( + conf.clone(), + locator.clone(), + signer, + )?; + Ok(Box::new(hook) as Box) } } @@ -327,6 +360,13 @@ impl ChainConf { )?); Ok(indexer as Box>) } + ChainConnectionConf::CosmosNative(conf) => { + let indexer = Box::new(h_cosmos_native::CosmosNativeDispatchIndexer::new( + conf.clone(), + locator, + )?); + Ok(indexer as Box>) + } } .context(ctx) } @@ -372,6 +412,13 @@ impl ChainConf { )?); Ok(indexer as Box>) } + ChainConnectionConf::CosmosNative(conf) => { + let indexer = Box::new(h_cosmos_native::CosmosNativeDeliveryIndexer::new( + conf.clone(), + locator, + )?); + Ok(indexer as Box>) + } } .context(ctx) } @@ -411,6 +458,13 @@ impl ChainConf { )?); Ok(paymaster as Box) } + ChainConnectionConf::CosmosNative(conf) => { + let indexer = Box::new(h_cosmos_native::CosmosNativeGasPaymaster::new( + conf.clone(), + locator, + )?); + Ok(indexer as Box) + } } .context(ctx) } @@ -460,6 +514,13 @@ impl ChainConf { )?); Ok(indexer as Box>) } + ChainConnectionConf::CosmosNative(conf) => { + let indexer = Box::new(h_cosmos_native::CosmosNativeGasPaymaster::new( + conf.clone(), + locator, + )?); + Ok(indexer as Box>) + } } .context(ctx) } @@ -509,6 +570,13 @@ impl ChainConf { )?); Ok(indexer as Box>) } + ChainConnectionConf::CosmosNative(conf) => { + let indexer = Box::new(h_cosmos_native::CosmosNativeTreeInsertionIndexer::new( + conf.clone(), + locator, + )?); + Ok(indexer as Box>) + } } .context(ctx) } @@ -538,6 +606,16 @@ impl ChainConf { signer, )?); + Ok(va as Box) + } + ChainConnectionConf::CosmosNative(conf) => { + let signer = self.cosmos_native_signer().await.context(ctx)?; + let va = Box::new(h_cosmos_native::CosmosNativeValidatorAnnounce::new( + conf.clone(), + locator.clone(), + signer, + )?); + Ok(va as Box) } } @@ -579,6 +657,10 @@ impl ChainConf { )?); Ok(ism as Box) } + ChainConnectionConf::CosmosNative(conf) => { + let ism = Box::new(h_cosmos_native::CosmosNativeIsm::new(conf, locator)?); + Ok(ism as Box) + } } .context(ctx) } @@ -613,6 +695,11 @@ impl ChainConf { )?); Ok(ism as Box) } + ChainConnectionConf::CosmosNative(conf) => { + let ism: Box = + Box::new(h_cosmos_native::CosmosNativeIsm::new(conf, locator)?); + Ok(ism as Box) + } } .context(ctx) } @@ -647,6 +734,7 @@ impl ChainConf { )?); Ok(ism as Box) } + ChainConnectionConf::CosmosNative(_connection_conf) => todo!(), } .context(ctx) } @@ -682,6 +770,7 @@ impl ChainConf { Ok(ism as Box) } + ChainConnectionConf::CosmosNative(_connection_conf) => todo!(), } .context(ctx) } @@ -710,6 +799,9 @@ impl ChainConf { ChainConnectionConf::Cosmos(_) => { Err(eyre!("Cosmos does not support CCIP read ISM yet")).context(ctx) } + ChainConnectionConf::CosmosNative(_connection_conf) => { + Err(eyre!("Cosmos does not support CCIP read ISM yet")).context(ctx) + } } .context(ctx) } @@ -734,6 +826,9 @@ impl ChainConf { Box::new(conf.build::().await?) } ChainConnectionConf::Cosmos(_) => Box::new(conf.build::().await?), + ChainConnectionConf::CosmosNative(_) => { + Box::new(conf.build::().await?) + } }; Ok(Some(chain_signer)) } else { @@ -759,6 +854,10 @@ impl ChainConf { self.signer().await } + async fn cosmos_native_signer(&self) -> Result> { + self.signer().await + } + /// Try to build an agent metrics configuration from the chain config pub async fn agent_metrics_conf(&self, agent_name: String) -> Result { let chain_signer_address = self.chain_signer().await?.map(|s| s.address_string()); diff --git a/rust/main/hyperlane-base/src/settings/mod.rs b/rust/main/hyperlane-base/src/settings/mod.rs index 6c71e20080..d0cced8a2c 100644 --- a/rust/main/hyperlane-base/src/settings/mod.rs +++ b/rust/main/hyperlane-base/src/settings/mod.rs @@ -70,6 +70,7 @@ pub use trace::*; mod envs { pub use hyperlane_cosmos as h_cosmos; + pub use hyperlane_cosmos_native as h_cosmos_native; pub use hyperlane_ethereum as h_eth; pub use hyperlane_fuel as h_fuel; pub use hyperlane_sealevel as h_sealevel; diff --git a/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs b/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs index 70e1b81835..0c87697d71 100644 --- a/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs +++ b/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs @@ -157,6 +157,99 @@ pub fn build_cosmos_connection_conf( } } +pub fn build_cosmos_native_connection_conf( + rpcs: &[Url], + chain: &ValueParser, + err: &mut ConfigParsingError, + operation_batch: OperationBatchConfig, +) -> Option { + let mut local_err = ConfigParsingError::default(); + let grpcs = + parse_base_and_override_urls(chain, "grpcUrls", "customGrpcUrls", "http", &mut local_err); + let apis = + parse_base_and_override_urls(chain, "apiUrls", "customApiUrls", "http", &mut local_err); + + let chain_id = chain + .chain(&mut local_err) + .get_key("chainId") + .parse_string() + .end() + .or_else(|| { + local_err.push(&chain.cwp + "chain_id", eyre!("Missing chain id for chain")); + None + }); + + let prefix = chain + .chain(err) + .get_key("bech32Prefix") + .parse_string() + .end() + .or_else(|| { + local_err.push( + &chain.cwp + "bech32Prefix", + eyre!("Missing bech32 prefix for chain"), + ); + None + }); + + let canonical_asset = if let Some(asset) = chain + .chain(err) + .get_opt_key("canonicalAsset") + .parse_string() + .end() + { + Some(asset.to_string()) + } else if let Some(hrp) = prefix { + Some(format!("u{}", hrp)) + } else { + local_err.push( + &chain.cwp + "canonical_asset", + eyre!("Missing canonical asset for chain"), + ); + None + }; + + let gas_price = chain + .chain(err) + .get_opt_key("gasPrice") + .and_then(parse_cosmos_gas_price) + .end(); + + let contract_address_bytes = chain + .chain(err) + .get_opt_key("contractAddressBytes") + .parse_u64() + .end(); + + let native_token = parse_native_token(chain, err, 18); + + if !local_err.is_ok() { + err.merge(local_err); + None + } else { + let gas_price = gas_price.unwrap(); + let gas_price = h_cosmos_native::RawCosmosAmount { + denom: gas_price.denom, + amount: gas_price.amount, + }; + + Some(ChainConnectionConf::CosmosNative( + h_cosmos_native::ConnectionConf::new( + grpcs, + rpcs.to_owned(), + apis, + chain_id.unwrap().to_string(), + prefix.unwrap().to_string(), + canonical_asset.unwrap(), + gas_price, + contract_address_bytes.unwrap().try_into().unwrap(), + operation_batch, + native_token, + ), + )) + } +} + fn build_sealevel_connection_conf( url: &Url, chain: &ValueParser, @@ -227,5 +320,8 @@ pub fn build_connection_conf( HyperlaneDomainProtocol::Cosmos => { build_cosmos_connection_conf(rpcs, chain, err, operation_batch) } + HyperlaneDomainProtocol::CosmosNative => { + build_cosmos_native_connection_conf(rpcs, chain, err, operation_batch) + } } } diff --git a/rust/main/hyperlane-base/src/settings/signers.rs b/rust/main/hyperlane-base/src/settings/signers.rs index 4233a5c692..0d4ed792b1 100644 --- a/rust/main/hyperlane-base/src/settings/signers.rs +++ b/rust/main/hyperlane-base/src/settings/signers.rs @@ -166,3 +166,29 @@ impl ChainSigner for hyperlane_cosmos::Signer { self.address.clone() } } + +#[async_trait] +impl BuildableWithSignerConf for hyperlane_cosmos_native::Signer { + async fn build(conf: &SignerConf) -> Result { + if let SignerConf::CosmosKey { + key, + prefix, + account_address_type, + } = conf + { + Ok(hyperlane_cosmos_native::Signer::new( + key.as_bytes().to_vec(), + prefix.clone(), + account_address_type, + )?) + } else { + bail!(format!("{conf:?} key is not supported by cosmos")); + } + } +} + +impl ChainSigner for hyperlane_cosmos_native::Signer { + fn address_string(&self) -> String { + self.address.clone() + } +} diff --git a/rust/main/hyperlane-base/src/traits/checkpoint_syncer.rs b/rust/main/hyperlane-base/src/traits/checkpoint_syncer.rs index 0dc6a1e6b7..4be4fefb83 100644 --- a/rust/main/hyperlane-base/src/traits/checkpoint_syncer.rs +++ b/rust/main/hyperlane-base/src/traits/checkpoint_syncer.rs @@ -16,7 +16,8 @@ pub trait CheckpointSyncer: Debug + Send + Sync { /// Update the latest index of this syncer if necessary async fn update_latest_index(&self, index: u32) -> Result<()> { let curr = self.latest_index().await?.unwrap_or(0); - if index > curr { + // always write the 0th index + if index > curr || index == 0 { self.write_latest_index(index).await?; } Ok(()) diff --git a/rust/main/hyperlane-core/src/chain.rs b/rust/main/hyperlane-core/src/chain.rs index 134881bf71..98df375807 100644 --- a/rust/main/hyperlane-core/src/chain.rs +++ b/rust/main/hyperlane-core/src/chain.rs @@ -189,6 +189,8 @@ pub enum KnownHyperlaneDomain { SealevelTest2 = 13376, CosmosTest99990 = 99990, CosmosTest99991 = 99991, + CosmosTestNative1 = 75898671, + CosmosTestNative2 = 75898670, // -- Test chains -- // @@ -199,6 +201,7 @@ pub enum KnownHyperlaneDomain { ConnextSepolia = 6398, Holesky = 17000, MoonbaseAlpha = 1287, + KyveAlpha = 75898669, PlumeTestnet = 161221135, ScrollSepolia = 534351, Sepolia = 11155111, @@ -270,6 +273,8 @@ pub enum HyperlaneDomainProtocol { Sealevel, /// A Cosmos-based chain type which uses hyperlane-cosmos. Cosmos, + /// A Cosmos based chain with uses a module instead of a contract. + CosmosNative, } impl HyperlaneDomainProtocol { @@ -277,9 +282,7 @@ impl HyperlaneDomainProtocol { use HyperlaneDomainProtocol::*; match self { Ethereum => format!("{:?}", H160::from(addr)), - Fuel => format!("{:?}", addr), - Sealevel => format!("{:?}", addr), - Cosmos => format!("{:?}", addr), + _ => format!("{:?}", addr), } } } @@ -328,7 +331,7 @@ impl KnownHyperlaneDomain { ], LocalTestChain: [ Test1, Test2, Test3, FuelTest1, SealevelTest1, SealevelTest2, CosmosTest99990, - CosmosTest99991 + CosmosTest99991, CosmosTestNative1, CosmosTestNative2, KyveAlpha ], }) } @@ -349,7 +352,7 @@ impl KnownHyperlaneDomain { // Test chains Alfajores, BinanceSmartChainTestnet, Chiado, ConnextSepolia, Holesky, MoonbaseAlpha, PlumeTestnet, - ScrollSepolia, Sepolia, SuperpositionTestnet + ScrollSepolia, Sepolia, SuperpositionTestnet, ], HyperlaneDomainProtocol::Fuel: [FuelTest1], @@ -360,6 +363,11 @@ impl KnownHyperlaneDomain { // Local chains CosmosTest99990, CosmosTest99991, ], + HyperlaneDomainProtocol::CosmosNative: [ + CosmosTestNative1, + CosmosTestNative2, + KyveAlpha + ] }) } @@ -392,10 +400,11 @@ impl KnownHyperlaneDomain { // Local chains CosmosTest99990, CosmosTest99991, FuelTest1, SealevelTest1, SealevelTest2, Test1, Test2, Test3, + CosmosTestNative1, CosmosTestNative2, // Test chains Alfajores, BinanceSmartChainTestnet, Chiado, Fuji, Holesky, MoonbaseAlpha, ScrollSepolia, - Sepolia + Sepolia, KyveAlpha ], }) } @@ -576,7 +585,7 @@ impl HyperlaneDomain { use HyperlaneDomainProtocol::*; let protocol = self.domain_protocol(); many_to_one!(match protocol { - IndexMode::Block: [Ethereum, Cosmos], + IndexMode::Block: [Ethereum, Cosmos, CosmosNative], IndexMode::Sequence : [Sealevel, Fuel], }) } From a3b5412adb4ae9edbbc4dcb295488cbb517c7b70 Mon Sep 17 00:00:00 2001 From: yjamin Date: Thu, 30 Jan 2025 13:36:18 +0100 Subject: [PATCH 2/4] feat: removed grpc, code cleanup --- rust/main/Cargo.lock | 47 +- .../agents/relayer/src/msg/metadata/base.rs | 1 + .../chains/hyperlane-cosmos-native/Cargo.toml | 12 +- .../hyperlane-cosmos-native/src/error.rs | 15 +- .../src/indexers/delivery.rs | 11 +- .../src/indexers/dispatch.rs | 13 +- .../src/indexers/gas_paymaster.rs | 11 +- .../src/indexers/indexer.rs | 31 +- .../src/indexers/tree_insertion.rs | 9 +- .../chains/hyperlane-cosmos-native/src/ism.rs | 4 +- .../chains/hyperlane-cosmos-native/src/lib.rs | 4 +- .../src/libs/account/tests.rs | 2 - .../hyperlane-cosmos-native/src/mailbox.rs | 23 +- .../src/merkle_tree_hook.rs | 2 +- .../hyperlane-cosmos-native/src/providers.rs | 9 +- .../src/providers/cosmos.rs | 259 ++++------- .../src/providers/grpc.rs | 419 ------------------ .../src/providers/rest.rs | 127 ++++-- .../src/providers/rpc.rs | 376 ++++++++++++++++ .../src/trait_builder.rs | 18 +- .../src/validator_announce.rs | 15 +- rust/main/config/testnet_config.json | 137 ++---- .../cursors/sequence_aware/backward.rs | 11 +- .../src/settings/parser/connection_parser.rs | 11 +- rust/main/utils/run-locally/src/config.rs | 1 + 25 files changed, 713 insertions(+), 855 deletions(-) delete mode 100644 rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs create mode 100644 rust/main/chains/hyperlane-cosmos-native/src/providers/rpc.rs diff --git a/rust/main/Cargo.lock b/rust/main/Cargo.lock index f70daf7ce6..4b7b94d7c8 100644 --- a/rust/main/Cargo.lock +++ b/rust/main/Cargo.lock @@ -4689,7 +4689,6 @@ dependencies = [ "base64 0.21.7", "bech32 0.11.0", "cosmrs", - "cosmwasm-std 2.1.3", "crypto", "derive-new", "futures", @@ -4699,9 +4698,6 @@ dependencies = [ "hyper-tls", "hyperlane-core", "hyperlane-cosmwasm-interface", - "ibc-proto", - "injective-protobuf", - "injective-std", "itertools 0.12.1", "once_cell", "prost 0.13.4", @@ -4723,47 +4719,6 @@ dependencies = [ "url", ] -[[package]] -name = "hyperlane-cosmos-native" -version = "0.1.0" -dependencies = [ - "async-trait", - "base64 0.21.7", - "bech32 0.9.1", - "cosmrs", - "cosmwasm-std 2.1.3", - "crypto", - "derive-new", - "futures", - "hex 0.4.3", - "http 0.2.12", - "hyper", - "hyper-tls", - "hyperlane-core", - "hyperlane-cosmwasm-interface", - "injective-protobuf", - "injective-std", - "itertools 0.12.1", - "once_cell", - "prost 0.13.4", - "protobuf", - "reqwest", - "ripemd", - "serde", - "serde_json", - "sha2 0.10.8", - "sha256", - "tendermint", - "tendermint-rpc", - "thiserror", - "time", - "tokio", - "tonic 0.9.2", - "tracing", - "tracing-futures", - "url", -] - [[package]] name = "hyperlane-cosmwasm-interface" version = "0.0.6-rc6" @@ -6842,7 +6797,7 @@ dependencies = [ name = "prost-derive" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", "itertools 0.12.1", diff --git a/rust/main/agents/relayer/src/msg/metadata/base.rs b/rust/main/agents/relayer/src/msg/metadata/base.rs index 4bc55457f9..61dbc58553 100644 --- a/rust/main/agents/relayer/src/msg/metadata/base.rs +++ b/rust/main/agents/relayer/src/msg/metadata/base.rs @@ -243,6 +243,7 @@ impl MessageMetadataBuilder { .module_type() .await .context("When fetching module type")?; + let cloned = self.clone_with_incremented_depth()?; let metadata_builder: Box = match module_type { diff --git a/rust/main/chains/hyperlane-cosmos-native/Cargo.toml b/rust/main/chains/hyperlane-cosmos-native/Cargo.toml index 7d4faa4aa5..0525aa7c87 100644 --- a/rust/main/chains/hyperlane-cosmos-native/Cargo.toml +++ b/rust/main/chains/hyperlane-cosmos-native/Cargo.toml @@ -12,8 +12,7 @@ version = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } bech32 = { workspace = true } -cosmrs = { workspace = true, features = ["cosmwasm", "tokio", "grpc", "rpc"] } -cosmwasm-std = { workspace = true } +cosmrs = { workspace = true, features = ["tokio", "grpc", "rpc"] } crypto = { path = "../../utils/crypto" } derive-new = { workspace = true } futures = { workspace = true } @@ -23,8 +22,6 @@ hyperlane-core = { path = "../../hyperlane-core", features = ["async"] } hyperlane-cosmwasm-interface.workspace = true hyper = { workspace = true } hyper-tls = { workspace = true } -injective-protobuf = { workspace = true } -injective-std = { workspace = true } itertools = { workspace = true } once_cell = { workspace = true } protobuf = { workspace = true } @@ -39,12 +36,7 @@ time = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } reqwest = { workspace = true, features = ["json"] } -tonic = { workspace = true, features = [ - "transport", - "tls", - "tls-roots", - "tls-roots-common", -] } +tonic = { workspace = true, features = ["transport", "tls", "tls-roots"] } tracing = { workspace = true } tracing-futures = { workspace = true } url = { workspace = true } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/error.rs b/rust/main/chains/hyperlane-cosmos-native/src/error.rs index 1905c722c5..dce5d8ad44 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/error.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/error.rs @@ -14,9 +14,12 @@ pub enum HyperlaneCosmosError { /// base64 error #[error("{0}")] Base64(#[from] base64::DecodeError), - /// bech32 error + /// bech32 decode error #[error("{0}")] - Bech32(#[from] bech32::Error), + Bech32Decode(#[from] bech32::DecodeError), + /// bech32 encode error + #[error("{0}")] + Bech32Encode(#[from] bech32::EncodeError), /// gRPC error #[error("{0}")] GrpcError(#[from] tonic::Status), @@ -26,12 +29,9 @@ pub enum HyperlaneCosmosError { /// Cosmos error report #[error("{0}")] CosmosErrorReport(#[from] cosmrs::ErrorReport), - #[error("{0}")] /// Cosmrs Tendermint Error - CosmrsTendermintError(#[from] cosmrs::tendermint::Error), #[error("{0}")] - /// CosmWasm Error - CosmWasmError(#[from] cosmwasm_std::StdError), + CosmrsTendermintError(#[from] cosmrs::tendermint::Error), /// Tonic error #[error("{0}")] Tonic(#[from] tonic::transport::Error), @@ -40,7 +40,7 @@ pub enum HyperlaneCosmosError { TonicGenError(#[from] tonic::codegen::StdError), /// Tendermint RPC Error #[error(transparent)] - TendermintError(#[from] tendermint_rpc::error::Error), + TendermintRpcError(#[from] tendermint_rpc::error::Error), /// Prost error #[error("{0}")] Prost(#[from] prost::DecodeError), @@ -71,6 +71,7 @@ pub enum HyperlaneCosmosError { /// Parsing attempt failed #[error("Parsing attempt failed. (Errors: {0:?})")] ParsingAttemptsFailed(Vec), + /// Reqwest Error #[error("{0}")] ReqwestError(reqwest::Error), } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs index 0b10e3848f..c3b87de0ec 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs @@ -31,6 +31,7 @@ pub struct CosmosNativeDeliveryIndexer { } impl CosmosNativeDeliveryIndexer { + /// New Delivery Indexer pub fn new(conf: ConnectionConf, locator: ContractLocator) -> ChainResult { let provider = CosmosNativeProvider::new(locator.domain.clone(), conf, locator, None)?; Ok(CosmosNativeDeliveryIndexer { @@ -47,8 +48,12 @@ impl CosmosNativeDeliveryIndexer { let mut contract_address: Option = None; for attribute in attrs { - let value = attribute.value.replace("\"", ""); - match attribute.key.as_str() { + let key = attribute.key_str().map_err(HyperlaneCosmosError::from)?; + let value = attribute + .value_str() + .map_err(HyperlaneCosmosError::from)? + .replace("\"", ""); + match key { "message_id" => { message_id = Some(value.parse()?); } @@ -98,7 +103,7 @@ impl Indexer for CosmosNativeDeliveryIndexer { #[async_trait] impl SequenceAwareIndexer for CosmosNativeDeliveryIndexer { async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { - let tip = Indexer::::get_finalized_block_number(&self).await?; + let tip = self.get_finalized_block_number().await?; Ok((None, tip)) } } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs index 64c9fd7c1a..5bae00ba5c 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs @@ -34,6 +34,7 @@ pub struct CosmosNativeDispatchIndexer { } impl CosmosNativeDispatchIndexer { + /// New Dispatch Indexer pub fn new(conf: ConnectionConf, locator: ContractLocator) -> ChainResult { let provider = CosmosNativeProvider::new(locator.domain.clone(), conf, locator.clone(), None)?; @@ -55,10 +56,14 @@ impl CosmosNativeDispatchIndexer { let mut contract_address: Option = None; for attribute in attrs { - let value = attribute.value.replace("\"", ""); - let value = value.trim_start_matches("0x"); - match attribute.key.as_str() { + let key = attribute.key_str().map_err(HyperlaneCosmosError::from)?; + let value = attribute + .value_str() + .map_err(HyperlaneCosmosError::from)? + .replace("\"", ""); + match key { "message" => { + let value = value.strip_prefix("0x").unwrap_or(&value); let mut reader = Cursor::new(hex::decode(value)?); message = Some(HyperlaneMessage::read_from(&mut reader)?); } @@ -110,7 +115,7 @@ impl SequenceAwareIndexer for CosmosNativeDispatchIndexer { #[instrument(err, skip(self), ret)] #[allow(clippy::blocks_in_conditions)] // TODO: `rustc` 1.80.1 clippy issue async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { - let tip = Indexer::::get_finalized_block_number(&self).await?; + let tip = self.get_finalized_block_number().await?; let sequence = self .provider .rest() diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs index f0fd32ad8f..ca5be802e8 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs @@ -40,6 +40,7 @@ pub struct CosmosNativeGasPaymaster { impl InterchainGasPaymaster for CosmosNativeGasPaymaster {} impl CosmosNativeGasPaymaster { + /// Gas Payment Indexer pub fn new(conf: ConnectionConf, locator: ContractLocator) -> ChainResult { let provider = CosmosNativeProvider::new(locator.domain.clone(), conf.clone(), locator.clone(), None)?; @@ -65,8 +66,12 @@ impl CosmosNativeGasPaymaster { let mut destination: Option = None; for attribute in attrs { - let value = attribute.value.replace("\"", ""); - match attribute.key.as_str() { + let key = attribute.key_str().map_err(HyperlaneCosmosError::from)?; + let value = attribute + .value_str() + .map_err(HyperlaneCosmosError::from)? + .replace("\"", ""); + match key { "igp_id" => igp_id = Some(value.parse()?), "message_id" => message_id = Some(value.parse()?), "gas_amount" => gas_amount = Some(U256::from_dec_str(&value)?), @@ -162,7 +167,7 @@ impl Indexer for CosmosNativeGasPaymaster { #[async_trait] impl SequenceAwareIndexer for CosmosNativeGasPaymaster { async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { - let tip = Indexer::::get_finalized_block_number(&self).await?; + let tip = self.get_finalized_block_number().await?; Ok((None, tip)) } } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/indexer.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/indexer.rs index 273a36eb55..63039ffd68 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/indexers/indexer.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/indexer.rs @@ -38,29 +38,38 @@ impl ParsedEvent { self.event } } - +/// Event indexer +/// +/// Indexes all events of a specified event type. #[derive(Debug, Clone)] pub struct EventIndexer { target_type: String, provider: Arc, } +/// Parsing function +/// +/// This function is used to parse the event attributes into a ParsedEvent. pub type Parser = for<'a> fn(&'a Vec) -> ChainResult>; impl EventIndexer { + /// Create a new EventIndexer. pub fn new(target_type: String, provider: Arc) -> EventIndexer { EventIndexer { - target_type: target_type, - provider: provider, + target_type, + provider, } } + /// Current block height + /// + /// used by the indexer struct pub async fn get_finalized_block_number(&self) -> ChainResult { - let result = self.provider.grpc().get_block_number().await?; + let result = self.provider.rpc().get_block_number().await?; Ok(result as u32) } - pub async fn fetch_logs_by_tx_hash( + pub(crate) async fn fetch_logs_by_tx_hash( &self, tx_hash: H512, parser: Parser, @@ -69,7 +78,7 @@ impl EventIndexer { T: PartialEq + 'static, Indexed: From, { - let tx_response = self.provider.get_tx(&tx_hash).await?; + let tx_response = self.provider.rpc().get_tx(&tx_hash).await?; let block_height = tx_response.height; let block = self .provider @@ -83,7 +92,7 @@ impl EventIndexer { Ok(result) } - pub async fn fetch_logs_in_range( + pub(crate) async fn fetch_logs_in_range( &self, range: RangeInclusive, parser: Parser, @@ -108,7 +117,6 @@ impl EventIndexer { .flatten() .map(|(logs, block_number)| { if let Err(err) = &logs { - warn!(?err, "error"); warn!(?err, ?block_number, "Failed to fetch logs for block"); } logs @@ -132,8 +140,8 @@ impl EventIndexer { where T: PartialEq + Debug + 'static, { - let block = self.provider.get_block(block_height).await?; - let block_results = self.provider.get_block_results(block_height).await?; + let block = self.provider.rpc().get_block(block_height).await?; + let block_results = self.provider.rpc().get_block_results(block_height).await?; let result = self.handle_txs(block, block_results, parser); Ok(result) } @@ -218,9 +226,6 @@ impl EventIndexer { parser(&event.attributes) .map_err(|err| { - // This can happen if we attempt to parse an event that just happens - // to have the same name but a different structure. - println!("Failed to parse event attributes: {}", err); trace!(?err, tx_hash=?tx_hash, log_idx, ?event, "Failed to parse event attributes"); }) .ok() diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs index ba1ef19888..e28dab2fe7 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs @@ -33,6 +33,7 @@ pub struct CosmosNativeTreeInsertionIndexer { } impl CosmosNativeTreeInsertionIndexer { + /// New Tree Insertion Indexer pub fn new(conf: ConnectionConf, locator: ContractLocator) -> ChainResult { let provider = CosmosNativeProvider::new(locator.domain.clone(), conf, locator.clone(), None)?; @@ -56,8 +57,12 @@ impl CosmosNativeTreeInsertionIndexer { let mut contract_address: Option = None; for attribute in attrs { - let value = attribute.value.replace("\"", ""); - match attribute.key.as_str() { + let key = attribute.key_str().map_err(HyperlaneCosmosError::from)?; + let value = attribute + .value_str() + .map_err(HyperlaneCosmosError::from)? + .replace("\"", ""); + match key { "message_id" => { message_id = Some(value.parse()?); } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/ism.rs b/rust/main/chains/hyperlane-cosmos-native/src/ism.rs index 56ab3a5e75..f9a1eef880 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/ism.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/ism.rs @@ -10,6 +10,7 @@ use tonic::async_trait; use crate::{ConnectionConf, CosmosNativeProvider, Signer, ISM}; +/// Cosmos Native ISM #[derive(Debug)] pub struct CosmosNativeIsm { /// The domain of the ISM contract. @@ -36,7 +37,6 @@ impl CosmosNativeIsm { async fn get_ism(&self) -> ChainResult> { let isms = self.provider.rest().isms(ReorgPeriod::None).await?; - for ism in isms { match ism.clone() { ISM::NoOpISM { id, .. } if id.parse::()? == self.address => { @@ -103,7 +103,7 @@ impl InterchainSecurityModule for CosmosNativeIsm { message: &HyperlaneMessage, metadata: &[u8], ) -> ChainResult> { - // TODO: is only relevant for aggeration isms -> cosmos native does not support them yet + // NOTE: is only relevant for aggeration isms -> cosmos native does not support them yet Ok(Some(1.into())) } } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/lib.rs b/rust/main/chains/hyperlane-cosmos-native/src/lib.rs index 414616b675..9282ca2038 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/lib.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/lib.rs @@ -1,7 +1,7 @@ -//! Implementation of hyperlane for cosmos. +//! Implementation of hyperlane for the native cosmos module. #![forbid(unsafe_code)] -// #![warn(missing_docs)] +#![warn(missing_docs)] // TODO: Remove once we start filling things in #![allow(unused_variables)] #![allow(unused_imports)] // TODO: `rustc` 1.80.1 clippy issue diff --git a/rust/main/chains/hyperlane-cosmos-native/src/libs/account/tests.rs b/rust/main/chains/hyperlane-cosmos-native/src/libs/account/tests.rs index 0ba8f73d74..9daecc360d 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/libs/account/tests.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/libs/account/tests.rs @@ -1,6 +1,4 @@ use cosmrs::crypto::PublicKey; -use cosmwasm_std::HexBinary; - use crypto::decompress_public_key; use hyperlane_core::AccountAddressType; use AccountAddressType::{Bitcoin, Ethereum}; diff --git a/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs b/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs index 83a07e917d..ababdff0b7 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs @@ -1,4 +1,4 @@ -use cosmrs::Any; +use cosmrs::{proto::cosmos::base::abci::v1beta1::TxResponse, Any, Tx}; use hex::ToHex; use hyperlane_core::{ rpc_clients::BlockNumberGetter, ChainResult, ContractLocator, HyperlaneChain, @@ -8,8 +8,11 @@ use hyperlane_core::{ use prost::Message; use tonic::async_trait; -use crate::{ConnectionConf, CosmosNativeProvider, MsgProcessMessage, Signer}; +use crate::{ + ConnectionConf, CosmosNativeProvider, HyperlaneCosmosError, MsgProcessMessage, Signer, +}; +/// Cosmos Native Mailbox #[derive(Debug, Clone)] pub struct CosmosNativeMailbox { provider: CosmosNativeProvider, @@ -127,14 +130,16 @@ impl Mailbox for CosmosNativeMailbox { let response = self .provider - .grpc() + .rpc() .send(vec![any_encoded], gas_limit) .await?; + let tx = TxResponse::decode(response.data).map_err(HyperlaneCosmosError::from)?; + Ok(TxOutcome { - transaction_id: H256::from_slice(hex::decode(response.txhash)?.as_slice()).into(), - executed: response.code == 0, - gas_used: U256::from(response.gas_used), + transaction_id: H256::from_slice(response.hash.as_bytes()).into(), + executed: tx.code == 0, + gas_used: tx.gas_used.into(), gas_price: U256::one().try_into()?, }) } @@ -147,10 +152,10 @@ impl Mailbox for CosmosNativeMailbox { ) -> ChainResult { let hex_string = hex::encode(metadata); let any_encoded = self.encode_hyperlane_message(message, metadata); - let gas_limit = self.provider.grpc().estimate_gas(vec![any_encoded]).await?; + let gas_limit = self.provider.rpc().estimate_gas(vec![any_encoded]).await?; Ok(TxCostEstimate { gas_limit: gas_limit.into(), - gas_price: self.provider.grpc().gas_price(), + gas_price: self.provider.rpc().gas_price(), l2_gas_limit: None, }) } @@ -158,6 +163,6 @@ impl Mailbox for CosmosNativeMailbox { /// Get the calldata for a transaction to process a message with a proof /// against the provided signed checkpoint fn process_calldata(&self, message: &HyperlaneMessage, metadata: &[u8]) -> Vec { - todo!() // TODO: check if we really don't need that + todo!() // we dont need this for now } } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/merkle_tree_hook.rs b/rust/main/chains/hyperlane-cosmos-native/src/merkle_tree_hook.rs index ddd8e1b3f7..76863df372 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/merkle_tree_hook.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/merkle_tree_hook.rs @@ -95,7 +95,7 @@ impl MerkleTreeHook for CosmosMerkleTreeHook { let branch = branch.as_slice(); let branch: [H256; 32] = match branch.try_into() { Ok(ba) => ba, - Err(_) => { + Err(e) => { return Err(ChainCommunicationError::CustomError( "Failed to convert incremental tree. expected branch length of 32".to_string(), )) diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers.rs index 0b651fe00d..fd4c5a17a8 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/providers.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers.rs @@ -1,9 +1,8 @@ mod cosmos; -mod grpc; mod rest; +mod rpc; -pub use cosmos::{ - CosmosNativeProvider, MsgAnnounceValidator, MsgProcessMessage, MsgRemoteTransfer, -}; -pub use grpc::*; +pub use cosmos::CosmosNativeProvider; +pub(crate) use cosmos::{MsgAnnounceValidator, MsgProcessMessage, MsgRemoteTransfer}; pub use rest::*; +pub use rpc::*; diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs index 41079317f6..cb24b311ab 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs @@ -1,13 +1,21 @@ -use std::io::Cursor; +use std::{io::Cursor, ops::Deref}; use cosmrs::{ crypto::PublicKey, - proto::{cosmos::base::abci::v1beta1::TxResponse, tendermint::types::Block}, + proto::{ + cosmos::{ + auth::v1beta1::QueryAccountRequest, + bank::v1beta1::{QueryBalanceRequest, QueryBalanceResponse}, + base::abci::v1beta1::TxResponse, + }, + tendermint::types::Block, + }, tx::{SequenceNumber, SignerInfo, SignerPublicKey}, AccountId, Any, Coin, Tx, }; +use derive_new::new; -use super::{grpc::GrpcProvider, rest::RestProvider, CosmosFallbackProvider}; +use super::{rest::RestProvider, RpcProvider}; use crate::{ ConnectionConf, CosmosAccountId, CosmosAddress, CosmosAmount, HyperlaneCosmosError, Signer, }; @@ -33,9 +41,32 @@ use time::OffsetDateTime; use tonic::async_trait; use tracing::{debug, trace, warn}; -// proto structs for encoding and decoding transactions +/// Wrapper of `FallbackProvider` for use in `hyperlane-cosmos-native` +#[derive(new, Clone)] +pub(crate) struct CosmosFallbackProvider { + fallback_provider: FallbackProvider, +} + +impl Deref for CosmosFallbackProvider { + type Target = FallbackProvider; + + fn deref(&self) -> &Self::Target { + &self.fallback_provider + } +} + +impl std::fmt::Debug for CosmosFallbackProvider +where + C: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.fallback_provider.fmt(f) + } +} + +// structs for encoding and decoding transactions with protofbuf #[derive(Clone, PartialEq, ::prost::Message)] -pub struct MsgProcessMessage { +pub(crate) struct MsgProcessMessage { #[prost(string, tag = "1")] pub mailbox_id: ::prost::alloc::string::String, #[prost(string, tag = "2")] @@ -47,7 +78,7 @@ pub struct MsgProcessMessage { } #[derive(Clone, PartialEq, ::prost::Message)] -pub struct MsgAnnounceValidator { +pub(crate) struct MsgAnnounceValidator { #[prost(string, tag = "1")] pub validator: ::prost::alloc::string::String, #[prost(string, tag = "2")] @@ -61,7 +92,7 @@ pub struct MsgAnnounceValidator { } #[derive(Clone, PartialEq, ::prost::Message)] -pub struct MsgRemoteTransfer { +pub(crate) struct MsgRemoteTransfer { #[prost(string, tag = "1")] pub sender: ::prost::alloc::string::String, #[prost(string, tag = "2")] @@ -72,71 +103,51 @@ pub struct MsgRemoteTransfer { pub amount: ::prost::alloc::string::String, } -#[derive(Debug, Clone)] -struct CosmosHttpClient { - client: HttpClient, -} - +/// Cosmos Native Provider +/// +/// implements the HyperlaneProvider trait #[derive(Debug, Clone)] pub struct CosmosNativeProvider { - connection_conf: ConnectionConf, - provider: CosmosFallbackProvider, - grpc: GrpcProvider, + conf: ConnectionConf, rest: RestProvider, + rpc: RpcProvider, domain: HyperlaneDomain, } impl CosmosNativeProvider { - #[doc = "Create a new Cosmos Provider instance"] + /// Create a new Cosmos Provider instance pub fn new( domain: HyperlaneDomain, conf: ConnectionConf, locator: ContractLocator, signer: Option, ) -> ChainResult { - let clients = conf - .get_rpc_urls() - .iter() - .map(|url| { - tendermint_rpc::Url::try_from(url.to_owned()) - .map_err(ChainCommunicationError::from_other) - .and_then(|url| { - tendermint_rpc::HttpClientUrl::try_from(url) - .map_err(ChainCommunicationError::from_other) - }) - .and_then(|url| { - HttpClient::builder(url) - .compat_mode(CompatMode::latest()) - .build() - .map_err(ChainCommunicationError::from_other) - }) - .map(|client| CosmosHttpClient { client }) - }) - .collect::, _>>()?; - - let providers = FallbackProvider::new(clients); - let client = CosmosFallbackProvider::new(providers); - let gas_price = CosmosAmount::try_from(conf.get_minimum_gas_price().clone())?; - let grpc_provider = GrpcProvider::new( - domain.clone(), - conf.clone(), - gas_price.clone(), - locator, - signer, - )?; - + let rpc = RpcProvider::new(conf.clone(), signer)?; let rest = RestProvider::new(conf.get_api_urls().iter().map(|url| url.to_string())); Ok(CosmosNativeProvider { domain, - connection_conf: conf, - provider: client, - grpc: grpc_provider, + conf, + rpc, rest, }) } + /// RPC Provider + /// + /// This is used for general chain communication like getting the block number, block, transaction, etc. + pub fn rpc(&self) -> &RpcProvider { + &self.rpc + } + + /// Rest Provider + /// + /// This is used for the Module Communication and querying the module state. Like mailboxes, isms etc. + pub fn rest(&self) -> &RestProvider { + &self.rest + } + // extract the contract address from the tx fn contract(tx: &Tx) -> ChainResult { // check for all transfer messages @@ -148,17 +159,17 @@ impl CosmosNativeProvider { .cloned() .collect(); + // right now one transaction can include max. one transfer if remote_transfers.len() > 1 { let msg = "transaction contains multiple execution messages"; Err(HyperlaneCosmosError::ParsingFailed(msg.to_owned()))? } - let msg = &remote_transfers[0]; - let result = MsgRemoteTransfer::decode(Cursor::new(msg.value.to_vec())).map_err(|err| { - HyperlaneCosmosError::ParsingFailed(format!( - "Can't parse any to MsgRemoteTransfer. {msg:?}" - )) + let msg = remote_transfers.first().ok_or_else(|| { + ChainCommunicationError::from_other_str("tx does not contain any remote transfers") })?; + let result = + MsgRemoteTransfer::decode(msg.value.as_slice()).map_err(HyperlaneCosmosError::from)?; let recipient = result.recipient; let recipient: H256 = recipient.parse()?; @@ -198,7 +209,7 @@ impl CosmosNativeProvider { let account_id = CosmosAccountId::account_id_from_pubkey( public_key, - &self.connection_conf.get_bech32_prefix(), + &self.conf.get_bech32_prefix(), &account_address_type, )?; @@ -270,7 +281,7 @@ impl CosmosNativeProvider { /// in the configuration of a chain. If fees contain an entry in a different denomination, /// we report it in the logs. fn report_unsupported_denominations(&self, tx: &Tx, tx_hash: &H256) -> ChainResult<()> { - let supported_denomination = self.connection_conf.get_minimum_gas_price().denom; + let supported_denomination = self.conf.get_minimum_gas_price().denom; let unsupported_denominations = tx .auth_info .fee @@ -298,7 +309,7 @@ impl CosmosNativeProvider { /// /// If fees are expressed in an unsupported denomination, they will be ignored. fn convert_fee(&self, coin: &Coin) -> ChainResult { - let native_token = self.connection_conf.get_native_token(); + let native_token = self.conf.get_native_token(); if coin.denom.as_ref() != native_token.denom { return Ok(U256::zero()); @@ -312,7 +323,6 @@ impl CosmosNativeProvider { } fn calculate_gas_price(&self, hash: &H256, tx: &Tx) -> ChainResult { - // TODO support multiple denominations for amount let supported = self.report_unsupported_denominations(tx, hash); if supported.is_err() { return Ok(U256::max_value()); @@ -333,84 +343,15 @@ impl CosmosNativeProvider { Ok(fee / gas_limit) } - - pub fn grpc(&self) -> &GrpcProvider { - &self.grpc - } - - pub fn rest(&self) -> &RestProvider { - &self.rest - } - - pub async fn get_tx(&self, hash: &H512) -> ChainResult { - let hash: H256 = H256::from_slice(&h512_to_bytes(hash)); - - let tendermint_hash = Hash::from_bytes(Algorithm::Sha256, hash.as_bytes()) - .expect("transaction hash should be of correct size"); - - let response = - self.provider - .call(|client| { - let future = async move { - client.client.tx(tendermint_hash, false).await.map_err(|e| { - ChainCommunicationError::from(HyperlaneCosmosError::from(e)) - }) - }; - Box::pin(future) - }) - .await?; - - let received_hash = H256::from_slice(response.hash.as_bytes()); - if received_hash != hash { - return Err(ChainCommunicationError::from_other_str(&format!( - "received incorrect transaction, expected hash: {:?}, received hash: {:?}", - hash, received_hash, - ))); - } - - Ok(response) - } - - pub async fn get_block(&self, height: u32) -> ChainResult { - let response = - self.provider - .call(|client| { - let future = async move { - client.client.block(height).await.map_err(|e| { - ChainCommunicationError::from(HyperlaneCosmosError::from(e)) - }) - }; - Box::pin(future) - }) - .await?; - - Ok(response) - } - - pub async fn get_block_results(&self, height: u32) -> ChainResult { - let response = - self.provider - .call(|client| { - let future = async move { - client.client.block_results(height).await.map_err(|e| { - ChainCommunicationError::from(HyperlaneCosmosError::from(e)) - }) - }; - Box::pin(future) - }) - .await?; - - Ok(response) - } } impl HyperlaneChain for CosmosNativeProvider { - #[doc = " Return the domain"] + /// Return the domain fn domain(&self) -> &HyperlaneDomain { &self.domain } - #[doc = " A provider for the chain"] + /// A provider for the chain fn provider(&self) -> Box { Box::new(self.clone()) } @@ -419,18 +360,7 @@ impl HyperlaneChain for CosmosNativeProvider { #[async_trait] impl HyperlaneProvider for CosmosNativeProvider { async fn get_block_by_height(&self, height: u64) -> ChainResult { - let response = - self.provider - .call(|client| { - let future = async move { - client.client.block(height as u32).await.map_err(|e| { - ChainCommunicationError::from(HyperlaneCosmosError::from(e)) - }) - }; - Box::pin(future) - }) - .await?; - + let response = self.rpc.get_block(height as u32).await?; let block = response.block; let block_height = block.header.height.value(); @@ -454,36 +384,13 @@ impl HyperlaneProvider for CosmosNativeProvider { } async fn get_txn_by_hash(&self, hash: &H512) -> ChainResult { - let hash: H256 = H256::from_slice(&h512_to_bytes(hash)); - - let tendermint_hash = Hash::from_bytes(Algorithm::Sha256, hash.as_bytes()) - .expect("transaction hash should be of correct size"); - - let response = - self.provider - .call(|client| { - let future = async move { - client.client.tx(tendermint_hash, false).await.map_err(|e| { - ChainCommunicationError::from(HyperlaneCosmosError::from(e)) - }) - }; - Box::pin(future) - }) - .await?; - - let received_hash = H256::from_slice(response.hash.as_bytes()); - - if received_hash != hash { - return Err(ChainCommunicationError::from_other_str(&format!( - "received incorrect transaction, expected hash: {:?}, received hash: {:?}", - hash, received_hash, - ))); - } - + let response = self.rpc.get_tx(hash).await?; let tx = Tx::from_bytes(&response.tx)?; let contract = Self::contract(&tx)?; let (sender, nonce) = self.sender_and_nonce(&tx)?; + + let hash: H256 = H256::from_slice(&h512_to_bytes(hash)); let gas_price = self.calculate_gas_price(&hash, &tx)?; let tx_info = TxnInfo { @@ -512,24 +419,10 @@ impl HyperlaneProvider for CosmosNativeProvider { } async fn get_balance(&self, address: String) -> ChainResult { - self.grpc - .get_balance(address, self.connection_conf.get_canonical_asset()) - .await + self.rpc.get_balance(address).await } async fn get_chain_metrics(&self) -> ChainResult> { return Ok(None); } } - -#[async_trait] -impl BlockNumberGetter for CosmosHttpClient { - async fn get_block_number(&self) -> Result { - let block = self - .client - .latest_block() - .await - .map_err(Into::::into)?; - Ok(block.block.header.height.value()) - } -} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs deleted file mode 100644 index a5f9aa96d1..0000000000 --- a/rust/main/chains/hyperlane-cosmos-native/src/providers/grpc.rs +++ /dev/null @@ -1,419 +0,0 @@ -use std::{ - fmt::{Debug, Formatter}, - ops::Deref, -}; - -use async_trait::async_trait; -use base64::Engine; -use cosmrs::{ - proto::{ - cosmos::{ - auth::v1beta1::{ - query_client::QueryClient as QueryAccountClient, BaseAccount, QueryAccountRequest, - }, - bank::v1beta1::{query_client::QueryClient as QueryBalanceClient, QueryBalanceRequest}, - base::{ - abci::v1beta1::TxResponse, - tendermint::v1beta1::{service_client::ServiceClient, GetLatestBlockRequest}, - }, - tx::v1beta1::{ - service_client::ServiceClient as TxServiceClient, BroadcastMode, - BroadcastTxRequest, SimulateRequest, TxRaw, - }, - }, - cosmwasm::wasm::v1::{ - query_client::QueryClient as WasmQueryClient, ContractInfo, MsgExecuteContract, - QueryContractInfoRequest, QuerySmartContractStateRequest, - }, - prost::{self, Message}, - }, - tx::{self, Fee, MessageExt, SignDoc, SignerInfo}, - Any, Coin, -}; -use derive_new::new; -use protobuf::Message as _; -use serde::Serialize; -use tonic::{ - transport::{Channel, Endpoint}, - GrpcMethod, IntoRequest, -}; -use tracing::{debug, instrument}; -use url::Url; - -use hyperlane_core::{ - rpc_clients::{BlockNumberGetter, FallbackProvider}, - ChainCommunicationError, ChainResult, ContractLocator, FixedPointNumber, HyperlaneDomain, U256, -}; - -use crate::CosmosAmount; -use crate::HyperlaneCosmosError; -use crate::{ConnectionConf, CosmosAddress, Signer}; - -/// A multiplier applied to a simulated transaction's gas usage to -/// calculate the estimated gas. -const GAS_ESTIMATE_MULTIPLIER: f64 = 2.0; // TODO: this has to be adjusted accordingly and per chain -/// The number of blocks in the future in which a transaction will -/// be valid for. -const TIMEOUT_BLOCKS: u64 = 1000; - -#[derive(new, Clone)] -pub struct CosmosFallbackProvider { - fallback_provider: FallbackProvider, -} - -impl Deref for CosmosFallbackProvider { - type Target = FallbackProvider; - - fn deref(&self) -> &Self::Target { - &self.fallback_provider - } -} - -impl Debug for CosmosFallbackProvider -where - C: Debug, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.fallback_provider.fmt(f) - } -} - -#[derive(Debug, Clone, new)] -struct CosmosChannel { - channel: Channel, - /// The url that this channel is connected to. - /// Not explicitly used, but useful for debugging. - _url: Url, -} - -#[async_trait] -impl BlockNumberGetter for CosmosChannel { - async fn get_block_number(&self) -> Result { - let mut client = ServiceClient::new(self.channel.clone()); - let request = tonic::Request::new(GetLatestBlockRequest {}); - - let response = client - .get_latest_block(request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); - let height = response - .block - .ok_or_else(|| ChainCommunicationError::from_other_str("block not present"))? - .header - .ok_or_else(|| ChainCommunicationError::from_other_str("header not present"))? - .height; - - Ok(height as u64) - } -} - -#[derive(Debug, Clone)] -/// CosmWasm GRPC provider. -pub struct GrpcProvider { - /// Connection configuration. - conf: ConnectionConf, - /// Signer for transactions. - signer: Option, - /// GRPC Channel that can be cheaply cloned. - /// See `` - provider: CosmosFallbackProvider, - gas_price: CosmosAmount, -} - -impl GrpcProvider { - /// Create new CosmWasm GRPC Provider. - pub fn new( - domain: HyperlaneDomain, - conf: ConnectionConf, - gas_price: CosmosAmount, - locator: ContractLocator, - signer: Option, - ) -> ChainResult { - // get all the configured grpc urls and convert them to a Vec - let channels: Result, _> = conf - .get_grpc_urls() - .into_iter() - .map(|url| { - Endpoint::new(url.to_string()) - .map(|e| CosmosChannel::new(e.connect_lazy(), url)) - .map_err(Into::::into) - }) - .collect(); - let mut builder = FallbackProvider::builder(); - builder = builder.add_providers(channels?); - let fallback_provider = builder.build(); - let provider = CosmosFallbackProvider::new(fallback_provider); - - let contract_address = CosmosAddress::from_h256( - locator.address, - &conf.get_bech32_prefix(), - conf.get_contract_address_bytes(), - )?; - - Ok(Self { - conf, - signer, - provider, - gas_price, - }) - } - - /// Gets a signer, or returns an error if one is not available. - fn get_signer(&self) -> ChainResult<&Signer> { - self.signer - .as_ref() - .ok_or(ChainCommunicationError::SignerUnavailable) - } - - /// Get the gas price - pub fn gas_price(&self) -> FixedPointNumber { - self.gas_price.amount.clone() - } - - /// Generates an unsigned SignDoc for a transaction and the Coin amount - /// required to pay for tx fees. - async fn generate_unsigned_sign_doc_and_fee( - &self, - msgs: Vec, - gas_limit: u64, - ) -> ChainResult<(SignDoc, Coin)> { - // As this function is only used for estimating gas or sending transactions, - // we can reasonably expect to have a signer. - let signer = self.get_signer()?; - let account_info = self.account_query(signer.address.clone()).await?; - let current_height = self.latest_block_height().await?; - let timeout_height = current_height + TIMEOUT_BLOCKS; - - let tx_body = tx::Body::new( - msgs, - String::default(), - TryInto::::try_into(timeout_height) - .map_err(ChainCommunicationError::from_other)?, - ); - let signer_info = SignerInfo::single_direct(Some(signer.public_key), account_info.sequence); - - let amount: u128 = (FixedPointNumber::from(gas_limit) * self.gas_price()) - .ceil_to_integer() - .try_into()?; - let fee_coin = Coin::new( - // The fee to pay is the gas limit * the gas price - amount, - self.conf.get_canonical_asset().as_str(), - ) - .map_err(Into::::into)?; - let auth_info = - signer_info.auth_info(Fee::from_amount_and_gas(fee_coin.clone(), gas_limit)); - - let chain_id = self - .conf - .get_chain_id() - .parse() - .map_err(Into::::into)?; - - Ok(( - SignDoc::new(&tx_body, &auth_info, &chain_id, account_info.account_number) - .map_err(Into::::into)?, - fee_coin, - )) - } - - /// Generates a raw signed transaction including `msgs`, estimating gas if a limit is not provided, - /// and the Coin amount required to pay for tx fees. - async fn generate_raw_signed_tx_and_fee( - &self, - msgs: Vec, - gas_limit: Option, - ) -> ChainResult<(Vec, Coin)> { - let gas_limit = if let Some(l) = gas_limit { - l - } else { - self.estimate_gas(msgs.clone()).await? - }; - - let (sign_doc, fee) = self - .generate_unsigned_sign_doc_and_fee(msgs, gas_limit) - .await?; - - let signer = self.get_signer()?; - let tx_signed = sign_doc - .sign(&signer.signing_key()?) - .map_err(Into::::into)?; - Ok(( - tx_signed - .to_bytes() - .map_err(Into::::into)?, - fee, - )) - } - - /// send a transaction - pub async fn send( - &self, - msgs: Vec, - gas_limit: Option, - ) -> ChainResult { - let gas_limit = if let Some(l) = gas_limit { - l - } else { - self.estimate_gas(msgs.clone()).await? - }; - - let (tx_bytes, _) = self - .generate_raw_signed_tx_and_fee(msgs, Some(gas_limit)) - .await?; - let tx_response = self - .provider - .call(move |provider| { - let tx_bytes_clone = tx_bytes.clone(); - let future = async move { - let mut client = TxServiceClient::new(provider.channel.clone()); - let request = tonic::Request::new(BroadcastTxRequest { - tx_bytes: tx_bytes_clone, - mode: BroadcastMode::Sync as i32, - }); - - let tx_response = client - .broadcast_tx(request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner() - .tx_response - .ok_or_else(|| { - ChainCommunicationError::from_other_str("tx_response not present") - })?; - Ok(tx_response) - }; - Box::pin(future) - }) - .await?; - Ok(tx_response) - } - - /// Estimates gas for a transaction containing `msgs`. - pub async fn estimate_gas(&self, msgs: Vec) -> ChainResult { - // Get a sign doc with 0 gas, because we plan to simulate - let (sign_doc, _) = self.generate_unsigned_sign_doc_and_fee(msgs, 0).await?; - - let raw_tx = TxRaw { - body_bytes: sign_doc.body_bytes, - auth_info_bytes: sign_doc.auth_info_bytes, - // The poorly documented trick to simulating a tx without a valid signature is to just pass - // in a single empty signature. Taken from cosmjs: - // https://github.com/cosmos/cosmjs/blob/44893af824f0712d1f406a8daa9fcae335422235/packages/stargate/src/modules/tx/queries.ts#L67 - signatures: vec![vec![]], - }; - let tx_bytes = raw_tx - .to_bytes() - .map_err(ChainCommunicationError::from_other)?; - let gas_used = self - .provider - .call(move |provider| { - let tx_bytes_clone = tx_bytes.clone(); - let future = async move { - let mut client = TxServiceClient::new(provider.channel.clone()); - #[allow(deprecated)] - let sim_req = tonic::Request::new(SimulateRequest { - tx: None, - tx_bytes: tx_bytes_clone, - }); - let gas_used = client - .simulate(sim_req) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner() - .gas_info - .ok_or_else(|| { - ChainCommunicationError::from_other_str("gas info not present") - })? - .gas_used; - - Ok(gas_used) - }; - Box::pin(future) - }) - .await?; - - let gas_estimate = (gas_used as f64 * GAS_ESTIMATE_MULTIPLIER) as u64; - - Ok(gas_estimate) - } - - /// Fetches balance for a given `address` and `denom` - pub async fn get_balance(&self, address: String, denom: String) -> ChainResult { - let response = self - .provider - .call(move |provider| { - let address = address.clone(); - let denom = denom.clone(); - let future = async move { - let mut client = QueryBalanceClient::new(provider.channel.clone()); - let balance_request = - tonic::Request::new(QueryBalanceRequest { address, denom }); - let response = client - .balance(balance_request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); - Ok(response) - }; - Box::pin(future) - }) - .await?; - - let balance = response - .balance - .ok_or_else(|| ChainCommunicationError::from_other_str("account not present"))?; - - Ok(U256::from_dec_str(&balance.amount)?) - } - - /// Queries an account. - pub async fn account_query(&self, account: String) -> ChainResult { - let response = self - .provider - .call(move |provider| { - let address = account.clone(); - let future = async move { - let mut client = QueryAccountClient::new(provider.channel.clone()); - let request = tonic::Request::new(QueryAccountRequest { address }); - let response = client - .account(request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); - Ok(response) - }; - Box::pin(future) - }) - .await?; - - let account = BaseAccount::decode( - response - .account - .ok_or_else(|| ChainCommunicationError::from_other_str("account not present"))? - .value - .as_slice(), - ) - .map_err(Into::::into)?; - Ok(account) - } - - async fn latest_block_height(&self) -> ChainResult { - let height = self - .provider - .call(move |provider| { - let future = async move { provider.get_block_number().await }; - Box::pin(future) - }) - .await?; - Ok(height) - } -} - -#[async_trait] -impl BlockNumberGetter for GrpcProvider { - async fn get_block_number(&self) -> Result { - self.latest_block_height().await - } -} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/rest.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/rest.rs index fe6d4a399f..fd177fc9f7 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/providers/rest.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers/rest.rs @@ -1,127 +1,197 @@ -use std::cmp::max; +use std::{cmp::max, sync::atomic::AtomicU32}; +use base64::Engine; use hyperlane_core::{ rpc_clients::{BlockNumberGetter, FallbackProvider}, utils, ChainCommunicationError, ChainResult, ReorgPeriod, H160, H256, }; use reqwest::Error; use serde::{de::DeserializeOwned, Deserialize, Deserializer}; +use tendermint::block::Height; +use tendermint_rpc::endpoint::abci_query::{self, AbciQuery}; use tonic::async_trait; use crate::HyperlaneCosmosError; -use super::CosmosFallbackProvider; +use super::cosmos::CosmosFallbackProvider; #[derive(Debug, Clone)] struct RestClient { url: String, } +/// Rest Provider +/// +/// Responsible for making requests to the Hyperlane Cosmos Rest API #[derive(Debug, Clone)] pub struct RestProvider { clients: CosmosFallbackProvider, } +/// Incremental Tree +/// +/// contains the branch and count of the tree #[derive(serde::Deserialize, Clone, Debug)] pub struct IncrementalTree { - pub branch: Vec, // base64 encoded + /// base64 encoded strings + pub branch: [String; 32], + /// leaf count pub count: usize, } +/// Mailbox +/// +/// contains the mailbox information #[derive(serde::Deserialize, Clone, Debug)] pub struct Mailbox { + /// 32 byte hex string pub id: String, + /// bech32 encoded address pub creator: String, + /// number of messages sent pub message_sent: usize, + /// number of messages received pub message_received: usize, + /// 32 byte hex string, address of the default ism pub default_ism: String, + /// cucrrent incremental tree of the mailbox pub tree: IncrementalTree, } +/// MultiSig ISM +/// +/// contains the validator public keys and threshold #[derive(serde::Deserialize, Clone, Debug)] pub struct MultiSig { + /// ethereum addresses of the validators pub validator_pub_keys: Vec, + /// threshold for the multi sig to be valid pub threshold: usize, } +/// ISM Types +/// +/// There are multiple ISM Types: MultiSigISM and NoOpISM +/// Each ISM has base fields containing its id, creator and ism_type #[derive(serde::Deserialize, Clone, Debug)] #[serde(untagged)] // this is needed because the ISM can be either NoOpISM or MultiSigISM pub enum ISM { + /// Multisig ISM MultiSigISM { + /// 32 byte hex string id: String, + /// bech32 encoded address creator: String, + /// type of ISM (MultiSig) ism_type: usize, + /// MultiSig relevant values like validators and threshold multi_sig: MultiSig, }, + /// NoOp ISM NoOpISM { + /// 32 byte hex string id: String, - ism_type: usize, + /// bech32 encoded address creator: String, + /// type of ISM (NoOp) + ism_type: usize, }, } +/// List of mailboxes #[derive(serde::Deserialize, Clone, Debug)] pub struct MailboxesResponse { mailboxes: Vec, } +/// Mailbox Response #[derive(serde::Deserialize, Clone, Debug)] pub struct MailboxResponse { mailbox: Mailbox, } +/// List of ISM #[derive(serde::Deserialize, Clone, Debug)] pub struct ISMResponse { isms: Vec, } +/// Represents a single warp route configuration +/// +/// Contains information about token bridging routes including +/// identifiers, token types, and destination details #[derive(serde::Deserialize, Clone, Debug)] pub struct WarpRoute { + /// 32 byte hex encoded address pub id: String, + /// bech32 encoded address pub creator: String, + /// Type of token being bridged pub token_type: String, + /// 32 byte hex encoded address pub origin_mailbox: String, + /// Original denomination of the token pub origin_denom: String, + /// Domain ID of the receiver chain pub receiver_domain: usize, + /// 32 byte hex encoded address. NOTE: 20 byte address are padded to with zeros to 32 bytes pub receiver_contract: String, } +/// Response wrapper for warp routes query #[derive(serde::Deserialize, Clone, Debug)] pub struct WarpRoutesResponse { + /// List of available warp routes pub tokens: Vec, } +/// Response containing a count value #[derive(serde::Deserialize, Clone, Debug)] pub struct CountResponse { + /// The count value pub count: u32, } +/// Response indicating message delivery status #[derive(serde::Deserialize, Clone, Debug)] pub struct DeliveredResponse { + /// Whether the message has been delivered pub delivered: bool, } +/// Status information about a Cosmos node #[derive(serde::Deserialize, Clone, Debug)] pub struct NodeStatus { + /// The earliest block height stored in the node #[serde(deserialize_with = "string_to_number")] pub earliest_store_height: usize, + /// Current block height of the node #[serde(deserialize_with = "string_to_number")] pub height: usize, } +/// Response containing an ISM identifier #[derive(serde::Deserialize, Clone, Debug)] pub struct RecipientIsmResponse { + /// The identifier of the ISM. 32 byte hex encoded address ism_id: String, } +/// Response containing the latest checkpoint information #[derive(serde::Deserialize, Clone, Debug)] pub struct LatestCheckpointResponse { - pub root: String, // encoded base64 string + /// The merkle root encoded as a base64 string + pub root: String, + /// leaf count for that checkpoint pub count: u32, } +/// Response containing validator storage locations +/// +/// Contains a list of storage location strings for validators #[derive(serde::Deserialize, Clone, Debug)] pub struct ValidatorStorageLocationsResponse { + /// List of storage locations for the validator storage_locations: Vec, } @@ -131,7 +201,7 @@ impl BlockNumberGetter for RestClient { let url = self.url.to_owned() + "cosmos/base/node/v1beta1/status"; let response = reqwest::get(url.clone()) .await - .map_err(Into::::into)?; + .map_err(HyperlaneCosmosError::from)?; let result: Result = response.json().await; match result { Ok(result) => Ok(result.height as u64), @@ -145,7 +215,7 @@ impl BlockNumberGetter for RestClient { } impl RestProvider { - #[doc = "todo"] + /// Returns a new Rest Provider pub fn new(urls: impl IntoIterator) -> RestProvider { let provider = FallbackProvider::new(urls.into_iter().map(|url| RestClient { url })); RestProvider { @@ -162,17 +232,17 @@ impl RestProvider { let reorg_period = reorg_period.clone(); let path = path.to_owned(); let future = async move { - let final_url = client.url.to_string() + "hyperlane/" + &path; + let final_url = client.url.to_string() + "hyperlane/v1/" + &path; let request = reqwest::Client::new(); let response = match reorg_period { ReorgPeriod::None => request .get(final_url.clone()) .send() .await - .map_err(Into::::into)?, + .map_err(HyperlaneCosmosError::from)?, ReorgPeriod::Blocks(non_zero) => { let remote_height = client.get_block_number().await?; - if non_zero.get() as u64 > remote_height { + if (non_zero.get() as u64) > remote_height { return Err(ChainCommunicationError::InvalidRequest { msg: "reorg period can not be greater than block height." .to_string(), @@ -184,7 +254,7 @@ impl RestProvider { .header("x-cosmos-block-height", delta) .send() .await - .map_err(Into::::into)? + .map_err(HyperlaneCosmosError::from)? } ReorgPeriod::Tag(_) => todo!(), }; @@ -206,34 +276,33 @@ impl RestProvider { /// list of all mailboxes deployed pub async fn mailboxes(&self, reorg_period: ReorgPeriod) -> ChainResult> { - let mailboxes: MailboxesResponse = self.get("mailbox/v1/mailboxes", reorg_period).await?; + let mailboxes: MailboxesResponse = self.get("mailboxes", reorg_period).await?; Ok(mailboxes.mailboxes) } /// list of all mailboxes deployed pub async fn mailbox(&self, id: H256, reorg_period: ReorgPeriod) -> ChainResult { - let mailboxes: MailboxResponse = self - .get(&format!("mailbox/v1/mailboxes/{id:?}"), reorg_period) - .await?; + let mailboxes: MailboxResponse = + self.get(&format!("mailboxes/{id:?}"), reorg_period).await?; Ok(mailboxes.mailbox) } /// list of all isms pub async fn isms(&self, reorg_period: ReorgPeriod) -> ChainResult> { - let isms: ISMResponse = self.get("ism/v1/isms", reorg_period).await?; + let isms: ISMResponse = self.get("isms", reorg_period).await?; Ok(isms.isms) } /// list of all warp routes pub async fn warp_tokens(&self, reorg_period: ReorgPeriod) -> ChainResult> { - let warp: WarpRoutesResponse = self.get("warp/v1/tokens", reorg_period).await?; + let warp: WarpRoutesResponse = self.get("tokens", reorg_period).await?; Ok(warp.tokens) } /// returns the current leaf count for mailbox pub async fn leaf_count(&self, mailbox: H256, reorg_period: ReorgPeriod) -> ChainResult { let leafs: CountResponse = self - .get(&format!("mailbox/v1/tree/count/{mailbox:?}"), reorg_period) + .get(&format!("mailboxes/{mailbox:?}/tree/count"), reorg_period) .await?; Ok(leafs.count) } @@ -244,15 +313,17 @@ impl RestProvider { .call(move |client| { let mailbox = mailbox.clone(); let future = async move { - let final_url = - &format!("{}/hyperlane/mailbox/v1/tree/count/{mailbox:?}", client.url); + let final_url = &format!( + "{}/hyperlane/v1/mailboxes/{mailbox:?}/tree/count", + client.url + ); let client = reqwest::Client::new(); let response = client .get(final_url.clone()) .header("x-cosmos-block-height", height) .send() .await - .map_err(Into::::into)?; + .map_err(HyperlaneCosmosError::from)?; let result: Result = response.json().await; match result { @@ -272,10 +343,7 @@ impl RestProvider { /// returns if the message id has been delivered pub async fn delivered(&self, message_id: H256) -> ChainResult { let response: DeliveredResponse = self - .get( - &format!("mailbox/v1/delivered/{message_id:?}"), - ReorgPeriod::None, - ) + .get(&format!("delivered/{message_id:?}"), ReorgPeriod::None) .await?; Ok(response.delivered) } @@ -288,7 +356,7 @@ impl RestProvider { ) -> ChainResult { let response: LatestCheckpointResponse = self .get( - &format!("mailbox/v1/tree/latest_checkpoint/{mailbox:?}"), + &format!("mailboxes/{mailbox:?}/tree/latest_checkpoint"), height, ) .await?; @@ -298,10 +366,7 @@ impl RestProvider { /// returns the recipient ism pub async fn recipient_ism(&self, recipient: H256) -> ChainResult { let response: RecipientIsmResponse = self - .get( - &format!("mailbox/v1/recipient_ism/{recipient:?}"), - ReorgPeriod::None, - ) + .get(&format!("recipient_ism/{recipient:?}"), ReorgPeriod::None) .await?; utils::hex_or_base58_to_h256(&response.ism_id).map_err(|e| { HyperlaneCosmosError::AddressError("invalid recipient ism address".to_string()).into() @@ -313,7 +378,7 @@ impl RestProvider { let validator = H160::from(validator); let response: ValidatorStorageLocationsResponse = self .get( - &format!("mailbox/v1/announced_storage_locations/{validator:?}"), + &format!("announced_storage_locations/{validator:?}"), ReorgPeriod::None, ) .await?; diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/rpc.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/rpc.rs new file mode 100644 index 0000000000..c6f89782e7 --- /dev/null +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers/rpc.rs @@ -0,0 +1,376 @@ +use std::{io::Cursor, sync::atomic::AtomicU32}; + +use cosmrs::{ + crypto::PublicKey, + proto::cosmos::{ + auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse}, + bank::v1beta1::{QueryBalanceRequest, QueryBalanceResponse}, + tx::v1beta1::{SimulateRequest, SimulateResponse, TxRaw}, + }, + rpc::HttpClient, + tx::{self, Fee, MessageExt, SequenceNumber, SignDoc, SignerInfo, SignerPublicKey}, + AccountId, Any, Coin, Tx, +}; +use hyperlane_core::{ + h512_to_bytes, + rpc_clients::{BlockNumberGetter, FallbackProvider}, + utils::to_atto, + AccountAddressType, ChainCommunicationError, ChainResult, FixedPointNumber, H256, H512, U256, +}; +use itertools::Itertools; +use prost::Message; +use tendermint::{hash::Algorithm, Hash}; +use tendermint_rpc::{ + client::CompatMode, + endpoint::{ + block::Response as BlockResponse, block_results::Response as BlockResultsResponse, + broadcast::tx_sync, tx::Response as TxResponse, + }, + Client, +}; +use tonic::async_trait; +use tracing::{debug, warn}; + +use crate::{ + ConnectionConf, CosmosAccountId, CosmosAddress, CosmosAmount, HyperlaneCosmosError, Signer, +}; + +use super::{cosmos::CosmosFallbackProvider, MsgRemoteTransfer}; + +#[derive(Debug, Clone)] +struct CosmosHttpClient { + client: HttpClient, +} + +/// RPC Provider for Cosmos +/// +/// Responsible for chain communication +#[derive(Debug, Clone)] +pub struct RpcProvider { + provider: CosmosFallbackProvider, + conf: ConnectionConf, + signer: Option, + gas_price: CosmosAmount, +} + +#[async_trait] +impl BlockNumberGetter for CosmosHttpClient { + async fn get_block_number(&self) -> Result { + let block = self + .client + .latest_block() + .await + .map_err(HyperlaneCosmosError::from)?; + + Ok(block.block.header.height.value()) + } +} + +impl RpcProvider { + /// Returns a new Rpc Provider + pub fn new(conf: ConnectionConf, signer: Option) -> ChainResult { + let clients = conf + .get_rpc_urls() + .iter() + .map(|url| { + tendermint_rpc::Url::try_from(url.to_owned()) + .map_err(ChainCommunicationError::from_other) + .and_then(|url| { + tendermint_rpc::HttpClientUrl::try_from(url) + .map_err(ChainCommunicationError::from_other) + }) + .and_then(|url| { + HttpClient::builder(url) + .compat_mode(CompatMode::latest()) + .build() + .map_err(ChainCommunicationError::from_other) + }) + .map(|client| CosmosHttpClient { client }) + }) + .collect::, _>>()?; + + let provider = FallbackProvider::new(clients); + let provider = CosmosFallbackProvider::new(provider); + let gas_price = CosmosAmount::try_from(conf.get_minimum_gas_price().clone())?; + + Ok(RpcProvider { + provider, + conf, + signer, + gas_price, + }) + } + + /// Get the transaction by hash + pub async fn get_tx(&self, hash: &H512) -> ChainResult { + let hash: H256 = H256::from_slice(&h512_to_bytes(hash)); + + let tendermint_hash = Hash::from_bytes(Algorithm::Sha256, hash.as_bytes()) + .expect("transaction hash should be of correct size"); + + let response = + self.provider + .call(|client| { + let future = async move { + client.client.tx(tendermint_hash, false).await.map_err(|e| { + ChainCommunicationError::from(HyperlaneCosmosError::from(e)) + }) + }; + Box::pin(future) + }) + .await?; + + let received_hash = H256::from_slice(response.hash.as_bytes()); + if received_hash != hash { + return Err(ChainCommunicationError::from_other_str(&format!( + "received incorrect transaction, expected hash: {:?}, received hash: {:?}", + hash, received_hash, + ))); + } + + Ok(response) + } + + /// Get the block by height + pub async fn get_block(&self, height: u32) -> ChainResult { + let response = + self.provider + .call(|client| { + let future = async move { + client.client.block(height).await.map_err(|e| { + ChainCommunicationError::from(HyperlaneCosmosError::from(e)) + }) + }; + Box::pin(future) + }) + .await?; + + Ok(response) + } + + /// Get the block results by height + pub async fn get_block_results(&self, height: u32) -> ChainResult { + let response = + self.provider + .call(|client| { + let future = async move { + client.client.block_results(height).await.map_err(|e| { + ChainCommunicationError::from(HyperlaneCosmosError::from(e)) + }) + }; + Box::pin(future) + }) + .await?; + + Ok(response) + } + + async fn abci_query(&self, path: &str, request: T) -> ChainResult + where + T: Message, + R: Message + std::default::Default, + { + let bytes = request.encode_to_vec(); + let response = self + .provider + .call(|client| { + let bytes = bytes.clone(); + let path = path.to_owned(); + let future = async move { + let query = client + .client + .abci_query(Some(path), bytes, None, false) + .await + .map_err(|e| ChainCommunicationError::from(HyperlaneCosmosError::from(e))); + query + }; + Box::pin(future) + }) + .await?; + + if response.code.is_err() { + return Err(ChainCommunicationError::from_other_str(&format!( + "ABCI query failed: path={}, code={}, log={}", + path, + response.code.value(), + response.log + ))); + } + + // this should always work as the response must have a default value + let response = R::decode(response.value.as_slice()).map_err(HyperlaneCosmosError::from)?; + Ok(response) + } + + /// Returns the denom balance of that address. Will use the denom specified as the canonical asset in the config + pub async fn get_balance(&self, address: String) -> ChainResult { + let response: QueryBalanceResponse = self + .abci_query( + "/cosmos.bank.v1beta1.Query/Balance", + QueryBalanceRequest { + address, + denom: self.conf.get_canonical_asset(), + }, + ) + .await?; + let balance = response + .balance + .ok_or_else(|| ChainCommunicationError::from_other_str("account not present"))?; + + Ok(U256::from_dec_str(&balance.amount)?) + } + + /// Gets a signer, or returns an error if one is not available. + fn get_signer(&self) -> ChainResult<&Signer> { + self.signer + .as_ref() + .ok_or(ChainCommunicationError::SignerUnavailable) + } + + async fn get_account(&self, address: String) -> ChainResult { + let response: QueryAccountResponse = self + .abci_query( + "/cosmos.auth.v1beta1.Query/Account", + QueryAccountRequest { address }, + ) + .await?; + let account = BaseAccount::decode( + response + .account + .ok_or_else(|| ChainCommunicationError::from_other_str("account not present"))? + .value + .as_slice(), + ) + .map_err(HyperlaneCosmosError::from)?; + Ok(account) + } + + /// Get the gas price + pub fn gas_price(&self) -> FixedPointNumber { + self.gas_price.amount.clone() + } + + /// Generates an unsigned SignDoc for a transaction and the Coin amount + /// required to pay for tx fees. + async fn generate_sign_doc( + &self, + msgs: Vec, + gas_limit: u64, + ) -> ChainResult { + // As this function is only used for estimating gas or sending transactions, + // we can reasonably expect to have a signer. + let signer = self.get_signer()?; + let account_info = self.get_account(signer.address.clone()).await?; + + // timeout height of zero means that we do not have a timeout height TODO: double check + let tx_body = tx::Body::new(msgs, String::default(), 0u32); + let signer_info = SignerInfo::single_direct(Some(signer.public_key), account_info.sequence); + + let amount: u128 = (FixedPointNumber::from(gas_limit) * self.gas_price()) + .ceil_to_integer() + .try_into()?; + let fee_coin = Coin::new( + // The fee to pay is the gas limit * the gas price + amount, + self.conf.get_canonical_asset().as_str(), + ) + .map_err(HyperlaneCosmosError::from)?; + + let auth_info = + signer_info.auth_info(Fee::from_amount_and_gas(fee_coin.clone(), gas_limit)); + + let chain_id = self + .conf + .get_chain_id() + .parse() + .map_err(HyperlaneCosmosError::from)?; + + Ok( + SignDoc::new(&tx_body, &auth_info, &chain_id, account_info.account_number) + .map_err(HyperlaneCosmosError::from)?, + ) + } + + /// Estimates the gas that will be used when a transaction with msgs is sent. + /// + /// Note: that simulated result will be multiplied by the gas multiplier in the gas config + pub async fn estimate_gas(&self, msgs: Vec) -> ChainResult { + // Get a sign doc with 0 gas, because we plan to simulate + let sign_doc = self.generate_sign_doc(msgs, 0).await?; + + let raw_tx = TxRaw { + body_bytes: sign_doc.body_bytes, + auth_info_bytes: sign_doc.auth_info_bytes, + signatures: vec![vec![]], + }; + let tx_bytes = raw_tx + .to_bytes() + .map_err(ChainCommunicationError::from_other)?; + + #[allow(deprecated)] + let response: SimulateResponse = self + .abci_query( + "/cosmos.tx.v1beta1.Service/Simulate", + SimulateRequest { tx_bytes, tx: None }, + ) + .await?; + + let gas_used = response + .gas_info + .ok_or(ChainCommunicationError::from_other_str( + "gas info not present", + ))? + .gas_used; + + let gas_estimate = (gas_used as f64 * self.conf.get_gas_multiplier()) as u64; + + Ok(gas_estimate) + } + + /// Sends a transaction and waits for confirmation + /// + /// gas_limit will be automatically set if None is passed + pub async fn send( + &self, + msgs: Vec, + gas_limit: Option, + ) -> ChainResult { + let gas_limit = match gas_limit { + Some(limit) => limit, + None => self.estimate_gas(msgs.clone()).await?, + }; + + let sign_doc = self.generate_sign_doc(msgs, gas_limit).await?; + let signer = self.get_signer()?; + let signed_tx = sign_doc + .sign(&signer.signing_key()?) + .map_err(HyperlaneCosmosError::from)?; + let signed_tx = signed_tx.to_bytes()?; + + self.provider + .call(|client| { + let signed_tx = signed_tx.clone(); + let future = async move { + client + .client + .broadcast_tx_sync(signed_tx) + .await + .map_err(ChainCommunicationError::from_other) + }; + Box::pin(future) + }) + .await + } +} + +#[async_trait] +impl BlockNumberGetter for RpcProvider { + async fn get_block_number(&self) -> Result { + self.provider + .call(|client| { + let future = async move { client.get_block_number().await }; + Box::pin(future) + }) + .await + } +} diff --git a/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs b/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs index 4e4da175b3..65bbc43fe2 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/trait_builder.rs @@ -12,8 +12,6 @@ use hyperlane_core::{ pub struct ConnectionConf { /// API urls to connect to api_urls: Vec, - /// The GRPC urls to connect to - grpc_urls: Vec, /// The RPC url to connect to rpc_urls: Vec, /// The chain ID @@ -26,6 +24,8 @@ pub struct ConnectionConf { /// minimum price set by the validator. /// More details here: https://docs.cosmos.network/main/learn/beginner/gas-fees#antehandler gas_price: RawCosmosAmount, + /// The gas multiplier is used to estimate gas cost. The gas limit of the simulated transaction will be multiplied by this modifier. + gas_multiplier: f64, /// The number of bytes used to represent a contract address. /// Cosmos address lengths are sometimes less than 32 bytes, so this helps to serialize it in /// bech32 with the appropriate length. @@ -85,11 +85,6 @@ pub enum ConnectionConfError { } impl ConnectionConf { - /// Get the GRPC url - pub fn get_grpc_urls(&self) -> Vec { - self.grpc_urls.clone() - } - /// Get the RPC urls pub fn get_rpc_urls(&self) -> Vec { self.rpc_urls.clone() @@ -130,23 +125,27 @@ impl ConnectionConf { self.api_urls.clone() } + /// Returns the gas multiplier from the config. Used to estimate txn costs more reliable + pub fn get_gas_multiplier(&self) -> f64 { + self.gas_multiplier + } + /// Create a new connection configuration #[allow(clippy::too_many_arguments)] pub fn new( - grpc_urls: Vec, rpc_urls: Vec, api_urls: Vec, chain_id: String, bech32_prefix: String, canonical_asset: String, minimum_gas_price: RawCosmosAmount, + gas_multiplier: f64, contract_address_bytes: usize, operation_batch: OperationBatchConfig, native_token: NativeToken, ) -> Self { Self { api_urls, - grpc_urls, rpc_urls, chain_id, bech32_prefix, @@ -155,6 +154,7 @@ impl ConnectionConf { contract_address_bytes, operation_batch, native_token, + gas_multiplier, } } } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs b/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs index 3ca3f3664f..781ba42bb8 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs @@ -10,7 +10,10 @@ use hyperlane_core::{ }; use prost::Message; -use crate::{signers::Signer, ConnectionConf, CosmosNativeProvider, MsgAnnounceValidator}; +use crate::{ + signers::Signer, ConnectionConf, CosmosNativeProvider, HyperlaneCosmosError, + MsgAnnounceValidator, +}; /// A reference to a ValidatorAnnounce contract on some Cosmos chain #[derive(Debug)] @@ -100,12 +103,12 @@ impl ValidatorAnnounce for CosmosNativeValidatorAnnounce { value: announce.encode_to_vec(), }; - let response = self.provider.grpc().send(vec![any_msg], None).await; - let response = response?; + let response = self.provider.rpc().send(vec![any_msg], None).await?; + let tx = TxResponse::decode(response.data).map_err(HyperlaneCosmosError::from)?; Ok(TxOutcome { - transaction_id: H256::from_slice(hex::decode(response.txhash)?.as_slice()).into(), - executed: response.code == 0, - gas_used: U256::from(response.gas_used), + transaction_id: H256::from_slice(response.hash.as_bytes()).into(), + executed: tx.code == 0, + gas_used: tx.gas_used.into(), gas_price: U256::one().try_into()?, }) } diff --git a/rust/main/config/testnet_config.json b/rust/main/config/testnet_config.json index 9c62113037..2101c85f56 100644 --- a/rust/main/config/testnet_config.json +++ b/rust/main/config/testnet_config.json @@ -2111,17 +2111,13 @@ "bech32Prefix": "kyve", "blockExplorers": [], "blocks": { - "confirmations": 2, - "estimateBlockTime": 7, - "reorgPeriod": 5 + "confirmations": 1, + "estimateBlockTime": 1, + "reorgPeriod": 0 }, "canonicalAsset": "tkyve", "chainId": "kyve-local", "contractAddressBytes": 20, - "deployer": { - "name": "Abacus Works", - "url": "https://www.hyperlane.xyz" - }, "displayName": "Cosmos Native 1", "domainId": 75898671, "gasCurrencyCoinGeckoId": "kyve-network", @@ -2129,25 +2125,20 @@ "amount": "0.02", "denom": "tkyve" }, - "grpcUrls": [ - { - "http": "http://127.0.0.1:9090" - } - ], + "gasMultiplier": 2.0, "interchainGasPaymaster": "0x5b97fe8b2b8b2ef118e6540ce2ee38b68ee9d1250d43ac53febef300300c345c", "mailbox": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", "merkleTreeHook": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", "name": "cosmostestnative1", + "index": { + "from": 2068 + }, "nativeToken": { "decimals": 6, "denom": "tkyve", "name": "KYVE", "symbol": "KYVE" }, - "index": { - "from": 2, - "chunk": 10 - }, "protocol": "cosmosnative", "apiUrls": [ { @@ -2159,15 +2150,14 @@ "http": "http://127.0.0.1:26657" } ], - "slip44": 118, "validatorAnnounce": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", "technicalStack": "other", + "isTestnet": true, "signer": { "key": "0x33913dd43a5d5764f7a23da212a8664fc4f5eedc68db35f3eb4a5c4f046b5b51", "prefix": "kyve", "type": "cosmosKey" - }, - "isTestnet": true + } }, "cosmostestnative2": { "bech32Prefix": "kyve", @@ -2180,10 +2170,6 @@ "canonicalAsset": "tkyve", "chainId": "kyve-local", "contractAddressBytes": 20, - "deployer": { - "name": "Abacus Works", - "url": "https://www.hyperlane.xyz" - }, "displayName": "Cosmos Native 2", "domainId": 75898670, "gasCurrencyCoinGeckoId": "kyve-network", @@ -2191,11 +2177,6 @@ "amount": "0.02", "denom": "tkyve" }, - "grpcUrls": [ - { - "http": "http://127.0.0.1:9090" - } - ], "interchainGasPaymaster": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", "mailbox": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", "merkleTreeHook": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", @@ -2217,18 +2198,8 @@ "http": "http://127.0.0.1:26657" } ], - "index": { - "from": 2, - "chunk": 10 - }, - "slip44": 118, "validatorAnnounce": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", "technicalStack": "other", - "signer": { - "key": "0x33913dd43a5d5764f7a23da212a8664fc4f5eedc68db35f3eb4a5c4f046b5b51", - "prefix": "kyve", - "type": "cosmosKey" - }, "isTestnet": true }, "kyvealpha": { @@ -2242,10 +2213,6 @@ "canonicalAsset": "ukyve", "chainId": "kyve-alpha", "contractAddressBytes": 20, - "deployer": { - "name": "Abacus Works", - "url": "https://www.hyperlane.xyz" - }, "displayName": "KYVEAlpha", "domainId": 75898669, "gasCurrencyCoinGeckoId": "kyve-network", @@ -2253,11 +2220,6 @@ "amount": "0.02", "denom": "ukyve" }, - "grpcUrls": [ - { - "http": "https://grpc.alpha.kyve.network" - } - ], "interchainGasPaymaster": "0x5b97fe8b2b8b2ef118e6540ce2ee38b68ee9d1250d43ac53febef300300c345c", "mailbox": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", "merkleTreeHook": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", @@ -2279,17 +2241,8 @@ "http": "http://3.75.39.208:26670" } ], - "index": { - "from": 9525962 - }, - "slip44": 118, "validatorAnnounce": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", "technicalStack": "other", - "signer": { - "key": "0x33913dd43a5d5764f7a23da212a8664fc4f5eedc68db35f3eb4a5c4f046b5b51", - "prefix": "kyve", - "type": "cosmosKey" - }, "isTestnet": true }, "moonbasealpha": { @@ -2332,42 +2285,6 @@ "interchainGasPaymaster": "0x046fBeDf08f313FF1134069F45c0991dd730b25a", "mailbox": "0x046fBeDf08f313FF1134069F45c0991dd730b25a" }, - "local1": { - "chainId": 31337, - "displayName": "Local1", - "domainId": 31337, - "isTestnet": true, - "name": "local1", - "nativeToken": { - "decimals": 18, - "name": "Ether", - "symbol": "ETH" - }, - "protocol": "ethereum", - "rpcUrls": [ - { - "http": "http://localhost:8545" - } - ], - "staticMerkleRootMultisigIsmFactory": "0x5FbDB2315678afecb367f032d93F642f64180aa3", - "staticMessageIdMultisigIsmFactory": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", - "staticAggregationIsmFactory": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", - "staticAggregationHookFactory": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", - "domainRoutingIsmFactory": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - "staticMerkleRootWeightedMultisigIsmFactory": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", - "staticMessageIdWeightedMultisigIsmFactory": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "proxyAdmin": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - "mailbox": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", - "interchainAccountRouter": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", - "interchainAccountIsm": "0x9A676e781A523b5d0C0e43731313A708CB607508", - "validatorAnnounce": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", - "testRecipient": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", - "merkleTreeHook": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", - "interchainGasPaymaster": "0x0000000000000000000000000000000000000000", - "index": { - "from": 0 - } - }, "sonicsvmtestnet": { "blockExplorers": [ { @@ -2541,6 +2458,42 @@ "index": { "from": 13357661 } + }, + "local1": { + "chainId": 31337, + "displayName": "Local1", + "domainId": 31337, + "isTestnet": true, + "name": "local1", + "nativeToken": { + "decimals": 18, + "name": "Ether", + "symbol": "ETH" + }, + "protocol": "ethereum", + "rpcUrls": [ + { + "http": "http://localhost:8545" + } + ], + "staticMerkleRootMultisigIsmFactory": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "staticMessageIdMultisigIsmFactory": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "staticAggregationIsmFactory": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "staticAggregationHookFactory": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "domainRoutingIsmFactory": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "staticMerkleRootWeightedMultisigIsmFactory": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "staticMessageIdWeightedMultisigIsmFactory": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "proxyAdmin": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "mailbox": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "interchainAccountRouter": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", + "interchainAccountIsm": "0x9A676e781A523b5d0C0e43731313A708CB607508", + "validatorAnnounce": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", + "testRecipient": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", + "merkleTreeHook": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", + "interchainGasPaymaster": "0x0000000000000000000000000000000000000000", + "index": { + "from": 0 + } } }, "defaultRpcConsensusType": "fallback" diff --git a/rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/backward.rs b/rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/backward.rs index d7c9396ca9..4739851bf1 100644 --- a/rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/backward.rs +++ b/rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/backward.rs @@ -123,10 +123,15 @@ impl BackwardSequenceAware &self, current_indexing_snapshot: &TargetSnapshot, ) -> RangeInclusive { - // Query the block range ending at the current_indexing_snapshot's at_block. - current_indexing_snapshot + let start = current_indexing_snapshot .at_block - .saturating_sub(self.chunk_size)..=current_indexing_snapshot.at_block + .saturating_sub(self.chunk_size); + // limit this to 1 + if start < 1 { + return 1..=current_indexing_snapshot.at_block; + } + // Query the block range ending at the current_indexing_snapshot's at_block. + start..=current_indexing_snapshot.at_block } /// Gets the next sequence range to index. diff --git a/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs b/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs index 4a095e60d2..af095d3aba 100644 --- a/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs +++ b/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs @@ -167,8 +167,6 @@ pub fn build_cosmos_native_connection_conf( operation_batch: OperationBatchConfig, ) -> Option { let mut local_err = ConfigParsingError::default(); - let grpcs = - parse_base_and_override_urls(chain, "grpcUrls", "customGrpcUrls", "http", &mut local_err); let apis = parse_base_and_override_urls(chain, "apiUrls", "customApiUrls", "http", &mut local_err); @@ -218,6 +216,13 @@ pub fn build_cosmos_native_connection_conf( .and_then(parse_cosmos_gas_price) .end(); + let gas_multiplier = chain + .chain(err) + .get_opt_key("gasMultiplier") + .parse_f64() + .end() + .unwrap_or(1.4); + let contract_address_bytes = chain .chain(err) .get_opt_key("contractAddressBytes") @@ -238,13 +243,13 @@ pub fn build_cosmos_native_connection_conf( Some(ChainConnectionConf::CosmosNative( h_cosmos_native::ConnectionConf::new( - grpcs, rpcs.to_owned(), apis, chain_id.unwrap().to_string(), prefix.unwrap().to_string(), canonical_asset.unwrap(), gas_price, + gas_multiplier, contract_address_bytes.unwrap().try_into().unwrap(), operation_batch, native_token, diff --git a/rust/main/utils/run-locally/src/config.rs b/rust/main/utils/run-locally/src/config.rs index c03f859ba8..0b78b05109 100644 --- a/rust/main/utils/run-locally/src/config.rs +++ b/rust/main/utils/run-locally/src/config.rs @@ -16,6 +16,7 @@ impl Config { let ci_mode = env::var("E2E_CI_MODE") .map(|k| k.parse::().unwrap()) .unwrap_or_default(); + Arc::new(Self { ci_mode, is_ci_env: env::var("CI").as_deref() == Ok("true"), From 8cc2a91f9207eb49b1ff69905c392b86e3b4d193 Mon Sep 17 00:00:00 2001 From: yjamin Date: Tue, 4 Feb 2025 16:53:25 +0100 Subject: [PATCH 3/4] update: E2E Tests --- rust/main/Cargo.lock | 1 + .../m20230309_000001_create_table_domain.rs | 16 + .../src/indexers/delivery.rs | 5 +- .../src/indexers/dispatch.rs | 5 +- .../src/indexers/gas_paymaster.rs | 2 +- .../src/indexers/tree_insertion.rs | 2 +- .../hyperlane-cosmos-native/src/mailbox.rs | 2 +- .../src/providers/cosmos.rs | 45 +- .../src/providers/rpc.rs | 2 +- .../src/validator_announce.rs | 2 +- rust/main/config/testnet_config.json | 171 ------ .../src/contract_sync/cursors/mod.rs | 2 +- .../src/settings/parser/connection_parser.rs | 2 +- rust/main/hyperlane-core/src/chain.rs | 4 +- rust/main/utils/run-locally/Cargo.toml | 2 + .../utils/run-locally/src/cosmosnative/cli.rs | 223 ++++++++ .../run-locally/src/cosmosnative/constants.rs | 17 + .../utils/run-locally/src/cosmosnative/mod.rs | 497 ++++++++++++++++++ .../run-locally/src/cosmosnative/types.rs | 115 ++++ rust/main/utils/run-locally/src/main.rs | 1 + 20 files changed, 923 insertions(+), 193 deletions(-) create mode 100644 rust/main/utils/run-locally/src/cosmosnative/cli.rs create mode 100644 rust/main/utils/run-locally/src/cosmosnative/constants.rs create mode 100644 rust/main/utils/run-locally/src/cosmosnative/mod.rs create mode 100644 rust/main/utils/run-locally/src/cosmosnative/types.rs diff --git a/rust/main/Cargo.lock b/rust/main/Cargo.lock index 4b7b94d7c8..fe8c6f83f1 100644 --- a/rust/main/Cargo.lock +++ b/rust/main/Cargo.lock @@ -7462,6 +7462,7 @@ dependencies = [ "hyperlane-base", "hyperlane-core", "hyperlane-cosmos", + "hyperlane-cosmos-native", "hyperlane-cosmwasm-interface", "jobserver", "k256 0.13.4", diff --git a/rust/main/agents/scraper/migration/src/m20230309_000001_create_table_domain.rs b/rust/main/agents/scraper/migration/src/m20230309_000001_create_table_domain.rs index 606a16c5da..705d01e8f5 100644 --- a/rust/main/agents/scraper/migration/src/m20230309_000001_create_table_domain.rs +++ b/rust/main/agents/scraper/migration/src/m20230309_000001_create_table_domain.rs @@ -497,6 +497,22 @@ const DOMAINS: &[RawDomain] = &[ is_test_net: true, is_deprecated: false, }, + RawDomain { + name: "cosmostestnative1", + token: "KYVE", + domain: 75898670, + chain_id: 75898670, + is_test_net: true, + is_deprecated: false, + }, + RawDomain { + name: "cosmostestnative2", + token: "KYVE", + domain: 75898671, + chain_id: 75898671, + is_test_net: true, + is_deprecated: false, + }, // ---------- End: E2E tests chains ---------------- ]; diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs index c3b87de0ec..b46e784051 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/delivery.rs @@ -35,10 +35,7 @@ impl CosmosNativeDeliveryIndexer { pub fn new(conf: ConnectionConf, locator: ContractLocator) -> ChainResult { let provider = CosmosNativeProvider::new(locator.domain.clone(), conf, locator, None)?; Ok(CosmosNativeDeliveryIndexer { - indexer: EventIndexer::new( - "hyperlane.mailbox.v1.Process".to_string(), - Arc::new(provider), - ), + indexer: EventIndexer::new("hyperlane.core.v1.Process".to_string(), Arc::new(provider)), }) } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs index 5bae00ba5c..0ca11cd631 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/dispatch.rs @@ -41,10 +41,7 @@ impl CosmosNativeDispatchIndexer { let provider = Arc::new(provider); Ok(CosmosNativeDispatchIndexer { - indexer: EventIndexer::new( - "hyperlane.mailbox.v1.Dispatch".to_string(), - provider.clone(), - ), + indexer: EventIndexer::new("hyperlane.core.v1.Dispatch".to_string(), provider.clone()), provider, address: locator.address, }) diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs index ca5be802e8..32e13602b2 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/gas_paymaster.rs @@ -46,7 +46,7 @@ impl CosmosNativeGasPaymaster { CosmosNativeProvider::new(locator.domain.clone(), conf.clone(), locator.clone(), None)?; Ok(CosmosNativeGasPaymaster { indexer: EventIndexer::new( - "hyperlane.mailbox.v1.GasPayment".to_string(), + "hyperlane.core.v1.GasPayment".to_string(), Arc::new(provider), ), address: locator.address.clone(), diff --git a/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs b/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs index e28dab2fe7..7a1462463d 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/indexers/tree_insertion.rs @@ -40,7 +40,7 @@ impl CosmosNativeTreeInsertionIndexer { let provider = Arc::new(provider); Ok(CosmosNativeTreeInsertionIndexer { indexer: EventIndexer::new( - "hyperlane.mailbox.v1.InsertedIntoTree".to_string(), + "hyperlane.core.v1.InsertedIntoTree".to_string(), provider.clone(), ), provider, diff --git a/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs b/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs index ababdff0b7..b0b0063964 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/mailbox.rs @@ -56,7 +56,7 @@ impl CosmosNativeMailbox { relayer: signer, }; Any { - type_url: "/hyperlane.mailbox.v1.MsgProcessMessage".to_string(), + type_url: "/hyperlane.core.v1.MsgProcessMessage".to_string(), value: process.encode_to_vec(), } } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs index cb24b311ab..0ad8f0252f 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs @@ -24,8 +24,9 @@ use hyperlane_core::{ rpc_clients::{BlockNumberGetter, FallbackProvider}, utils::{self, to_atto}, AccountAddressType, BlockInfo, ChainCommunicationError, ChainInfo, ChainResult, - ContractLocator, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, HyperlaneProviderError, - LogMeta, ModuleType, TxnInfo, TxnReceiptInfo, H256, H512, U256, + ContractLocator, HyperlaneChain, HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, + HyperlaneProviderError, LogMeta, ModuleType, RawHyperlaneMessage, TxnInfo, TxnReceiptInfo, + H256, H512, U256, }; use itertools::Itertools; use prost::Message; @@ -148,8 +149,44 @@ impl CosmosNativeProvider { &self.rest } + fn check_msg_process(tx: &Tx) -> ChainResult> { + // check for all transfer messages + let remote_transfers: Vec = tx + .body + .messages + .iter() + .filter(|a| a.type_url == "/hyperlane.core.v1.MsgProcessMessage") + .cloned() + .collect(); + + // right now one transaction can include max. one transfer + if remote_transfers.len() > 1 { + let msg = "transaction contains multiple execution messages"; + Err(HyperlaneCosmosError::ParsingFailed(msg.to_owned()))? + } + + let msg = remote_transfers.first(); + match msg { + Some(msg) => { + let result = MsgProcessMessage::decode(msg.value.as_slice()) + .map_err(HyperlaneCosmosError::from)?; + let message: RawHyperlaneMessage = hex::decode(result.message)?; + let message = HyperlaneMessage::from(message); + Ok(Some(message.recipient)) + } + None => Ok(None), + } + } + // extract the contract address from the tx + // the tx is either a MsgPorcessMessage on the destination or a MsgRemoteTransfer on the origin + // we check for both tx types, if both are missing or an error occured while parsing we return the error fn contract(tx: &Tx) -> ChainResult { + // first check for the process message + if let Some(recipient) = Self::check_msg_process(tx)? { + return Ok(recipient); + } + // check for all transfer messages let remote_transfers: Vec = tx .body @@ -170,9 +207,7 @@ impl CosmosNativeProvider { })?; let result = MsgRemoteTransfer::decode(msg.value.as_slice()).map_err(HyperlaneCosmosError::from)?; - - let recipient = result.recipient; - let recipient: H256 = recipient.parse()?; + let recipient: H256 = result.recipient.parse()?; Ok(recipient) } diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/rpc.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/rpc.rs index c6f89782e7..6396b31b39 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/providers/rpc.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers/rpc.rs @@ -197,7 +197,6 @@ impl RpcProvider { ))); } - // this should always work as the response must have a default value let response = R::decode(response.value.as_slice()).map_err(HyperlaneCosmosError::from)?; Ok(response) } @@ -342,6 +341,7 @@ impl RpcProvider { let sign_doc = self.generate_sign_doc(msgs, gas_limit).await?; let signer = self.get_signer()?; + let signed_tx = sign_doc .sign(&signer.signing_key()?) .map_err(HyperlaneCosmosError::from)?; diff --git a/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs b/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs index 781ba42bb8..81f754d2c2 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/validator_announce.rs @@ -99,7 +99,7 @@ impl ValidatorAnnounce for CosmosNativeValidatorAnnounce { }; let any_msg = Any { - type_url: "/hyperlane.mailbox.v1.MsgAnnounceValidator".to_string(), + type_url: "/hyperlane.core.v1.MsgAnnounceValidator".to_string(), value: announce.encode_to_vec(), }; diff --git a/rust/main/config/testnet_config.json b/rust/main/config/testnet_config.json index 2101c85f56..6a7856d9cc 100644 --- a/rust/main/config/testnet_config.json +++ b/rust/main/config/testnet_config.json @@ -2107,101 +2107,6 @@ "from": 86008 } }, - "cosmostestnative1": { - "bech32Prefix": "kyve", - "blockExplorers": [], - "blocks": { - "confirmations": 1, - "estimateBlockTime": 1, - "reorgPeriod": 0 - }, - "canonicalAsset": "tkyve", - "chainId": "kyve-local", - "contractAddressBytes": 20, - "displayName": "Cosmos Native 1", - "domainId": 75898671, - "gasCurrencyCoinGeckoId": "kyve-network", - "gasPrice": { - "amount": "0.02", - "denom": "tkyve" - }, - "gasMultiplier": 2.0, - "interchainGasPaymaster": "0x5b97fe8b2b8b2ef118e6540ce2ee38b68ee9d1250d43ac53febef300300c345c", - "mailbox": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", - "merkleTreeHook": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", - "name": "cosmostestnative1", - "index": { - "from": 2068 - }, - "nativeToken": { - "decimals": 6, - "denom": "tkyve", - "name": "KYVE", - "symbol": "KYVE" - }, - "protocol": "cosmosnative", - "apiUrls": [ - { - "http": "http://127.0.0.1:1317" - } - ], - "rpcUrls": [ - { - "http": "http://127.0.0.1:26657" - } - ], - "validatorAnnounce": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", - "technicalStack": "other", - "isTestnet": true, - "signer": { - "key": "0x33913dd43a5d5764f7a23da212a8664fc4f5eedc68db35f3eb4a5c4f046b5b51", - "prefix": "kyve", - "type": "cosmosKey" - } - }, - "cosmostestnative2": { - "bech32Prefix": "kyve", - "blockExplorers": [], - "blocks": { - "confirmations": 2, - "estimateBlockTime": 7, - "reorgPeriod": 5 - }, - "canonicalAsset": "tkyve", - "chainId": "kyve-local", - "contractAddressBytes": 20, - "displayName": "Cosmos Native 2", - "domainId": 75898670, - "gasCurrencyCoinGeckoId": "kyve-network", - "gasPrice": { - "amount": "0.02", - "denom": "tkyve" - }, - "interchainGasPaymaster": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", - "mailbox": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", - "merkleTreeHook": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", - "name": "cosmostestnative2", - "nativeToken": { - "decimals": 6, - "denom": "tkyve", - "name": "KYVE", - "symbol": "KYVE" - }, - "protocol": "cosmosnative", - "apiUrls": [ - { - "http": "http://127.0.0.1:1317" - } - ], - "rpcUrls": [ - { - "http": "http://127.0.0.1:26657" - } - ], - "validatorAnnounce": "0xe81bf6f262305f49f318d68f33b04866f092ffdb2ecf9c98469b4a8b829f65e4", - "technicalStack": "other", - "isTestnet": true - }, "kyvealpha": { "bech32Prefix": "kyve", "blockExplorers": [], @@ -2245,46 +2150,6 @@ "technicalStack": "other", "isTestnet": true }, - "moonbasealpha": { - "blockExplorers": [ - { - "apiUrl": "https://api-moonbase.moonscan.io/api", - "family": "etherscan", - "name": "MoonScan", - "url": "https://moonbase.moonscan.io" - } - ], - "blocks": { - "confirmations": 1, - "estimateBlockTime": 12, - "reorgPeriod": 1 - }, - "chainId": 1287, - "displayName": "Moonbase Alpha", - "displayNameShort": "Moonbase", - "domainId": 1287, - "isTestnet": true, - "index": { - "from": 10463874, - "chunk": 999 - }, - "name": "moonbasealpha", - "nativeToken": { - "decimals": 18, - "name": "DEV", - "symbol": "DEV" - }, - "protocol": "ethereum", - "rpcUrls": [ - { - "http": "https://rpc.api.moonbase.moonbeam.network" - } - ], - "merkleTreeHook": "0xF43AF3c413ba00f3BD933aa3061FA8db86e5b057", - "validatorAnnounce": "0xDbe1f8D6DD161d1309247665E50a742949d419c1", - "interchainGasPaymaster": "0x046fBeDf08f313FF1134069F45c0991dd730b25a", - "mailbox": "0x046fBeDf08f313FF1134069F45c0991dd730b25a" - }, "sonicsvmtestnet": { "blockExplorers": [ { @@ -2458,42 +2323,6 @@ "index": { "from": 13357661 } - }, - "local1": { - "chainId": 31337, - "displayName": "Local1", - "domainId": 31337, - "isTestnet": true, - "name": "local1", - "nativeToken": { - "decimals": 18, - "name": "Ether", - "symbol": "ETH" - }, - "protocol": "ethereum", - "rpcUrls": [ - { - "http": "http://localhost:8545" - } - ], - "staticMerkleRootMultisigIsmFactory": "0x5FbDB2315678afecb367f032d93F642f64180aa3", - "staticMessageIdMultisigIsmFactory": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", - "staticAggregationIsmFactory": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", - "staticAggregationHookFactory": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", - "domainRoutingIsmFactory": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - "staticMerkleRootWeightedMultisigIsmFactory": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", - "staticMessageIdWeightedMultisigIsmFactory": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "proxyAdmin": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - "mailbox": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", - "interchainAccountRouter": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", - "interchainAccountIsm": "0x9A676e781A523b5d0C0e43731313A708CB607508", - "validatorAnnounce": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", - "testRecipient": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", - "merkleTreeHook": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", - "interchainGasPaymaster": "0x0000000000000000000000000000000000000000", - "index": { - "from": 0 - } } }, "defaultRpcConsensusType": "fallback" diff --git a/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs b/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs index 58de29d2da..a3f92b22d8 100644 --- a/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs +++ b/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs @@ -91,7 +91,7 @@ impl Indexable for Delivery { HyperlaneDomainProtocol::Fuel => todo!(), HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::RateLimited, - HyperlaneDomainProtocol::CosmosNative => CursorType::SequenceAware, + HyperlaneDomainProtocol::CosmosNative => CursorType::RateLimited, } } diff --git a/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs b/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs index af095d3aba..e65cf18a64 100644 --- a/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs +++ b/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs @@ -221,7 +221,7 @@ pub fn build_cosmos_native_connection_conf( .get_opt_key("gasMultiplier") .parse_f64() .end() - .unwrap_or(1.4); + .unwrap_or(1.8); let contract_address_bytes = chain .chain(err) diff --git a/rust/main/hyperlane-core/src/chain.rs b/rust/main/hyperlane-core/src/chain.rs index 30401de987..688c4eac25 100644 --- a/rust/main/hyperlane-core/src/chain.rs +++ b/rust/main/hyperlane-core/src/chain.rs @@ -193,8 +193,8 @@ pub enum KnownHyperlaneDomain { SealevelTest2 = 13376, CosmosTest99990 = 99990, CosmosTest99991 = 99991, - CosmosTestNative1 = 75898671, - CosmosTestNative2 = 75898670, + CosmosTestNative1 = 75898670, + CosmosTestNative2 = 75898671, // -- Test chains -- // diff --git a/rust/main/utils/run-locally/Cargo.toml b/rust/main/utils/run-locally/Cargo.toml index 9dedae9cea..9fb4521a5c 100644 --- a/rust/main/utils/run-locally/Cargo.toml +++ b/rust/main/utils/run-locally/Cargo.toml @@ -11,6 +11,7 @@ version.workspace = true hyperlane-base = { path = "../../hyperlane-base" } hyperlane-core = { path = "../../hyperlane-core", features = ["float"] } hyperlane-cosmos = { path = "../../chains/hyperlane-cosmos" } +hyperlane-cosmos-native = { path = "../../chains/hyperlane-cosmos-native" } toml_edit.workspace = true k256.workspace = true jobserver.workspace = true @@ -43,3 +44,4 @@ vergen = { version = "8.3.2", features = ["build", "git", "gitcl"] } [features] cosmos = [] +cosmosnative = [] diff --git a/rust/main/utils/run-locally/src/cosmosnative/cli.rs b/rust/main/utils/run-locally/src/cosmosnative/cli.rs new file mode 100644 index 0000000000..02392941a6 --- /dev/null +++ b/rust/main/utils/run-locally/src/cosmosnative/cli.rs @@ -0,0 +1,223 @@ +use std::{fs, path::PathBuf, thread::sleep, time::Duration}; + +use crate::{ + log, + program::Program, + utils::{concat_path, AgentHandles, TaskHandle}, +}; + +use super::{ + constants::{CHAIN_ID, DENOM, KEY_CHAIN_VALIDATOR}, + types::Contracts, +}; + +const GENESIS_FUND: u128 = 1000000000000; + +#[derive(Debug)] +pub struct SimApp { + pub(crate) bin: String, + pub(crate) home: String, + pub(crate) addr: String, + pub(crate) p2p_addr: String, + pub(crate) rpc_addr: String, + pub(crate) api_addr: String, + pub(crate) pprof_addr: String, +} + +pub(crate) fn modify_json( + file: impl Into, + modifier: Box, +) { + let path = file.into(); + let mut config: T = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); + + modifier(&mut config); + + fs::write(path, serde_json::to_string_pretty(&config).unwrap()).unwrap(); +} + +/// Sim app +/// +/// the sim app is a light cosmos chain that implementes the hyperlane cosmos module +impl SimApp { + pub fn new(bin: String, home: String, port_offset: u32) -> Self { + let port_base = 26657 + port_offset * 5; // we increment by 5 ports as we need 5 unique ports per chain + let addr_base = "tcp://127.0.01"; + + let mut next_port = port_base; + let mut get_next_addr = || { + let port = next_port; + next_port += 1; + format!("{addr_base}:{port}") + }; + + let addr = get_next_addr(); + let p2p_addr = get_next_addr(); + let rpc_addr = get_next_addr(); + let api_addr = get_next_addr(); + let pprof_addr = get_next_addr().replace("tcp://", ""); + + return SimApp { + bin, + home, + addr, + rpc_addr, + p2p_addr, + pprof_addr, + api_addr, + }; + } + + fn cli(&self) -> Program { + Program::new(self.bin.clone()).arg("home", self.home.clone()) + } + + pub fn init(&self, domain: u32) { + self.cli().cmd("init-sample-chain").run().join(); + + // set the local domain + let client_config_path = concat_path(&self.home, "config/genesis.json"); + modify_json::( + client_config_path, + Box::new(move |config| { + config["app_state"]["hyperlane"]["params"]["domain"] = serde_json::json!(domain); + }), + ); + } + + pub fn start(&mut self) -> AgentHandles { + let node = self + .cli() + .cmd("start") + .arg("address", &self.addr) // default is tcp://0.0.0.0:26658 + .arg("p2p.laddr", &self.p2p_addr) // default is tcp://0.0.0.0:26655 + .arg("rpc.laddr", &self.rpc_addr) // default is tcp://0.0.0.0:26657 + .cmd("--grpc.enable=false") // disable grpc + .flag("api.enable") // enable api + .arg("api.address", &self.api_addr) + .arg("rpc.pprof_laddr", &self.pprof_addr) // default is localhost:6060 + .arg("log_level", "panic") + .spawn("SIMAPP", None); + sleep(Duration::from_secs(2)); + node + } + + fn tx<'a>(&self, args: impl IntoIterator) { + let mut program = Program::new(self.bin.clone()).cmd("tx"); + for arg in args { + program = program.cmd(arg); + } + program + .arg("from", KEY_CHAIN_VALIDATOR.0) + .arg("chain-id", CHAIN_ID) + .arg("fees", format!("40000{}", DENOM)) + .arg("node", &self.rpc_addr) + .arg("home", &self.home) + .arg("keyring-backend", "test") + .flag("yes") + .run() + .join(); + sleep(Duration::from_secs(1)); // wait for the block to mined + } + + pub fn remote_transfer(&self, from: &str, warp_route: &str, recipient: &str, amount: u32) { + Program::new(self.bin.clone()) + .cmd("tx") + .cmd("hyperlane-transfer") + .cmd("transfer") + .cmd(warp_route) + .cmd(recipient) + .cmd(&format!("{amount}")) + .arg("gas-limit", "800000") + .arg("max-hyperlane-fee", "1000000") + .arg("from", from) + .arg("chain-id", CHAIN_ID) + .arg("fees", format!("80000{}", DENOM)) + .arg("node", &self.rpc_addr) + .arg("home", &self.home) + .arg("keyring-backend", "test") + .arg("gas", "400000") + .flag("yes") + .run() + .join(); + sleep(Duration::from_secs(1)); // wait for the block to mined + } + + pub fn deploy(&self, destination_domain: &str) -> Contracts { + log!("deploying hyperlane for domain: {} ...", destination_domain); + + // create interchain gas paymaster + // the igp address expected to be: 0xd7194459d45619d04a5a0f9e78dc9594a0f37fd6da8382fe12ddda6f2f46d647 + // TODO: test against the tx result to see if everything was created correctly + self.tx(vec!["hyperlane", "igp", "create-igp", DENOM]); + + // set the interchain gas config -> this determins the interchain gaspayments + // cmd is following: igp-address remote-domain exchange-rate gas-price and gas-overhead + // this config requires a payment of at least 0.200001uhyp + self.tx(vec![ + "hyperlane", + "igp", + "set-destination-gas-config", + "0xd7194459d45619d04a5a0f9e78dc9594a0f37fd6da8382fe12ddda6f2f46d647", + destination_domain, + "1", + "1", + "200000", + ]); + + // create ism + // cmd is following: validator addresses threshold + // expected ism address: 0x934b867052ca9c65e33362112f35fb548f8732c2fe45f07b9c591b38e865def0 + let address = "0xb05b6a0aa112b61a7aa16c19cac27d970692995e"; // TODO: convert KEY_VALIDATOR to eth address + self.tx(vec![ + "hyperlane", + "ism", + "create-multisig-ism", + &address, + "1", + ]); + + // create mailbox + // cmd is following: default-ism default-igp + // expected mailbox address: 0x8ba32dc5efa59ba35e2cf6f541dfacbbf49c95891e5afc2c9ca36142de8fb880 + self.tx(vec![ + "hyperlane", + "mailbox", + "create-mailbox", + "0x934b867052ca9c65e33362112f35fb548f8732c2fe45f07b9c591b38e865def0", + "0xd7194459d45619d04a5a0f9e78dc9594a0f37fd6da8382fe12ddda6f2f46d647", + ]); + + // create warp route + // cmd is following: origin-mailbox denom receiver-domain receiver-contract + // expected address: 0x820e1a4aa659041704df5567a73778be57615a84041680218d18894bec1695b2 + self.tx(vec![ + "hyperlane-transfer", + "create-collateral-token", + "0x8ba32dc5efa59ba35e2cf6f541dfacbbf49c95891e5afc2c9ca36142de8fb880", + DENOM, + destination_domain, + "0xb32677d8121a50c7b960b8561ead86278a7d75ec786807983e1eebfcbc2d9cfc", + ]); + + // create warp route + // cmd is following: origin-mailbox denom receiver-domain receiver-contract + // expected address: 0x820e1a4aa659041704df5567a73778be57615a84041680218d18894bec1695b2 + self.tx(vec![ + "hyperlane-transfer", + "create-synthetic-token", + "0x8ba32dc5efa59ba35e2cf6f541dfacbbf49c95891e5afc2c9ca36142de8fb880", + destination_domain, + "0x820e1a4aa659041704df5567a73778be57615a84041680218d18894bec1695b2", + ]); + + Contracts { + mailbox: "0x8ba32dc5efa59ba35e2cf6f541dfacbbf49c95891e5afc2c9ca36142de8fb880" + .to_owned(), + igp: "0xd7194459d45619d04a5a0f9e78dc9594a0f37fd6da8382fe12ddda6f2f46d647".to_owned(), + tokens: vec![ + "0x820e1a4aa659041704df5567a73778be57615a84041680218d18894bec1695b2".to_owned(), + ], + } + } +} diff --git a/rust/main/utils/run-locally/src/cosmosnative/constants.rs b/rust/main/utils/run-locally/src/cosmosnative/constants.rs new file mode 100644 index 0000000000..9386860579 --- /dev/null +++ b/rust/main/utils/run-locally/src/cosmosnative/constants.rs @@ -0,0 +1,17 @@ +pub const PREFIX: &str = "hyp"; +pub const DENOM: &str = "uhyp"; +pub const CHAIN_ID: &str = "hyperlane-local"; +pub const BINARY_NAME: &str = "hypd"; + +pub const KEY_CHAIN_VALIDATOR: (&str, &str) = ( + "alice", + "0x33913dd43a5d5764f7a23da212a8664fc4f5eedc68db35f3eb4a5c4f046b5b51", +); +pub const KEY_VALIDATOR: (&str, &str) = ( + "bob", + "0x0afcf195989ebb6306f23271e50832332180b73055eb57f6d3c53263127e7d78", +); +pub const KEY_RELAYER: (&str, &str) = ( + "charlie", + "0x8ef41fc20bf963ce18494c0f13e9303f70abc4c1d1ecfdb0a329d7fd468865b8", +); diff --git a/rust/main/utils/run-locally/src/cosmosnative/mod.rs b/rust/main/utils/run-locally/src/cosmosnative/mod.rs new file mode 100644 index 0000000000..59394dcf5f --- /dev/null +++ b/rust/main/utils/run-locally/src/cosmosnative/mod.rs @@ -0,0 +1,497 @@ +#![allow(dead_code)] // TODO: `rustc` 1.80.1 clippy issue + +use std::{ + collections::BTreeMap, + fs, + path::PathBuf, + thread::sleep, + time::{Duration, Instant}, +}; + +use cli::SimApp; +use constants::{BINARY_NAME, KEY_CHAIN_VALIDATOR, KEY_VALIDATOR, PREFIX}; +use macro_rules_attribute::apply; +use maplit::hashmap; +use tempfile::tempdir; +use types::{AgentConfig, AgentConfigOut, Deployment}; + +use crate::{ + fetch_metric, log, + metrics::agent_balance_sum, + program::Program, + utils::{as_task, concat_path, stop_child, AgentHandles, TaskHandle}, + AGENT_BIN_PATH, +}; + +mod cli; +mod constants; +mod types; + +pub struct CosmosNativeStack { + pub validators: Vec, + pub relayer: AgentHandles, + pub scraper: AgentHandles, + pub postgres: AgentHandles, + pub nodes: Vec, +} + +// this is for clean up +// kills all the remaining children +impl Drop for CosmosNativeStack { + fn drop(&mut self) { + stop_child(&mut self.relayer.1); + stop_child(&mut self.scraper.1); + stop_child(&mut self.postgres.1); + self.validators + .iter_mut() + .for_each(|x| stop_child(&mut x.1)); + self.nodes + .iter_mut() + .for_each(|x| stop_child(&mut x.handle.1)); + } +} + +// right now we only test two chains that communicate with eachother +// we send a one uhyp from node1 -> node2, this will result in a wrapped uhyp on node2 +// we send a one uhyp from node2 -> node1, this will result in a wrapped uhyp on node1 +fn dispatch(node1: &Deployment, node2: &Deployment) -> u32 { + // TODO: make this dynamic + // NOTE: we have to pad the address to 32 bytes + // this is the alice address in hex + let account = "0x0000000000000000000000004200dacc2961e425f687ecF7571b5FF32B6Fe808"; + node1.chain.remote_transfer( + KEY_CHAIN_VALIDATOR.0, + "0x820e1a4aa659041704df5567a73778be57615a84041680218d18894bec1695b2", + account, + 1000000u32, + ); + node2.chain.remote_transfer( + KEY_CHAIN_VALIDATOR.0, + "0x820e1a4aa659041704df5567a73778be57615a84041680218d18894bec1695b2", + account, + 1000000u32, + ); + + return 2; +} + +#[apply(as_task)] +fn launch_cosmos_validator( + agent_config: AgentConfig, + agent_config_path: PathBuf, + debug: bool, +) -> AgentHandles { + let validator_bin = concat_path(format!("../../{AGENT_BIN_PATH}"), "validator"); + let validator_base = tempdir().expect("Failed to create a temp dir").into_path(); + let validator_base_db = concat_path(&validator_base, "db"); + + fs::create_dir_all(&validator_base_db).unwrap(); + println!("Validator DB: {:?}", validator_base_db); + + let checkpoint_path = concat_path(&validator_base, "checkpoint"); + let signature_path = concat_path(&validator_base, "signature"); + + let validator = Program::default() + .bin(validator_bin) + .working_dir("../../") + .env("CONFIG_FILES", agent_config_path.to_str().unwrap()) + .env( + "MY_VALIDATOR_SIGNATURE_DIRECTORY", + signature_path.to_str().unwrap(), + ) + .env("RUST_BACKTRACE", "1") + .hyp_env("CHECKPOINTSYNCER_PATH", checkpoint_path.to_str().unwrap()) + .hyp_env("CHECKPOINTSYNCER_TYPE", "localStorage") + .hyp_env("ORIGINCHAINNAME", agent_config.name) + .hyp_env("DB", validator_base_db.to_str().unwrap()) + .hyp_env("METRICSPORT", agent_config.metrics_port.to_string()) + .hyp_env("VALIDATOR_KEY", KEY_VALIDATOR.1) + .hyp_env("DEFAULTSIGNER_KEY", KEY_VALIDATOR.1) + .hyp_env("DEFAULTSIGNER_TYPE", "cosmosKey") + .hyp_env("DEFAULTSIGNER_PREFIX", PREFIX) + .hyp_env("TRACING_LEVEL", if debug { "debug" } else { "info" }) + .spawn("VAL", None); + + validator +} + +#[apply(as_task)] +fn launch_cosmos_relayer( + agent_config_path: String, + relay_chains: Vec, + metrics: u32, + debug: bool, +) -> AgentHandles { + let relayer_bin = concat_path(format!("../../{AGENT_BIN_PATH}"), "relayer"); + let relayer_base = tempdir().unwrap(); + + let relayer = Program::default() + .bin(relayer_bin) + .working_dir("../../") + .env("CONFIG_FILES", agent_config_path) + .env("RUST_BACKTRACE", "1") + .hyp_env("RELAYCHAINS", relay_chains.join(",")) + .hyp_env("DB", relayer_base.as_ref().to_str().unwrap()) + .hyp_env("ALLOWLOCALCHECKPOINTSYNCERS", "true") + .hyp_env("DEFAULTSIGNER_KEY", KEY_VALIDATOR.1) + .hyp_env("DEFAULTSIGNER_TYPE", "cosmosKey") + .hyp_env("DEFAULTSIGNER_PREFIX", PREFIX) + .hyp_env("TRACING_LEVEL", if debug { "debug" } else { "info" }) + .hyp_env( + "GASPAYMENTENFORCEMENT", + r#"[{ + "type": "minimum", + "payment": "1" + }]"#, + ) + .hyp_env("METRICSPORT", metrics.to_string()) + .spawn("RLY", None); + + relayer +} + +#[apply(as_task)] +#[allow(clippy::let_and_return)] // TODO: `rustc` 1.80.1 clippy issue +fn launch_cosmos_scraper( + agent_config_path: String, + chains: Vec, + metrics: u32, + debug: bool, +) -> AgentHandles { + let bin = concat_path(format!("../../{AGENT_BIN_PATH}"), "scraper"); + + let scraper = Program::default() + .bin(bin) + .working_dir("../../") + .env("CONFIG_FILES", agent_config_path) + .env("RUST_BACKTRACE", "1") + .hyp_env("CHAINSTOSCRAPE", chains.join(",")) + .hyp_env( + "DB", + "postgresql://postgres:47221c18c610@localhost:5432/postgres", + ) + .hyp_env("TRACING_LEVEL", if debug { "debug" } else { "info" }) + .hyp_env("METRICSPORT", metrics.to_string()) + .spawn("SCR", None); + + scraper +} + +fn make_target() -> String { + let os = if cfg!(target_os = "linux") { + "linux" + } else if cfg!(target_os = "macos") { + "darwin" + } else { + panic!("Current os is not supported by HypD") + }; + + let arch = if cfg!(target_arch = "aarch64") { + "arm64" + } else { + "amd64" + }; + + format!("{}_{}", os, arch) +} + +fn install_sim_app() -> PathBuf { + let target = make_target(); + + let dir_path = tempdir().unwrap().into_path(); + let dir_path = dir_path.to_str().unwrap(); + + let release_name = format!("{BINARY_NAME}_{target}"); + log!("Downloading Sim App {}", release_name); + let uri = format!( + "https://files.kyve.network/hyperlane/nightly/{}", // TODO: point to offical releases on github, right now we only have this private preview binary of a simulation app + release_name + ); + + Program::new("curl") + .arg("output", BINARY_NAME) + .flag("location") + .cmd(uri) + .flag("silent") + .working_dir(dir_path) + .run() + .join(); + + Program::new("chmod") + .cmd("+x") + .cmd(BINARY_NAME) + .working_dir(dir_path) + .run() + .join(); + + concat_path(dir_path, BINARY_NAME) +} + +#[allow(dead_code)] +fn run_locally() { + let hypd = install_sim_app().as_path().to_str().unwrap().to_string(); + + log!("Building rust..."); + Program::new("cargo") + .cmd("build") + .working_dir("../../") + .arg("features", "test-utils") + .arg("bin", "relayer") + .arg("bin", "validator") + .arg("bin", "scraper") + .arg("bin", "init-db") + .filter_logs(|l| !l.contains("workspace-inheritance")) + .run() + .join(); + + let metrics_port_start = 9090u32; + let domain_start = 75898670u32; + let node_count = 2; // right now this only works with two nodes. + + let nodes = (0..node_count) + .map(|i| { + let node_dir = tempdir().unwrap().path().to_str().unwrap().to_string(); + let mut node = SimApp::new(hypd.to_owned(), node_dir, i); + node.init(domain_start + i); + let handle = node.start(); + let contracts = node.deploy(&format!("{}", domain_start + (i + 1) % node_count)); + Deployment { + chain: node, + domain: domain_start + i, + metrics: metrics_port_start + i, + name: format!("cosmostestnative{}", i + 1), + contracts, + handle, + } + }) + .collect::>(); + + let node1 = &nodes[0]; + let node2 = &nodes[1]; + + // Mostly copy-pasta from `rust/main/utils/run-locally/src/main.rs` + + // count all the dispatched messages + let mut dispatched_messages = 0; + // dispatch the first batch of messages (before agents start) + dispatched_messages += dispatch(node1, node2); + + let config_dir = tempdir().unwrap(); + // export agent config + let agent_config_out = AgentConfigOut { + chains: nodes + .iter() + .map(|v| (v.name.clone(), AgentConfig::new(v))) + .collect::>(), + }; + + let agent_config_path = concat_path(&config_dir, "config.json"); + fs::write( + &agent_config_path, + serde_json::to_string_pretty(&agent_config_out).unwrap(), + ) + .unwrap(); + + log!("Running postgres db..."); + let postgres = Program::new("docker") + .cmd("run") + .flag("rm") + .arg("name", "scraper-testnet-postgres") + .arg("env", "POSTGRES_PASSWORD=47221c18c610") + .arg("publish", "5432:5432") + .cmd("postgres:14") + .spawn("SQL", None); + + sleep(Duration::from_secs(15)); + + log!("Init postgres db..."); + Program::new(concat_path(format!("../../{AGENT_BIN_PATH}"), "init-db")) + .run() + .join(); + + let debug = true; + + let hpl_val = agent_config_out + .chains + .clone() + .into_values() + .map(|agent_config| launch_cosmos_validator(agent_config, agent_config_path.clone(), debug)) + .collect::>(); + + let chains = agent_config_out.chains.into_keys().collect::>(); + let path = agent_config_path.to_str().unwrap(); + + let hpl_rly_metrics_port = metrics_port_start + node_count + 5u32; + let hpl_rly = + launch_cosmos_relayer(path.to_owned(), chains.clone(), hpl_rly_metrics_port, debug); + + let hpl_scr_metrics_port = hpl_rly_metrics_port + 1u32; + let hpl_scr = + launch_cosmos_scraper(path.to_owned(), chains.clone(), hpl_scr_metrics_port, debug); + + // give things a chance to fully start. + sleep(Duration::from_secs(20)); + + let starting_relayer_balance: f64 = agent_balance_sum(hpl_rly_metrics_port).unwrap(); + + // dispatch the second batch of messages (after agents start) + dispatched_messages += dispatch(node1, node2); + + let _stack = CosmosNativeStack { + validators: hpl_val.into_iter().map(|v| v.join()).collect(), + relayer: hpl_rly.join(), + scraper: hpl_scr.join(), + postgres, + nodes, + }; + + // TODO: refactor to share code + let loop_start = Instant::now(); + let mut failure_occurred = false; + const TIMEOUT_SECS: u64 = 60 * 10; + loop { + // look for the end condition. + if termination_invariants_met( + hpl_rly_metrics_port, + hpl_scr_metrics_port, + dispatched_messages, + starting_relayer_balance, + ) + .unwrap_or(false) + { + // end condition reached successfully + break; + } else if (Instant::now() - loop_start).as_secs() > TIMEOUT_SECS { + // we ran out of time + log!("timeout reached before message submission was confirmed"); + failure_occurred = true; + break; + } + + sleep(Duration::from_secs(5)); + } + + if failure_occurred { + panic!("E2E tests failed"); + } else { + log!("E2E tests passed"); + } +} + +fn termination_invariants_met( + relayer_metrics_port: u32, + scraper_metrics_port: u32, + messages_expected: u32, + starting_relayer_balance: f64, +) -> eyre::Result { + let expected_gas_payments = messages_expected; + let gas_payments_event_count = fetch_metric( + &relayer_metrics_port.to_string(), + "hyperlane_contract_sync_stored_events", + &hashmap! {"data_type" => "gas_payment"}, + )? + .iter() + .sum::(); + if gas_payments_event_count != expected_gas_payments { + log!( + "Relayer has indexed {} gas payments, expected {}", + gas_payments_event_count, + expected_gas_payments + ); + return Ok(false); + } + + let msg_processed_count = fetch_metric( + &relayer_metrics_port.to_string(), + "hyperlane_operations_processed_count", + &hashmap! {"phase" => "confirmed"}, + )? + .iter() + .sum::(); + if msg_processed_count != messages_expected { + log!( + "Relayer confirmed {} submitted messages, expected {}", + msg_processed_count, + messages_expected + ); + return Ok(false); + } + + let ending_relayer_balance: f64 = agent_balance_sum(relayer_metrics_port).unwrap(); + + // Make sure the balance was correctly updated in the metrics. + // Ideally, make sure that the difference is >= gas_per_tx * gas_cost, set here: + // https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/c2288eb31734ba1f2f997e2c6ecb30176427bc2c/rust/utils/run-locally/src/cosmos/cli.rs#L55 + // What's stopping this is that the format returned by the `uosmo` balance query is a surprisingly low number (0.000003999999995184) + // but then maybe the gas_per_tx is just very low - how can we check that? (maybe by simulating said tx) + if starting_relayer_balance <= ending_relayer_balance { + log!( + "Expected starting relayer balance to be greater than ending relayer balance, but got {} <= {}", + starting_relayer_balance, + ending_relayer_balance + ); + return Ok(false); + } + + let dispatched_messages_scraped = fetch_metric( + &scraper_metrics_port.to_string(), + "hyperlane_contract_sync_stored_events", + &hashmap! {"data_type" => "message_dispatch"}, + )? + .iter() + .sum::(); + if dispatched_messages_scraped != messages_expected { + log!( + "Scraper has scraped {} dispatched messages, expected {}", + dispatched_messages_scraped, + messages_expected + ); + return Ok(false); + } + + let gas_payments_scraped = fetch_metric( + &scraper_metrics_port.to_string(), + "hyperlane_contract_sync_stored_events", + &hashmap! {"data_type" => "gas_payment"}, + )? + .iter() + .sum::(); + if gas_payments_scraped != expected_gas_payments { + log!( + "Scraper has scraped {} gas payments, expected {}", + gas_payments_scraped, + expected_gas_payments + ); + return Ok(false); + } + + let delivered_messages_scraped = fetch_metric( + &scraper_metrics_port.to_string(), + "hyperlane_contract_sync_stored_events", + &hashmap! {"data_type" => "message_delivery"}, + )? + .iter() + .sum::(); + if delivered_messages_scraped != messages_expected { + log!( + "Scraper has scraped {} delivered messages, expected {}", + delivered_messages_scraped, + messages_expected + ); + return Ok(false); + } + + log!("Termination invariants have been meet"); + Ok(true) +} + +#[cfg(feature = "cosmosnative")] +#[cfg(test)] +mod test { + #[test] + fn test_run() { + use crate::cosmosnative::run_locally; + + run_locally(); + } +} diff --git a/rust/main/utils/run-locally/src/cosmosnative/types.rs b/rust/main/utils/run-locally/src/cosmosnative/types.rs new file mode 100644 index 0000000000..3930036cd8 --- /dev/null +++ b/rust/main/utils/run-locally/src/cosmosnative/types.rs @@ -0,0 +1,115 @@ +use std::collections::BTreeMap; + +use hyperlane_core::NativeToken; +use hyperlane_cosmos::RawCosmosAmount; + +use crate::utils::AgentHandles; + +use super::{ + cli::SimApp, + constants::{CHAIN_ID, DENOM, PREFIX}, +}; + +#[derive(serde::Serialize, serde::Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AgentConfigAddrs { + pub mailbox: String, + pub interchain_gas_paymaster: String, + pub validator_announce: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct AgentConfigSigner { + #[serde(rename = "type")] + pub typ: String, + pub key: String, + pub prefix: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct AgentConfigIndex { + pub from: u32, + pub chunk: u32, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct AgentUrl { + pub http: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AgentConfig { + pub name: String, + pub domain_id: u32, + pub metrics_port: u32, + pub mailbox: String, + pub interchain_gas_paymaster: String, + pub validator_announce: String, + pub merkle_tree_hook: String, + pub protocol: String, + pub chain_id: String, + pub rpc_urls: Vec, + pub api_urls: Vec, + pub bech32_prefix: String, + pub index: AgentConfigIndex, + pub gas_price: RawCosmosAmount, + pub contract_address_bytes: usize, + pub native_token: NativeToken, + pub canonical_asset: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct AgentConfigOut { + pub chains: BTreeMap, +} + +#[derive(Debug)] +pub struct Contracts { + pub(crate) mailbox: String, + pub(crate) igp: String, + pub(crate) tokens: Vec, +} + +pub struct Deployment { + pub(crate) chain: SimApp, + pub(crate) name: String, + pub(crate) metrics: u32, + pub(crate) domain: u32, + pub(crate) contracts: Contracts, + pub(crate) handle: AgentHandles, +} + +impl AgentConfig { + pub fn new(node: &Deployment) -> Self { + AgentConfig { + name: node.name.clone(), + domain_id: node.domain, + metrics_port: node.metrics, + mailbox: node.contracts.mailbox.clone(), + interchain_gas_paymaster: node.contracts.igp.clone(), + validator_announce: node.contracts.mailbox.clone(), + merkle_tree_hook: node.contracts.mailbox.clone(), + protocol: "cosmosnative".to_owned(), + chain_id: CHAIN_ID.to_owned(), + rpc_urls: vec![AgentUrl { + http: format!("http://{}", node.chain.rpc_addr.replace("tcp://", "")), + }], + api_urls: vec![AgentUrl { + http: format!("http://{}", node.chain.api_addr.replace("tcp://", "")), + }], + bech32_prefix: PREFIX.to_string(), + gas_price: RawCosmosAmount { + denom: DENOM.to_string(), + amount: "0.2".to_string(), + }, + contract_address_bytes: 20, + index: AgentConfigIndex { from: 1, chunk: 5 }, + native_token: NativeToken { + decimals: 6, + denom: DENOM.to_string(), + }, + canonical_asset: DENOM.to_owned(), + } + } +} diff --git a/rust/main/utils/run-locally/src/main.rs b/rust/main/utils/run-locally/src/main.rs index 7aeb3ae101..6ad0da64ed 100644 --- a/rust/main/utils/run-locally/src/main.rs +++ b/rust/main/utils/run-locally/src/main.rs @@ -46,6 +46,7 @@ use crate::{ mod config; mod cosmos; +mod cosmosnative; mod ethereum; mod invariants; mod logging; From 867042167f8ea9187f4b576f6629ae322a74a662 Mon Sep 17 00:00:00 2001 From: yjamin Date: Wed, 5 Feb 2025 10:57:15 +0100 Subject: [PATCH 4/4] fix: typos --- .../chains/hyperlane-cosmos-native/src/providers/cosmos.rs | 2 +- rust/main/utils/run-locally/src/cosmosnative/cli.rs | 6 +++--- rust/main/utils/run-locally/src/cosmosnative/mod.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs b/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs index 0ad8f0252f..b0288af961 100644 --- a/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs +++ b/rust/main/chains/hyperlane-cosmos-native/src/providers/cosmos.rs @@ -180,7 +180,7 @@ impl CosmosNativeProvider { // extract the contract address from the tx // the tx is either a MsgPorcessMessage on the destination or a MsgRemoteTransfer on the origin - // we check for both tx types, if both are missing or an error occured while parsing we return the error + // we check for both tx types, if both are missing or an error occurred while parsing we return the error fn contract(tx: &Tx) -> ChainResult { // first check for the process message if let Some(recipient) = Self::check_msg_process(tx)? { diff --git a/rust/main/utils/run-locally/src/cosmosnative/cli.rs b/rust/main/utils/run-locally/src/cosmosnative/cli.rs index 02392941a6..9c2bd326ab 100644 --- a/rust/main/utils/run-locally/src/cosmosnative/cli.rs +++ b/rust/main/utils/run-locally/src/cosmosnative/cli.rs @@ -38,11 +38,11 @@ pub(crate) fn modify_json( /// Sim app /// -/// the sim app is a light cosmos chain that implementes the hyperlane cosmos module +/// the sim app is a light cosmos chain that implemenets the hyperlane cosmos module impl SimApp { pub fn new(bin: String, home: String, port_offset: u32) -> Self { let port_base = 26657 + port_offset * 5; // we increment by 5 ports as we need 5 unique ports per chain - let addr_base = "tcp://127.0.01"; + let addr_base = "tcp://127.0.0.1"; let mut next_port = port_base; let mut get_next_addr = || { @@ -151,7 +151,7 @@ impl SimApp { // TODO: test against the tx result to see if everything was created correctly self.tx(vec!["hyperlane", "igp", "create-igp", DENOM]); - // set the interchain gas config -> this determins the interchain gaspayments + // set the interchain gas config -> this determines the interchain gaspayments // cmd is following: igp-address remote-domain exchange-rate gas-price and gas-overhead // this config requires a payment of at least 0.200001uhyp self.tx(vec![ diff --git a/rust/main/utils/run-locally/src/cosmosnative/mod.rs b/rust/main/utils/run-locally/src/cosmosnative/mod.rs index 59394dcf5f..da902e6052 100644 --- a/rust/main/utils/run-locally/src/cosmosnative/mod.rs +++ b/rust/main/utils/run-locally/src/cosmosnative/mod.rs @@ -51,7 +51,7 @@ impl Drop for CosmosNativeStack { } } -// right now we only test two chains that communicate with eachother +// right now we only test two chains that communicate with each other // we send a one uhyp from node1 -> node2, this will result in a wrapped uhyp on node2 // we send a one uhyp from node2 -> node1, this will result in a wrapped uhyp on node1 fn dispatch(node1: &Deployment, node2: &Deployment) -> u32 { @@ -204,7 +204,7 @@ fn install_sim_app() -> PathBuf { let release_name = format!("{BINARY_NAME}_{target}"); log!("Downloading Sim App {}", release_name); let uri = format!( - "https://files.kyve.network/hyperlane/nightly/{}", // TODO: point to offical releases on github, right now we only have this private preview binary of a simulation app + "https://files.kyve.network/hyperlane/nightly/{}", // TODO: point to official releases on github, right now we only have this private preview binary of a simulation app release_name );