From fdb0d82e1534d393b638725ed18a2d52cf716a78 Mon Sep 17 00:00:00 2001 From: Anca Zamfir Date: Thu, 19 Nov 2020 15:07:19 +0100 Subject: [PATCH] Channel open handshake message relaying/CLIs (#403) * Add chan init and try CLIs * Add ack and confirm CLIs * Move to tendermint rc3 * Update changelog --- CHANGELOG.md | 11 +- Cargo.toml | 2 +- relayer-cli/Cargo.toml | 11 +- relayer-cli/src/commands/query/channel.rs | 2 +- relayer-cli/src/commands/query/client.rs | 2 +- relayer-cli/src/commands/tx.rs | 17 + relayer-cli/src/commands/tx/channel.rs | 389 +++++++++++++++++++ relayer-cli/src/commands/tx/connection.rs | 2 +- relayer-cli/tests/integration.rs | 9 +- relayer/Cargo.toml | 8 +- relayer/src/chain.rs | 70 +++- relayer/src/chain/cosmos.rs | 2 +- relayer/src/error.rs | 18 +- relayer/src/event_monitor.rs | 1 - relayer/src/tx.rs | 1 + relayer/src/tx/channel.rs | 449 ++++++++++++++++++++++ relayer/src/tx/client.rs | 2 +- 17 files changed, 970 insertions(+), 26 deletions(-) create mode 100644 relayer-cli/src/commands/tx/channel.rs create mode 100644 relayer/src/tx/channel.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d809dee177..9eb182ccc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,13 @@ Special thanks to external contributors for this release: @CharlyCst ([#347]). - ICS 4 Domain Types for channel handshakes ([#315]) - [relayer] - Implement `query_header_at_height` via plain RPC queries (no light client verification) ([#336]) - - Implement the relayer logic for connection handshake message ([#358], [#359], [#360]) + - Implement the relayer logic for connection handshake messages ([#358], [#359], [#360]) + - Implement the relayer logic for channel handshake messages ([#371], [#372], [#373], [#374]) - [relayer-cli] - Merge light clients config in relayer config and add commands to add/remove light clients ([#348]) - CLI for client update message ([#277]) - - Implement the relayer CLI for connection handshake message ([#358], [#359], [#360]) + - Implement the relayer CLI for connection handshake messages ([#358], [#359], [#360]) + - Implement the relayer CLI for channel handshake messages ([#371], [#372], [#373], [#374]) - [proto-compiler] - Refactor and allow specifying a commit at which the Cosmos SDK should be checked out ([#366]) - Add a `--tag` option to the `clone-sdk` command to check out a tag instead of a commit ([#369]) @@ -36,6 +38,11 @@ Special thanks to external contributors for this release: @CharlyCst ([#347]). [#366]: https://github.com/informalsystems/ibc-rs/issues/366 [#368]: https://github.com/informalsystems/ibc-rs/issues/368 [#369]: https://github.com/informalsystems/ibc-rs/pulls/369 +[#371]: https://github.com/informalsystems/ibc-rs/issues/371 +[#372]: https://github.com/informalsystems/ibc-rs/issues/372 +[#373]: https://github.com/informalsystems/ibc-rs/issues/373 +[#374]: https://github.com/informalsystems/ibc-rs/issues/374 + [proto-compiler]: https://github.com/informalsystems/ibc-rs/tree/master/proto-compiler ### IMPROVEMENTS diff --git a/Cargo.toml b/Cargo.toml index 0001bad76b..85d053d133 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,4 @@ members = [ exclude = [ "proto-compiler" -] +] \ No newline at end of file diff --git a/relayer-cli/Cargo.toml b/relayer-cli/Cargo.toml index 546b8e981a..72abc0b171 100644 --- a/relayer-cli/Cargo.toml +++ b/relayer-cli/Cargo.toml @@ -26,23 +26,20 @@ prost-types = { version = "0.6.1" } hex = "0.4" [dependencies.tendermint-proto] -version = "0.17.0-rc2" +version = "=0.17.0-rc3" [dependencies.tendermint] -version = "0.17.0-rc2" +version = "=0.17.0-rc3" [dependencies.tendermint-rpc] -version = "0.17.0-rc2" +version = "=0.17.0-rc3" features = ["http-client", "websocket-client"] [dependencies.tendermint-light-client] -version = "0.17.0-rc2" +version = "=0.17.0-rc3" [dependencies.abscissa_core] version = "0.5.2" -# optional: use `gimli` to capture backtraces -# see https://github.com/rust-lang/backtrace-rs/issues/189 -# features = ["gimli-backtrace"] [dev-dependencies] abscissa_core = { version = "0.5.2", features = ["testing"] } diff --git a/relayer-cli/src/commands/query/channel.rs b/relayer-cli/src/commands/query/channel.rs index aee3d1ca09..ece521bbb0 100644 --- a/relayer-cli/src/commands/query/channel.rs +++ b/relayer-cli/src/commands/query/channel.rs @@ -12,7 +12,7 @@ use crate::error::{Error, Kind}; use ibc::ics24_host::error::ValidationError; use relayer::chain::{Chain, CosmosSDKChain}; use tendermint::chain::Id as ChainId; -use tendermint_proto::DomainType; +use tendermint_proto::Protobuf; #[derive(Clone, Command, Debug, Options)] pub struct QueryChannelEndCmd { diff --git a/relayer-cli/src/commands/query/client.rs b/relayer-cli/src/commands/query/client.rs index 20ec1dc53d..6f7223d162 100644 --- a/relayer-cli/src/commands/query/client.rs +++ b/relayer-cli/src/commands/query/client.rs @@ -12,7 +12,7 @@ use ibc::ics24_host::Path::{ClientConnections, ClientConsensusState, ClientState use relayer::chain::Chain; use relayer::chain::CosmosSDKChain; use tendermint::chain::Id as ChainId; -use tendermint_proto::DomainType; +use tendermint_proto::Protobuf; use std::convert::TryInto; diff --git a/relayer-cli/src/commands/tx.rs b/relayer-cli/src/commands/tx.rs index 376ddd687e..8fbafc40c8 100644 --- a/relayer-cli/src/commands/tx.rs +++ b/relayer-cli/src/commands/tx.rs @@ -3,6 +3,7 @@ use abscissa_core::{Command, Help, Options, Runnable}; use crate::commands::tx::client::{TxCreateClientCmd, TxUpdateClientCmd}; +mod channel; mod client; mod connection; @@ -47,4 +48,20 @@ pub enum TxRawCommands { /// The `tx raw conn-confirm` subcommand #[options(help = "tx raw conn-confirm")] ConnConfirm(connection::TxRawConnConfirmCmd), + + /// The `tx raw chan-init` subcommand + #[options(help = "tx raw chan-init")] + ChanInit(channel::TxRawChanInitCmd), + + /// The `tx raw chan-try` subcommand + #[options(help = "tx raw chan-try")] + ChanTry(channel::TxRawChanTryCmd), + + /// The `tx raw chan-ack` subcommand + #[options(help = "tx raw chan-ack")] + ChanAck(channel::TxRawChanAckCmd), + + /// The `tx raw chan-confirm` subcommand + #[options(help = "tx raw chan-confirm")] + ChanConfirm(channel::TxRawChanConfirmCmd), } diff --git a/relayer-cli/src/commands/tx/channel.rs b/relayer-cli/src/commands/tx/channel.rs new file mode 100644 index 0000000000..ae43bcecec --- /dev/null +++ b/relayer-cli/src/commands/tx/channel.rs @@ -0,0 +1,389 @@ +use crate::prelude::*; + +use abscissa_core::{Command, Options, Runnable}; +use ibc::ics04_channel::channel::Order; +use ibc::ics24_host::identifier::{ChannelId, ConnectionId, PortId}; + +use relayer::config::Config; + +use crate::error::{Error, Kind}; +use relayer::tx::channel::{ + build_chan_ack_and_send, build_chan_confirm_and_send, build_chan_init_and_send, + build_chan_try_and_send, ChannelOpenInitOptions, ChannelOpenOptions, +}; + +#[derive(Clone, Command, Debug, Options)] +pub struct TxRawChanInitCmd { + #[options(free, help = "identifier of the destination chain")] + dest_chain_id: String, + + #[options(free, help = "identifier of the source chain")] + src_chain_id: String, + + #[options(free, help = "identifier of the destination connection")] + dest_connection_id: ConnectionId, + + #[options(free, help = "identifier of the destination port")] + dest_port_id: PortId, + + #[options(free, help = "identifier of the source port")] + src_port_id: PortId, + + #[options(free, help = "identifier of the destination channel")] + dest_channel_id: ChannelId, + + #[options(help = "identifier of the source channel", short = "s")] + src_channel_id: Option, + + #[options(help = "the channel order", short = "o")] + ordering: Order, + + #[options( + help = "json key file for the signer, must include mnemonic", + short = "k" + )] + seed_file: String, +} + +impl TxRawChanInitCmd { + fn validate_options(&self, config: &Config) -> Result { + let dest_chain_config = config + .chains + .iter() + .find(|c| c.id == self.dest_chain_id.parse().unwrap()) + .ok_or_else(|| "missing destination chain configuration".to_string())?; + + let src_chain_config = config + .chains + .iter() + .find(|c| c.id == self.src_chain_id.parse().unwrap()) + .ok_or_else(|| "missing src chain configuration".to_string())?; + + let signer_seed = std::fs::read_to_string(&self.seed_file).map_err(|e| { + anomaly::Context::new("invalid signer seed file", Some(e.into())).to_string() + })?; + + let opts = ChannelOpenInitOptions { + dest_chain_config: dest_chain_config.clone(), + src_chain_config: src_chain_config.clone(), + + dest_connection_id: self.dest_connection_id.clone(), + + dest_port_id: self.dest_port_id.clone(), + src_port_id: self.src_port_id.clone(), + + dest_channel_id: self.dest_channel_id.clone(), + src_channel_id: self.src_channel_id.clone(), + + ordering: self.ordering, + signer_seed, + }; + + Ok(opts) + } +} + +impl Runnable for TxRawChanInitCmd { + fn run(&self) { + let config = app_config(); + + let opts = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + status_info!("Message", "{:?}", opts); + + let res: Result = + build_chan_init_and_send(&opts).map_err(|e| Kind::Tx.context(e).into()); + + match res { + Ok(receipt) => status_info!("channel init, result: ", "{:?}", receipt), + Err(e) => status_info!("channel init failed, error: ", "{}", e), + } + } +} + +#[derive(Clone, Command, Debug, Options)] +pub struct TxRawChanTryCmd { + #[options(free, help = "identifier of the destination chain")] + dest_chain_id: String, + + #[options(free, help = "identifier of the source chain")] + src_chain_id: String, + + #[options(free, help = "identifier of the destination connection")] + dest_connection_id: ConnectionId, + + #[options(free, help = "identifier of the destination port")] + dest_port_id: PortId, + + #[options(free, help = "identifier of the source port")] + src_port_id: PortId, + + #[options(free, help = "identifier of the destination channel")] + dest_channel_id: ChannelId, + + #[options(free, help = "identifier of the source channel")] + src_channel_id: ChannelId, + + #[options(help = "the channel order", short = "o")] + ordering: Order, + + #[options( + help = "json key file for the signer, must include mnemonic", + short = "k" + )] + seed_file: String, +} + +impl TxRawChanTryCmd { + fn validate_options(&self, config: &Config) -> Result { + let dest_chain_config = config + .chains + .iter() + .find(|c| c.id == self.dest_chain_id.parse().unwrap()) + .ok_or_else(|| "missing destination chain configuration".to_string())?; + + let src_chain_config = config + .chains + .iter() + .find(|c| c.id == self.src_chain_id.parse().unwrap()) + .ok_or_else(|| "missing src chain configuration".to_string())?; + + let signer_seed = std::fs::read_to_string(&self.seed_file).map_err(|e| { + anomaly::Context::new("invalid signer seed file", Some(e.into())).to_string() + })?; + + let opts = ChannelOpenOptions { + dest_chain_config: dest_chain_config.clone(), + src_chain_config: src_chain_config.clone(), + + dest_connection_id: self.dest_connection_id.clone(), + + dest_port_id: self.dest_port_id.clone(), + src_port_id: self.src_port_id.clone(), + + dest_channel_id: self.dest_channel_id.clone(), + src_channel_id: self.src_channel_id.clone(), + + ordering: self.ordering, + signer_seed, + }; + + Ok(opts) + } +} + +impl Runnable for TxRawChanTryCmd { + fn run(&self) { + let config = app_config(); + + let opts = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + status_info!("Message", "{:?}", opts); + + let res: Result = + build_chan_try_and_send(&opts).map_err(|e| Kind::Tx.context(e).into()); + + match res { + Ok(receipt) => status_info!("channel try, result: ", "{:?}", receipt), + Err(e) => status_info!("channel try failed, error: ", "{}", e), + } + } +} + +#[derive(Clone, Command, Debug, Options)] +pub struct TxRawChanAckCmd { + #[options(free, help = "identifier of the destination chain")] + dest_chain_id: String, + + #[options(free, help = "identifier of the source chain")] + src_chain_id: String, + + #[options(free, help = "identifier of the destination connection")] + dest_connection_id: ConnectionId, + + #[options(free, help = "identifier of the destination port")] + dest_port_id: PortId, + + #[options(free, help = "identifier of the source port")] + src_port_id: PortId, + + #[options(free, help = "identifier of the destination channel")] + dest_channel_id: ChannelId, + + #[options(free, help = "identifier of the source channel")] + src_channel_id: ChannelId, + + #[options(help = "the channel order", short = "o")] + ordering: Order, + + #[options( + help = "json key file for the signer, must include mnemonic", + short = "k" + )] + seed_file: String, +} + +impl TxRawChanAckCmd { + fn validate_options(&self, config: &Config) -> Result { + let dest_chain_config = config + .chains + .iter() + .find(|c| c.id == self.dest_chain_id.parse().unwrap()) + .ok_or_else(|| "missing destination chain configuration".to_string())?; + + let src_chain_config = config + .chains + .iter() + .find(|c| c.id == self.src_chain_id.parse().unwrap()) + .ok_or_else(|| "missing src chain configuration".to_string())?; + + let signer_seed = std::fs::read_to_string(&self.seed_file).map_err(|e| { + anomaly::Context::new("invalid signer seed file", Some(e.into())).to_string() + })?; + + let opts = ChannelOpenOptions { + dest_chain_config: dest_chain_config.clone(), + src_chain_config: src_chain_config.clone(), + + dest_connection_id: self.dest_connection_id.clone(), + + dest_port_id: self.dest_port_id.clone(), + src_port_id: self.src_port_id.clone(), + + dest_channel_id: self.dest_channel_id.clone(), + src_channel_id: self.src_channel_id.clone(), + + ordering: self.ordering, + signer_seed, + }; + + Ok(opts) + } +} + +impl Runnable for TxRawChanAckCmd { + fn run(&self) { + let config = app_config(); + + let opts = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + status_info!("Message", "{:?}", opts); + + let res: Result = + build_chan_ack_and_send(&opts).map_err(|e| Kind::Tx.context(e).into()); + + match res { + Ok(receipt) => status_info!("channel ack, result: ", "{:?}", receipt), + Err(e) => status_info!("channel ack failed, error: ", "{}", e), + } + } +} + +#[derive(Clone, Command, Debug, Options)] +pub struct TxRawChanConfirmCmd { + #[options(free, help = "identifier of the destination chain")] + dest_chain_id: String, + + #[options(free, help = "identifier of the source chain")] + src_chain_id: String, + + #[options(free, help = "identifier of the destination connection")] + dest_connection_id: ConnectionId, + + #[options(free, help = "identifier of the destination port")] + dest_port_id: PortId, + + #[options(free, help = "identifier of the source port")] + src_port_id: PortId, + + #[options(free, help = "identifier of the destination channel")] + dest_channel_id: ChannelId, + + #[options(free, help = "identifier of the source channel")] + src_channel_id: ChannelId, + + #[options(help = "the channel order", short = "o")] + ordering: Order, + + #[options( + help = "json key file for the signer, must include mnemonic", + short = "k" + )] + seed_file: String, +} + +impl TxRawChanConfirmCmd { + fn validate_options(&self, config: &Config) -> Result { + let dest_chain_config = config + .chains + .iter() + .find(|c| c.id == self.dest_chain_id.parse().unwrap()) + .ok_or_else(|| "missing destination chain configuration".to_string())?; + + let src_chain_config = config + .chains + .iter() + .find(|c| c.id == self.src_chain_id.parse().unwrap()) + .ok_or_else(|| "missing src chain configuration".to_string())?; + + let signer_seed = std::fs::read_to_string(&self.seed_file).map_err(|e| { + anomaly::Context::new("invalid signer seed file", Some(e.into())).to_string() + })?; + + let opts = ChannelOpenOptions { + dest_chain_config: dest_chain_config.clone(), + src_chain_config: src_chain_config.clone(), + + dest_connection_id: self.dest_connection_id.clone(), + + dest_port_id: self.dest_port_id.clone(), + src_port_id: self.src_port_id.clone(), + + dest_channel_id: self.dest_channel_id.clone(), + src_channel_id: self.src_channel_id.clone(), + + ordering: self.ordering, + signer_seed, + }; + + Ok(opts) + } +} + +impl Runnable for TxRawChanConfirmCmd { + fn run(&self) { + let config = app_config(); + + let opts = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + status_info!("Message", "{:?}", opts); + + let res: Result = + build_chan_confirm_and_send(&opts).map_err(|e| Kind::Tx.context(e).into()); + + match res { + Ok(receipt) => status_info!("channel confirm, result: ", "{:?}", receipt), + Err(e) => status_info!("channel confirm failed, error: ", "{}", e), + } + } +} diff --git a/relayer-cli/src/commands/tx/connection.rs b/relayer-cli/src/commands/tx/connection.rs index c889960988..414af0baa3 100644 --- a/relayer-cli/src/commands/tx/connection.rs +++ b/relayer-cli/src/commands/tx/connection.rs @@ -29,7 +29,7 @@ pub struct TxRawConnInitCmd { #[options(free, help = "identifier of the destination connection")] dest_connection_id: ConnectionId, - #[options(help = "identifier of the source connection", short = "d")] + #[options(help = "identifier of the source connection", short = "s")] src_connection_id: Option, #[options( diff --git a/relayer-cli/tests/integration.rs b/relayer-cli/tests/integration.rs index b486a29592..0706b6ec30 100644 --- a/relayer-cli/tests/integration.rs +++ b/relayer-cli/tests/integration.rs @@ -18,7 +18,7 @@ use ibc::ics24_host::Path::{ChannelEnds, ClientConnections}; use relayer::chain::{Chain, CosmosSDKChain}; use relayer::config::{default, ChainConfig, Config}; use tendermint::net::Address; -use tendermint_proto::DomainType; +use tendermint_proto::Protobuf; use std::convert::TryInto; use std::str::FromStr; @@ -100,8 +100,11 @@ fn query_channel_id() { assert_eq!(query.state(), &ChannelState::Init); assert_eq!(query.ordering(), &Order::Ordered); - assert_eq!(query.counterparty().port_id(), "secondport"); - assert_eq!(query.counterparty().channel_id(), "secondchannel"); + assert_eq!(query.counterparty().port_id().as_str(), "secondport"); + assert_eq!( + query.counterparty().channel_id().unwrap().as_str(), + "secondchannel" + ); assert_eq!(query.connection_hops()[0].as_str(), "connectionidatob"); assert_eq!(query.version(), "1.0"); } diff --git a/relayer/Cargo.toml b/relayer/Cargo.toml index 71475cfa94..b060c44432 100644 --- a/relayer/Cargo.toml +++ b/relayer/Cargo.toml @@ -37,16 +37,16 @@ bech32 = "0.7.2" tonic = "0.3.1" [dependencies.tendermint] -version = "0.17.0-rc2" +version = "=0.17.0-rc3" [dependencies.tendermint-rpc] -version = "0.17.0-rc2" +version = "=0.17.0-rc3" features = ["http-client", "websocket-client"] [dependencies.tendermint-light-client] -version = "0.17.0-rc2" +version = "=0.17.0-rc3" [dependencies.tendermint-proto] -version = "0.17.0-rc2" +version = "=0.17.0-rc3" [dev-dependencies] diff --git a/relayer/src/chain.rs b/relayer/src/chain.rs index 226186c98a..cc91a19cb5 100644 --- a/relayer/src/chain.rs +++ b/relayer/src/chain.rs @@ -5,7 +5,7 @@ use anomaly::fail; use prost_types::Any; use serde::{de::DeserializeOwned, Serialize}; -use tendermint_proto::DomainType; +use tendermint_proto::Protobuf; // TODO - tendermint deps should not be here //use tendermint::account::Id as AccountId; @@ -26,7 +26,7 @@ use ibc::ics03_connection::msgs::conn_open_init::MsgConnectionOpenInit; use ibc::ics03_connection::msgs::conn_open_try::MsgConnectionOpenTry; use ibc::ics03_connection::version::get_compatible_versions; use ibc::ics23_commitment::commitment::{CommitmentPrefix, CommitmentProof}; -use ibc::ics24_host::identifier::{ClientId, ConnectionId}; +use ibc::ics24_host::identifier::{ChannelId, ClientId, ConnectionId, PortId}; use ibc::ics24_host::Path; use ibc::ics24_host::Path::ClientConsensusState as ClientConsensusPath; use ibc::proofs::{ConsensusProof, Proofs}; @@ -40,7 +40,10 @@ use crate::tx::connection::{ConnectionMsgType, ConnectionOpenInitOptions, Connec use crate::util::block_on; pub(crate) mod cosmos; +use crate::tx::channel::ChannelMsgType; pub use cosmos::CosmosSDKChain; +use ibc::ics04_channel::channel::ChannelEnd; + pub mod handle; pub mod runtime; @@ -149,6 +152,24 @@ pub trait Chain { Ok((connection_end, res.proof)) } + fn proven_channel( + &self, + port_id: &PortId, + channel_id: &ChannelId, + height: ICSHeight, + ) -> Result<(ChannelEnd, MerkleProof), Error> { + let res = self + .ics_query( + Path::ChannelEnds(port_id.clone(), channel_id.clone()), + height, + true, + ) + .map_err(|e| Kind::Query.context(e))?; + let channel_end = ChannelEnd::decode_vec(&res.value).map_err(|e| Kind::Query.context(e))?; + + Ok((channel_end, res.proof)) + } + fn proven_client_consensus( &self, client_id: &ClientId, @@ -203,6 +224,22 @@ pub trait Chain { })?) } + fn query_channel( + &self, + port_id: &PortId, + channel_id: &ChannelId, + height: ICSHeight, + ) -> Result { + Ok(self + .ics_query( + Path::ChannelEnds(port_id.clone(), channel_id.clone()), + height, + false, + ) + .map_err(|e| Kind::Query.context(e)) + .and_then(|v| ChannelEnd::decode_vec(&v.value).map_err(|e| Kind::Query.context(e)))?) + } + /// Builds the required proofs and the client state for connection handshake messages. /// The proofs and client state must be obtained from queries at same height with value /// `height - 1` @@ -266,4 +303,33 @@ pub trait Chain { .map_err(|_| Kind::MalformedProof)?, )) } + + fn module_version(&self, port_id: &PortId) -> String { + // TODO - query the chain, currently hardcoded + if port_id.as_str() == "transfer" { + "ics20-1".to_string() + } else { + "".to_string() + } + } + + /// Builds the proof for channel handshake messages. + /// The proof must be obtained from queries at height `height - 1` + fn build_channel_proofs( + &self, + port_id: &PortId, + channel_id: &ChannelId, + height: ICSHeight, + ) -> Result { + // Set the height of the queries at height - 1 + let query_height = height + .decrement() + .map_err(|e| Kind::InvalidHeight.context(e))?; + + // Collect all proofs as required + let channel_proof = + CommitmentProof::from(self.proven_channel(port_id, channel_id, query_height)?.1); + + Ok(Proofs::new(channel_proof, None, None, height).map_err(|_| Kind::MalformedProof)?) + } } diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index de13eee614..e5c81d5947 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -11,7 +11,7 @@ use bitcoin::hashes::hex::ToHex; use k256::ecdsa::{SigningKey, VerifyKey}; use tendermint_proto::crypto::ProofOps; -use tendermint_proto::DomainType; +use tendermint_proto::Protobuf; use tendermint_rpc::endpoint::abci_query::AbciQuery; use tendermint_rpc::endpoint::broadcast; diff --git a/relayer/src/error.rs b/relayer/src/error.rs index c874f508d6..7230139c00 100644 --- a/relayer/src/error.rs +++ b/relayer/src/error.rs @@ -1,7 +1,7 @@ //! This module defines the various errors that be raised in the relayer. use anomaly::{BoxError, Context}; -use ibc::ics24_host::identifier::{ClientId, ConnectionId}; +use ibc::ics24_host::identifier::{ChannelId, ClientId, ConnectionId}; use thiserror::Error; /// An error that can be raised by the relayer. @@ -78,6 +78,22 @@ pub enum Kind { #[error("Failed to build conn open confirm {0}: {1}")] ConnOpenConfirm(ConnectionId, String), + /// Channel open init failure + #[error("Failed to build channel open init {0}: {1}")] + ChanOpenInit(ChannelId, String), + + /// Channel open try failure + #[error("Failed to build channel open try {0}: {1}")] + ChanOpenTry(ChannelId, String), + + /// Channel open ack failure + #[error("Failed to build channel open ack {0}: {1}")] + ChanOpenAck(ChannelId, String), + + /// Channel open confirm failure + #[error("Failed to build channel open confirm {0}: {1}")] + ChanOpenConfirm(ChannelId, String), + /// A message transaction failure #[error("Message transaction failure: {0}")] MessageTransaction(String), diff --git a/relayer/src/event_monitor.rs b/relayer/src/event_monitor.rs index 70d6f4619b..d3c7a70056 100644 --- a/relayer/src/event_monitor.rs +++ b/relayer/src/event_monitor.rs @@ -2,7 +2,6 @@ use ibc::events::IBCEvent; use tendermint::{chain, net, Error as TMError}; use tendermint_rpc::{ query::EventType, query::Query, Subscription, SubscriptionClient, WebSocketClient, - WebSocketClientDriver, }; use tokio::stream::StreamExt; use tokio::sync::mpsc::Sender; diff --git a/relayer/src/tx.rs b/relayer/src/tx.rs index 771d38780b..210a35c8ac 100644 --- a/relayer/src/tx.rs +++ b/relayer/src/tx.rs @@ -1,2 +1,3 @@ +pub mod channel; pub mod client; pub mod connection; diff --git a/relayer/src/tx/channel.rs b/relayer/src/tx/channel.rs new file mode 100644 index 0000000000..1e93dbde5f --- /dev/null +++ b/relayer/src/tx/channel.rs @@ -0,0 +1,449 @@ +use prost_types::Any; + +use ibc_proto::ibc::core::channel::v1::MsgChannelOpenAck as RawMsgChannelOpenAck; +use ibc_proto::ibc::core::channel::v1::MsgChannelOpenConfirm as RawMsgChannelOpenConfirm; +use ibc_proto::ibc::core::channel::v1::MsgChannelOpenInit as RawMsgChannelOpenInit; +use ibc_proto::ibc::core::channel::v1::MsgChannelOpenTry as RawMsgChannelOpenTry; +use ibc_proto::ibc::core::client::v1::MsgUpdateClient as RawMsgUpdateClient; + +use ibc::ics04_channel::channel::{ChannelEnd, Counterparty, Order, State}; +use ibc::ics04_channel::msgs::chan_open_ack::MsgChannelOpenAck; +use ibc::ics04_channel::msgs::chan_open_confirm::MsgChannelOpenConfirm; +use ibc::ics04_channel::msgs::chan_open_init::MsgChannelOpenInit; +use ibc::ics04_channel::msgs::chan_open_try::MsgChannelOpenTry; +use ibc::ics24_host::identifier::{ChannelId, ClientId, ConnectionId, PortId}; +use ibc::tx_msg::Msg; +use ibc::Height as ICSHeight; + +use crate::chain::{Chain, CosmosSDKChain}; +use crate::config::ChainConfig; +use crate::error::{Error, Kind}; +use crate::tx::client::build_update_client; + +/// Enumeration of proof carrying ICS4 message, helper for relayer. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ChannelMsgType { + OpenTry, + OpenAck, + OpenConfirm, +} + +#[derive(Clone, Debug)] +pub struct ChannelOpenInitOptions { + pub dest_chain_config: ChainConfig, + pub src_chain_config: ChainConfig, + pub dest_connection_id: ConnectionId, + pub dest_port_id: PortId, + pub src_port_id: PortId, + pub dest_channel_id: ChannelId, + pub src_channel_id: Option, + pub ordering: Order, + pub signer_seed: String, +} + +pub fn build_chan_init( + dest_chain: &mut CosmosSDKChain, + src_chain: &CosmosSDKChain, + opts: &ChannelOpenInitOptions, +) -> Result, Error> { + // Check that the destination chain will accept the message, i.e. it does not have the channel + if dest_chain + .query_channel( + &opts.dest_port_id, + &opts.dest_channel_id, + ICSHeight::default(), + ) + .is_ok() + { + return Err(Kind::ChanOpenInit( + opts.dest_channel_id.clone(), + "channel already exist".into(), + ) + .into()); + } + + // Get the signer from key seed file + let (_, signer) = dest_chain.key_and_signer(&opts.signer_seed)?; + + let counterparty = Counterparty::new(opts.src_port_id.clone(), opts.src_channel_id.clone()); + + let channel = ChannelEnd::new( + State::Init, + opts.ordering, + counterparty, + vec![opts.dest_connection_id.clone()], + dest_chain.module_version(&opts.dest_port_id), + ); + + // Build the domain type message + let new_msg = MsgChannelOpenInit { + port_id: opts.dest_port_id.clone(), + channel_id: opts.dest_channel_id.clone(), + channel, + signer, + }; + + Ok(vec![new_msg.to_any::()]) +} + +pub fn build_chan_init_and_send(opts: &ChannelOpenInitOptions) -> Result { + // Get the source and destination chains. + let src_chain = &CosmosSDKChain::from_config(opts.clone().src_chain_config)?; + let dest_chain = &mut CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + + let new_msgs = build_chan_init(dest_chain, src_chain, opts)?; + let (key, _) = dest_chain.key_and_signer(&opts.signer_seed)?; + + Ok(dest_chain.send(new_msgs, key, "".to_string(), 0)?) +} + +#[derive(Clone, Debug)] +pub struct ChannelOpenOptions { + pub dest_chain_config: ChainConfig, + pub src_chain_config: ChainConfig, + pub dest_port_id: PortId, + pub src_port_id: PortId, + pub dest_channel_id: ChannelId, + pub src_channel_id: ChannelId, + pub dest_connection_id: ConnectionId, + pub ordering: Order, + pub signer_seed: String, +} + +fn check_destination_channel_state( + channel_id: ChannelId, + existing_channel: ChannelEnd, + expected_channel: ChannelEnd, +) -> Result<(), Error> { + let good_connection_hops = + existing_channel.connection_hops() == expected_channel.connection_hops(); + + let good_state = + existing_channel.state().clone() as u32 <= expected_channel.state().clone() as u32; + + let good_channel_ids = existing_channel.counterparty().channel_id().is_none() + || existing_channel.counterparty().channel_id() + == expected_channel.counterparty().channel_id(); + + // TODO check versions + + if good_state && good_connection_hops && good_channel_ids { + Ok(()) + } else { + Err(Kind::ChanOpenTry( + channel_id, + "channel already exist in an incompatible state".into(), + ) + .into()) + } +} + +/// Retrieves the channel from destination and compares against the expected channel +/// built from the message type (`msg_type`) and options (`opts`). +/// If the expected and the destination channels are compatible, it returns the expected channel +fn validated_expected_channel( + dest_chain: &mut CosmosSDKChain, + src_chain: &CosmosSDKChain, + msg_type: ChannelMsgType, + opts: &ChannelOpenOptions, +) -> Result { + // If there is a channel present on the destination chain, it should look like this: + let counterparty = Counterparty::new( + opts.src_port_id.clone(), + Option::from(opts.src_channel_id.clone()), + ); + + // The highest expected state, depends on the message type: + let highest_state = match msg_type { + ChannelMsgType::OpenTry => State::Init, + ChannelMsgType::OpenAck => State::TryOpen, + ChannelMsgType::OpenConfirm => State::TryOpen, + }; + + let dest_expected_channel = ChannelEnd::new( + highest_state, + opts.ordering, + counterparty, + vec![opts.dest_connection_id.clone()], + dest_chain.module_version(&opts.dest_port_id), + ); + + // Retrieve existing channel if any + let dest_channel = dest_chain.query_channel( + &opts.dest_port_id, + &opts.dest_channel_id, + ICSHeight::default(), + ); + + // Check if a connection is expected to exist on destination chain + if msg_type == ChannelMsgType::OpenTry { + // TODO - check typed Err, or make query_channel return Option + // It is ok if there is no channel for Try Tx + if dest_channel.is_err() { + return Ok(dest_expected_channel); + } + } else { + // A channel must exist on destination chain for Ack and Confirm Tx-es to succeed + if dest_channel.is_err() { + return Err(Kind::ChanOpenTry( + opts.src_channel_id.clone(), + "missing channel on source chain".to_string(), + ) + .into()); + } + } + + check_destination_channel_state( + opts.dest_channel_id.clone(), + dest_channel?, + dest_expected_channel.clone(), + )?; + + Ok(dest_expected_channel) +} + +pub fn build_chan_try( + dest_chain: &mut CosmosSDKChain, + src_chain: &CosmosSDKChain, + opts: &ChannelOpenOptions, +) -> Result, Error> { + // Check that the destination chain will accept the message, i.e. it does not have the channel + let dest_expected_channel = + validated_expected_channel(dest_chain, src_chain, ChannelMsgType::OpenTry, opts).map_err( + |e| { + Kind::ChanOpenTry( + opts.src_channel_id.clone(), + "try options inconsistent with existing channel on destination chain" + .to_string(), + ) + .context(e) + }, + )?; + + let src_channel = src_chain + .query_channel( + &opts.src_port_id, + &opts.src_channel_id, + ICSHeight::default(), + ) + .map_err(|e| { + Kind::ChanOpenTry( + opts.dest_channel_id.clone(), + "channel does not exist on source".into(), + ) + .context(e) + })?; + + // Retrieve the connection + let dest_connection = + dest_chain.query_connection(&opts.dest_connection_id.clone(), ICSHeight::default())?; + + let ics_target_height = src_chain.query_latest_height()?; + + // Build message to update client on destination + let mut msgs = build_update_client( + dest_chain, + src_chain, + dest_connection.client_id().clone(), + ics_target_height, + &opts.signer_seed, + )?; + + let counterparty = + Counterparty::new(opts.src_port_id.clone(), Some(opts.src_channel_id.clone())); + + let channel = ChannelEnd::new( + State::Init, + opts.ordering, + counterparty, + vec![opts.dest_connection_id.clone()], + dest_chain.module_version(&opts.dest_port_id), + ); + + // Build the domain type message + let new_msg = MsgChannelOpenTry { + port_id: opts.dest_port_id.clone(), + channel_id: opts.dest_channel_id.clone(), + counterparty_chosen_channel_id: src_channel.counterparty().channel_id, + channel, + counterparty_version: src_chain.module_version(&opts.src_port_id), + proofs: src_chain.build_channel_proofs( + &opts.src_port_id, + &opts.src_channel_id, + ics_target_height, + )?, + signer: dest_chain.key_and_signer(&opts.signer_seed)?.1, + }; + + let mut new_msgs = vec![new_msg.to_any::()]; + + msgs.append(&mut new_msgs); + + Ok(msgs) +} + +pub fn build_chan_try_and_send(opts: &ChannelOpenOptions) -> Result { + // Get the source and destination chains. + let src_chain = &CosmosSDKChain::from_config(opts.clone().src_chain_config)?; + let dest_chain = &mut CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + + let new_msgs = build_chan_try(dest_chain, src_chain, opts)?; + let (key, _) = dest_chain.key_and_signer(&opts.signer_seed)?; + + Ok(dest_chain.send(new_msgs, key, "".to_string(), 0)?) +} + +pub fn build_chan_ack( + dest_chain: &mut CosmosSDKChain, + src_chain: &CosmosSDKChain, + opts: &ChannelOpenOptions, +) -> Result, Error> { + // Check that the destination chain will accept the message + let dest_expected_channel = + validated_expected_channel(dest_chain, src_chain, ChannelMsgType::OpenAck, opts).map_err( + |e| { + Kind::ChanOpenAck( + opts.src_channel_id.clone(), + "ack options inconsistent with existing channel on destination chain" + .to_string(), + ) + .context(e) + }, + )?; + + let src_channel = src_chain + .query_channel( + &opts.src_port_id, + &opts.src_channel_id, + ICSHeight::default(), + ) + .map_err(|e| { + Kind::ChanOpenAck( + opts.dest_channel_id.clone(), + "channel does not exist on source".into(), + ) + .context(e) + })?; + + // Retrieve the connection + let dest_connection = + dest_chain.query_connection(&opts.dest_connection_id.clone(), ICSHeight::default())?; + + let ics_target_height = src_chain.query_latest_height()?; + + // Build message to update client on destination + let mut msgs = build_update_client( + dest_chain, + src_chain, + dest_connection.client_id().clone(), + ics_target_height, + &opts.signer_seed, + )?; + + // Build the domain type message + let new_msg = MsgChannelOpenAck { + port_id: opts.dest_port_id.clone(), + channel_id: opts.dest_channel_id.clone(), + counterparty_channel_id: opts.src_channel_id.clone(), + counterparty_version: src_chain.module_version(&opts.dest_port_id), + proofs: src_chain.build_channel_proofs( + &opts.src_port_id, + &opts.src_channel_id, + ics_target_height, + )?, + signer: dest_chain.key_and_signer(&opts.signer_seed)?.1, + }; + + let mut new_msgs = vec![new_msg.to_any::()]; + + msgs.append(&mut new_msgs); + + Ok(msgs) +} + +pub fn build_chan_ack_and_send(opts: &ChannelOpenOptions) -> Result { + // Get the source and destination chains. + let src_chain = &CosmosSDKChain::from_config(opts.clone().src_chain_config)?; + let dest_chain = &mut CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + + let new_msgs = build_chan_ack(dest_chain, src_chain, opts)?; + let (key, _) = dest_chain.key_and_signer(&opts.signer_seed)?; + + Ok(dest_chain.send(new_msgs, key, "".to_string(), 0)?) +} + +pub fn build_chan_confirm( + dest_chain: &mut CosmosSDKChain, + src_chain: &CosmosSDKChain, + opts: &ChannelOpenOptions, +) -> Result, Error> { + // Check that the destination chain will accept the message + let dest_expected_channel = + validated_expected_channel(dest_chain, src_chain, ChannelMsgType::OpenConfirm, opts) + .map_err(|e| { + Kind::ChanOpenConfirm( + opts.src_channel_id.clone(), + "confirm options inconsistent with existing channel on destination chain" + .to_string(), + ) + .context(e) + })?; + + let src_channel = src_chain + .query_channel( + &opts.src_port_id, + &opts.src_channel_id, + ICSHeight::default(), + ) + .map_err(|e| { + Kind::ChanOpenConfirm( + opts.dest_channel_id.clone(), + "channel does not exist on source".into(), + ) + .context(e) + })?; + + // Retrieve the connection + let dest_connection = + dest_chain.query_connection(&opts.dest_connection_id.clone(), ICSHeight::default())?; + + let ics_target_height = src_chain.query_latest_height()?; + + // Build message to update client on destination + let mut msgs = build_update_client( + dest_chain, + src_chain, + dest_connection.client_id().clone(), + ics_target_height, + &opts.signer_seed, + )?; + + // Build the domain type message + let new_msg = MsgChannelOpenConfirm { + port_id: opts.dest_port_id.clone(), + channel_id: opts.dest_channel_id.clone(), + proofs: src_chain.build_channel_proofs( + &opts.src_port_id, + &opts.src_channel_id, + ics_target_height, + )?, + signer: dest_chain.key_and_signer(&opts.signer_seed)?.1, + }; + + let mut new_msgs = vec![new_msg.to_any::()]; + + msgs.append(&mut new_msgs); + + Ok(msgs) +} + +pub fn build_chan_confirm_and_send(opts: &ChannelOpenOptions) -> Result { + // Get the source and destination chains. + let src_chain = &CosmosSDKChain::from_config(opts.clone().src_chain_config)?; + let dest_chain = &mut CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + + let new_msgs = build_chan_confirm(dest_chain, src_chain, opts)?; + let (key, _) = dest_chain.key_and_signer(&opts.signer_seed)?; + + Ok(dest_chain.send(new_msgs, key, "".to_string(), 0)?) +} diff --git a/relayer/src/tx/client.rs b/relayer/src/tx/client.rs index e8992cf422..6706924950 100644 --- a/relayer/src/tx/client.rs +++ b/relayer/src/tx/client.rs @@ -7,7 +7,7 @@ use prost_types::Any; use tendermint::account::Id as AccountId; use tendermint_light_client::types::TrustThreshold; -use tendermint_proto::DomainType; +use tendermint_proto::Protobuf; use ibc_proto::ibc::core::client::v1::MsgCreateClient as RawMsgCreateClient; use ibc_proto::ibc::core::client::v1::MsgUpdateClient as RawMsgUpdateClient;