diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 4131034910..b2ea21557d 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -65,6 +65,11 @@ jobs: account_prefix: cosmos native_token: stake features: ica,ics29-fee,new-register-interchain-account,channel-upgrade,authz + - package: ibc-go-v8-polymer-multihop-simapp + command: simd + account_prefix: cosmos + native_token: stake + features: multihop - package: wasmd command: wasmd account_prefix: wasm diff --git a/Cargo.lock b/Cargo.lock index d20910950e..16c6c38b89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,9 +67,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -82,33 +82,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -478,9 +478,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" dependencies = [ "serde", ] @@ -493,9 +493,9 @@ checksum = "e6e9e01327e6c86e92ec72b1c798d4a94810f147209bbe3ffab6a86954937a6f" [[package]] name = "cc" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" [[package]] name = "cfg-if" @@ -580,9 +580,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "console" @@ -954,9 +954,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6dc8c8ff84895b051f07a0e65f975cf225131742531338752abfb324e4449ff" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", "regex", @@ -1228,7 +1228,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.3.0", "slab", "tokio", "tokio-util", @@ -1247,7 +1247,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.2.6", + "indexmap 2.3.0", "slab", "tokio", "tokio-util", @@ -1541,15 +1541,14 @@ dependencies = [ "tendermint", "tendermint-rpc", "time", - "toml 0.8.16", + "toml 0.8.19", "tonic", ] [[package]] name = "ibc-proto" version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1678333cf68c9094ca66aaf9a271269f1f6bf5c26881161def8bd88cee831a23" +source = "git+https://github.com/cosmos/ibc-proto-rs.git?branch=multihop#3f15d59102d6db8d61c4d472fa7c8660fb323d55" dependencies = [ "base64 0.22.1", "bytes", @@ -1625,7 +1624,7 @@ dependencies = [ "tiny-keccak", "tokio", "tokio-stream", - "toml 0.8.16", + "toml 0.8.19", "tonic", "tracing", "tracing-subscriber", @@ -1684,7 +1683,7 @@ dependencies = [ "reqwest", "serde", "tokio", - "toml 0.8.16", + "toml 0.8.19", "tracing", ] @@ -1765,7 +1764,7 @@ dependencies = [ "subtle-encoding", "tendermint-rpc", "tokio", - "toml 0.8.16", + "toml 0.8.19", "tonic", "tracing", "tracing-subscriber", @@ -1833,9 +1832,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -1859,9 +1858,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" @@ -2309,9 +2308,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "primitive-types" @@ -2495,9 +2497,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -2712,7 +2714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.2", + "rustls-pemfile 2.1.3", "rustls-pki-types", "schannel", "security-framework", @@ -2729,9 +2731,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64 0.22.1", "rustls-pki-types", @@ -2787,9 +2789,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.4" +version = "2.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4465c22496331e20eb047ff46e7366455bc01c0c02015c4a376de0b2cd3a1af" +checksum = "a870e34715d5d59c8536040d4d4e7a41af44d527dc50237036ba4090db7996fc" dependencies = [ "sdd", ] @@ -2821,9 +2823,9 @@ dependencies = [ [[package]] name = "sdd" -version = "1.7.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85f05a494052771fc5bd0619742363b5e24e5ad72ab3111ec2e27925b8edc5f3" +checksum = "177258b64c0faaa9ffd3c65cd3262c2bc7e2588dbbd9c1641d0346145c1bbda8" [[package]] name = "sec1" @@ -2943,9 +2945,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ "itoa", "memchr", @@ -3001,7 +3003,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "itoa", "ryu", "serde", @@ -3299,12 +3301,13 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.10.1" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", "windows-sys 0.52.0", ] @@ -3350,7 +3353,7 @@ dependencies = [ "serde", "serde_json", "tendermint", - "toml 0.8.16", + "toml 0.8.19", "url", ] @@ -3623,9 +3626,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.0" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3318c4fc7126c339a40fbc025927d0328ca32259f68bfe4321660644c1f626" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", @@ -3717,9 +3720,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -3729,20 +3732,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.17" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_spanned", "toml_datetime", @@ -3771,7 +3774,7 @@ dependencies = [ "pin-project", "prost", "rustls-native-certs 0.7.1", - "rustls-pemfile 2.1.2", + "rustls-pemfile 2.1.3", "socket2", "tokio", "tokio-rustls 0.26.0", @@ -4058,9 +4061,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" @@ -4190,11 +4193,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4221,6 +4224,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4344,9 +4356,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] @@ -4361,6 +4373,27 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "zeroize" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index 5e8958efac..c322817784 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,6 +121,7 @@ uuid = "1.10.0" overflow-checks = true [patch.crates-io] +ibc-proto = { git = "https://github.com/cosmos/ibc-proto-rs.git", branch = "multihop" } # tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "main" } # tendermint-rpc = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "main" } # tendermint-proto = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "main" } diff --git a/crates/relayer-cli/src/application.rs b/crates/relayer-cli/src/application.rs index a6b9c656e0..04184fe8f1 100644 --- a/crates/relayer-cli/src/application.rs +++ b/crates/relayer-cli/src/application.rs @@ -10,6 +10,7 @@ use abscissa_core::{ terminal::ColorChoice, Application, Configurable, FrameworkError, FrameworkErrorKind, StandardPaths, }; +use ibc_relayer::registry::{set_global_registry, SharedRegistry}; use ibc_relayer::{ config::{Config, TracingServerConfig}, util::debug_section::DebugSection, @@ -152,6 +153,9 @@ impl Application for CliApp { tracing::info!("running Hermes v{}", clap::crate_version!()); + // Set global registry to get or spawn chain handles + set_global_registry(SharedRegistry::new(config.clone())); + self.config.set_once(config); Ok(()) diff --git a/crates/relayer-cli/src/cli_utils.rs b/crates/relayer-cli/src/cli_utils.rs index 733914fd06..51c90fbc34 100644 --- a/crates/relayer-cli/src/cli_utils.rs +++ b/crates/relayer-cli/src/cli_utils.rs @@ -87,9 +87,18 @@ pub fn spawn_chain_counterparty( let chain = spawn_chain_runtime_generic::(config, chain_id)?; let channel_connection_client = channel_connection_client(&chain, port_id, channel_id).map_err(Error::supervisor)?; - let counterparty_chain = { - let counterparty_chain_id = channel_connection_client.client.client_state.chain_id(); - spawn_chain_runtime_generic::(config, &counterparty_chain_id)? + + let counterparty_chain = match channel_connection_client { + ChannelConnectionClient::SingleHop(ref chan_conn_client) => { + let counterparty_chain_id = chan_conn_client.dst_chain_id(); + spawn_chain_runtime_generic::(config, &counterparty_chain_id)? + } + + ChannelConnectionClient::Multihop(ref chan_conn_client) => { + let counterparty_chain_id = + chan_conn_client.dst_chain_id().map_err(Error::supervisor)?; + spawn_chain_runtime_generic::(config, &counterparty_chain_id)? + } }; Ok(( diff --git a/crates/relayer-cli/src/commands/create/channel.rs b/crates/relayer-cli/src/commands/create/channel.rs index 2295d09e6c..3c352e77b0 100644 --- a/crates/relayer-cli/src/commands/create/channel.rs +++ b/crates/relayer-cli/src/commands/create/channel.rs @@ -3,22 +3,25 @@ use abscissa_core::clap::Parser; use console::style; use dialoguer::Confirm; +use crate::cli_utils::{spawn_chain_runtime, ChainHandlePair}; +use crate::conclude::{exit_with_unrecoverable_error, Output}; +use crate::error::Error; +use crate::prelude::*; use ibc_relayer::chain::handle::ChainHandle; use ibc_relayer::chain::requests::{ IncludeProof, QueryClientStateRequest, QueryConnectionRequest, QueryHeight, }; use ibc_relayer::channel::Channel; use ibc_relayer::config::default::connection_delay; -use ibc_relayer::connection::Connection; +use ibc_relayer::connection::{Connection, ConnectionError}; use ibc_relayer::foreign_client::ForeignClient; use ibc_relayer_types::core::ics03_connection::connection::IdentifiedConnectionEnd; use ibc_relayer_types::core::ics04_channel::channel::Ordering; use ibc_relayer_types::core::ics04_channel::version::Version; -use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ConnectionId, PortId}; - -use crate::cli_utils::{spawn_chain_runtime, ChainHandlePair}; -use crate::conclude::{exit_with_unrecoverable_error, Output}; -use crate::prelude::*; +use ibc_relayer_types::core::ics24_host::identifier::{ + ChainId, ConnectionId, ConnectionIds, PortId, +}; +use ibc_relayer_types::core::ics33_multihop::channel_path::{ConnectionHop, ConnectionHops}; static PROMPT: &str = "Are you sure you want a new connection & clients to be created? Hermes will use default security parameters."; static HINT: &str = "Consider using the default invocation\n\nhermes create channel --a-port --b-port --a-chain --a-connection \n\nto reuse a pre-existing connection."; @@ -46,8 +49,20 @@ static HINT: &str = "Consider using the default invocation\n\nhermes create chan #[clap( override_usage = "hermes create channel [OPTIONS] --a-chain --a-connection --a-port --b-port - hermes create channel [OPTIONS] --a-chain --b-chain --a-port --b-port --new-client-connection" + hermes create channel [OPTIONS] --a-chain --b-chain --a-port --b-port --new-client-connection + + hermes create channel [OPTIONS] --a-chain --a-connection --connection-hops --a-port --b-port + + NOTE: The `--new-client-connection` option does not support connection hops. To open a multi-hop channel, please provide existing connections or initialize them manually before invoking this command." )] +// #[clap(override_usage = " +// hermes create channel [OPTIONS] --a-chain --a-connection --a-port --b-port + +// hermes create channel [OPTIONS] --a-chain --b-chain --a-port --b-port --new-client-connection +// hermes create channel [OPTIONS] --a-chain --a-connection --connection-hops --a-port --b-port + +// NOTE: The `--new-client-connection` option does not support connection hops. To open a multi-hop channel, please provide existing connections or initialize them manually before invoking this command. +// ")] pub struct CreateChannelCommand { #[clap( long = "a-chain", @@ -129,12 +144,42 @@ pub struct CreateChannelCommand { help = "Skip new_client_connection confirmation" )] yes: bool, + + // --connection-hops receives a list of ConnectionId of intermediate connections between two chains + // if they are to be connected via a multihop channel. The list of connection identifiers passed to + // `--connection-hops` starts with the identifier of the connection that comes after `--a-connection` + // in the channel path from `--a-chain` towards `--b-chain`. For example, given the following + // channel path, where `--a-chain` is Chain-A and `--b-chain` is Chain-D: + // + // +---------+ connection-1 +---------+ connection-2 +---------+ connection-3 +---------+ + // | Chain-A | ----------------> | Chain-B | ----------------> | Chain-C | ----------------> | Chain-D | + // +---------+ +---------+ +---------+ +---------+ + // + // The --connection-hops parameter should receive 'connection-2/connection-3' as argument. + #[clap( + long = "connection-hops", + visible_alias = "conn-hops", + value_name = "CONNECTION_IDS", + requires = "connection-a", + conflicts_with_all = &["new-client-connection", "chain-b"], + help_heading = "FLAGS", + help = "A list of identifiers of the intermediate connections between \ + side `a` and side `b` for a multi-hop channel, separated by slashes, \ + e.g, 'connection-1/connection-0' (optional)." + )] + connection_hops: Option, } impl Runnable for CreateChannelCommand { fn run(&self) { match &self.connection_a { - Some(conn) => self.run_reusing_connection(conn), + Some(conn) => { + if let Some(conn_hops) = &self.connection_hops { + self.run_multihop_reusing_connection(conn, conn_hops); + } else { + self.run_reusing_connection(conn); + } + } None => { if let Some(chain_b) = &self.chain_b { if self.new_client_connection { @@ -200,6 +245,8 @@ impl CreateChannelCommand { self.order, self.port_a.clone(), self.port_b.clone(), + None, + None, self.version.clone(), ) .unwrap_or_else(exit_with_unrecoverable_error); @@ -258,7 +305,169 @@ impl CreateChannelCommand { self.order, self.port_a.clone(), self.port_b.clone(), + None, + None, + self.version.clone(), + ) + .unwrap_or_else(exit_with_unrecoverable_error); + + Output::success(channel).exit(); + } + + /// Creates a new multi-hop channel, reusing existing connections as the channel path. + fn run_multihop_reusing_connection( + &self, + connection_a: &ConnectionId, + connection_hops: &ConnectionIds, + ) { + let config = app_config(); + + let mut a_side_hops = Vec::new(); // Hops from --a-chain's channel side towards --b-chain + let mut b_side_hops = Vec::new(); // Hops from --b-chain's channel side towards --a-chain + + // Join `connection_a` and `connection_hops` to create a Vec containing the identifiers + // of the connections that form the channel path from `--a-chain to `--b-chain` + let mut conn_hop_ids = connection_hops.clone().into_vec(); + conn_hop_ids.insert(0, connection_a.clone()); + + // The identifier of the chain from which we will start constructing the connection hops. + let mut chain_id = self.chain_a.clone(); + + // Iterate through the list of connection hop identifiers that constitute the + // channel path from `--a-chain` towards `--b-chain`. + for a_side_connection_id in conn_hop_ids.iter() { + let chain_handle = match spawn_chain_runtime(&config, &chain_id) { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_connection = match chain_handle.query_connection( + QueryConnectionRequest { + connection_id: a_side_connection_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_conn_client_state = match chain_handle.query_client_state( + QueryClientStateRequest { + client_id: a_side_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + // Obtain the counterparty ConnectionId and ChainId for the current connection hop + // towards `--b-chain` + let counterparty_conn_id = a_side_hop_connection + .counterparty() + .connection_id() + .unwrap_or_else(|| { + Output::error(ConnectionError::missing_counterparty_connection_id()).exit() + }); + + let counterparty_chain_id = a_side_hop_conn_client_state.chain_id().clone(); + + let counterparty_handle = match spawn_chain_runtime(&config, &counterparty_chain_id) { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the counterparty connection + let counterparty_connection = match counterparty_handle.query_connection( + QueryConnectionRequest { + connection_id: counterparty_conn_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + a_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + a_side_connection_id.clone(), + a_side_hop_connection.clone(), + ), + src_chain_id: chain_id.clone(), + dst_chain_id: a_side_hop_conn_client_state.chain_id(), + }); + + // Build the current hop from the opposite direction + b_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + counterparty_conn_id.clone(), + counterparty_connection, + ), + src_chain_id: a_side_hop_conn_client_state.chain_id(), + dst_chain_id: chain_id.clone(), + }); + + // Update chain_id to point to the next chain in the channel path + // from `--a-chain` towards `--b-chain` + chain_id = a_side_hop_conn_client_state.chain_id().clone(); + } + + // Ensure that the final chain in the path, stored in chain_id, is not the same chain as + // `--a-chain`, i.e, check that the connection hops do not lead back to `--a-chain`. + if chain_id == self.chain_a { + Output::error(Error::ics33_hops_return_to_source( + connection_hops.clone(), + self.chain_a.clone(), + )) + .exit() + } + + let a_chain = match spawn_chain_runtime(&config, &self.chain_a) { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + let b_chain = match spawn_chain_runtime(&config, &chain_id) { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + // The connection hops were assembled while traversing from --a-chain towards --b-chain. + // Reverse b_side_hops to to obtain the correct path from --b-chain to --a-chain. + b_side_hops.reverse(); + + // The first connection from `--a-chain` towards `--b-chain` + let a_side_connection = a_side_hops + .first() + .expect("a_side hops is never empty") + .connection + .clone(); + + // The first connection from `--b-chain` towards `--a-chain` + let b_side_connection = b_side_hops + .first() + .expect("b_side hops is never empty") + .connection + .clone(); + + let a_side_hops = Some(ConnectionHops::new(a_side_hops)); + let b_side_hops = Some(ConnectionHops::new(b_side_hops)); + + let channel = Channel::new_multihop( + a_chain, + b_chain, + a_side_connection, + b_side_connection, + self.order, + self.port_a.clone(), + self.port_b.clone(), + a_side_hops, // FIXME(MULTIHOP): Unsure about what to add here ('None' for now) + b_side_hops, // FIXME(MULTIHOP): Unsure about what to add here ('None' for now) self.version.clone(), + core::time::Duration::from_secs(0), // FIXME(MULTIHOP): We need to figure out how to determine the connection delay for multi-hop channels ) .unwrap_or_else(exit_with_unrecoverable_error); @@ -275,7 +484,9 @@ mod tests { use ibc_relayer_types::core::ics04_channel::channel::Ordering; use ibc_relayer_types::core::ics04_channel::version::Version; - use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ConnectionId, PortId}; + use ibc_relayer_types::core::ics24_host::identifier::{ + ChainId, ConnectionId, ConnectionIds, PortId, + }; #[test] fn test_create_channel_a_conn_required() { @@ -284,6 +495,7 @@ mod tests { chain_a: ChainId::from_string("chain_a"), chain_b: None, connection_a: Some(ConnectionId::from_str("connection_a").unwrap()), + connection_hops: None, port_a: PortId::from_str("port_id_a").unwrap(), port_b: PortId::from_str("port_id_b").unwrap(), order: Ordering::Unordered, @@ -312,6 +524,7 @@ mod tests { chain_a: ChainId::from_string("chain_a"), chain_b: None, connection_a: Some(ConnectionId::from_str("connection_a").unwrap()), + connection_hops: None, port_a: PortId::from_str("port_id_a").unwrap(), port_b: PortId::from_str("port_id_b").unwrap(), order: Ordering::Unordered, @@ -342,6 +555,7 @@ mod tests { chain_a: ChainId::from_string("chain_a"), chain_b: None, connection_a: Some(ConnectionId::from_str("connection_a").unwrap()), + connection_hops: None, port_a: PortId::from_str("port_id_a").unwrap(), port_b: PortId::from_str("port_id_b").unwrap(), order: Ordering::Ordered, @@ -372,6 +586,7 @@ mod tests { chain_a: ChainId::from_string("chain_a"), chain_b: None, connection_a: Some(ConnectionId::from_str("connection_a").unwrap()), + connection_hops: None, port_a: PortId::from_str("port_id_a").unwrap(), port_b: PortId::from_str("port_id_b").unwrap(), order: Ordering::Unordered, @@ -458,6 +673,7 @@ mod tests { chain_a: ChainId::from_string("chain_a"), chain_b: Some(ChainId::from_string("chain_b")), connection_a: None, + connection_hops: None, port_a: PortId::from_str("port_id_a").unwrap(), port_b: PortId::from_str("port_id_b").unwrap(), order: Ordering::Unordered, @@ -487,6 +703,7 @@ mod tests { chain_a: ChainId::from_string("chain_a"), chain_b: Some(ChainId::from_string("chain_b")), connection_a: None, + connection_hops: None, port_a: PortId::from_str("port_id_a").unwrap(), port_b: PortId::from_str("port_id_b").unwrap(), order: Ordering::Unordered, @@ -517,6 +734,7 @@ mod tests { chain_a: ChainId::from_string("chain_a"), chain_b: Some(ChainId::from_string("chain_b")), connection_a: None, + connection_hops: None, port_a: PortId::from_str("port_id_a").unwrap(), port_b: PortId::from_str("port_id_b").unwrap(), order: Ordering::Unordered, @@ -616,4 +834,105 @@ mod tests { ]) .is_err()) } + + #[test] + fn test_create_channel_conn_hops() { + assert_eq!( + CreateChannelCommand::parse_from([ + "test", + "--a-chain", + "chain_a", + "--a-connection", + "connection_a", + "--a-port", + "port_id_a", + "--b-port", + "port_id_b", + "--connection-hops", + "connection_a/connection_b", + ]), + CreateChannelCommand { + chain_a: ChainId::from_string("chain_a"), + chain_b: None, + connection_a: Some(ConnectionId::from_str("connection_a").unwrap()), + port_a: PortId::from_str("port_id_a").unwrap(), + port_b: PortId::from_str("port_id_b").unwrap(), + connection_hops: Some( + ConnectionIds::from_str("connection_a/connection_b").unwrap() + ), + order: Ordering::Unordered, + version: None, + new_client_connection: false, + yes: false + }, + ) + } + + #[test] + fn test_create_channel_conn_hops_alias() { + assert_eq!( + CreateChannelCommand::parse_from([ + "test", + "--a-chain", + "chain_a", + "--a-connection", + "connection_a", + "--a-port", + "port_id_a", + "--b-port", + "port_id_b", + "--conn-hops", + "connection_a/connection_b", + ]), + CreateChannelCommand { + chain_a: ChainId::from_string("chain_a"), + chain_b: None, + connection_a: Some(ConnectionId::from_str("connection_a").unwrap()), + port_a: PortId::from_str("port_id_a").unwrap(), + port_b: PortId::from_str("port_id_b").unwrap(), + connection_hops: Some( + ConnectionIds::from_str("connection_a/connection_b").unwrap() + ), + order: Ordering::Unordered, + version: None, + new_client_connection: false, + yes: false + }, + ) + } + + #[test] + fn test_create_channel_conn_hops_without_a_conn() { + assert!(CreateChannelCommand::try_parse_from([ + "test", + "--a-chain", + "chain_a", + "--a-port", + "port_id_a", + "--b-port", + "port_id_b", + "--connection-hops", + "connection_a/connection_b", + ]) + .is_err()) + } + + #[test] + fn test_create_channel_conn_hops_with_new_client_conn() { + assert!(CreateChannelCommand::try_parse_from([ + "test", + "--a-chain", + "chain_a", + "--b-chain", + "chain_b", + "--a-port", + "port_id_a", + "--b-port", + "port_id_b", + "--new-client-connection", + "--connection-hops", + "connection_a/connection_b", + ]) + .is_err()) + } } diff --git a/crates/relayer-cli/src/commands/query/packet/acks.rs b/crates/relayer-cli/src/commands/query/packet/acks.rs index 9d7a0c4f2e..aab3c27e15 100644 --- a/crates/relayer-cli/src/commands/query/packet/acks.rs +++ b/crates/relayer-cli/src/commands/query/packet/acks.rs @@ -46,16 +46,17 @@ impl QueryPacketAcknowledgementsCmd { fn execute(&self) -> Result, Error> { let config = app_config(); - let (chains, chan_conn_cli) = spawn_chain_counterparty::( + let (chains, chan_conn_client) = spawn_chain_counterparty::( &config, &self.chain_id, &self.port_id, &self.channel_id, )?; - if let Some((seqs, height)) = - acknowledgements_on_chain(&chains.src, &chains.dst, &chan_conn_cli.channel) - .map_err(Error::supervisor)? + let channel = chan_conn_client.channel(); + + if let Some((seqs, height)) = acknowledgements_on_chain(&chains.src, &chains.dst, channel) + .map_err(Error::supervisor)? { Ok(Some(PacketSeqs { seqs, height })) } else { diff --git a/crates/relayer-cli/src/commands/query/packet/pending.rs b/crates/relayer-cli/src/commands/query/packet/pending.rs index 188379969b..e9c3c6c5e7 100644 --- a/crates/relayer-cli/src/commands/query/packet/pending.rs +++ b/crates/relayer-cli/src/commands/query/packet/pending.rs @@ -4,9 +4,10 @@ use abscissa_core::clap::Parser; use serde::Serialize; use ibc_relayer::chain::counterparty::{ - channel_on_destination, pending_packet_summary, PendingPackets, + channel_on_destination, pending_packet_summary, ChannelConnectionClient, PendingPackets, }; use ibc_relayer::chain::handle::{BaseChainHandle, ChainHandle}; +use ibc_relayer::supervisor::Error as SupervisorError; use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ChannelId, PortId}; use crate::cli_utils::spawn_chain_counterparty; @@ -118,28 +119,43 @@ impl QueryPendingPacketsCmd { fn execute(&self) -> Result, Error> { let config = app_config(); - let (chains, chan_conn_cli) = spawn_chain_counterparty::( + let (chains, chan_conn_client) = spawn_chain_counterparty::( &config, &self.chain_id, &self.port_id, &self.channel_id, )?; + let channel = chan_conn_client.channel().clone(); + + let connection = match chan_conn_client { + ChannelConnectionClient::SingleHop(chan_conn_client) => chan_conn_client.connection, + ChannelConnectionClient::Multihop(chan_conn_client) => { + let connection = chan_conn_client + .connections + .last() + .ok_or( + SupervisorError::channel_connection_client_multihop_missing_connection( + self.channel_id.clone(), + ), + ) + .map_err(Error::supervisor)?; + + connection.clone() + } + }; + debug!( "fetched from source chain {} the following channel {:?}", - self.chain_id, chan_conn_cli.channel + self.chain_id, channel ); - let src_summary = pending_packet_summary(&chains.src, &chains.dst, &chan_conn_cli.channel) + let src_summary = pending_packet_summary(&chains.src, &chains.dst, &channel) .map_err(Error::supervisor)?; - let counterparty_channel = channel_on_destination( - &chan_conn_cli.channel, - &chan_conn_cli.connection, - &chains.dst, - ) - .map_err(Error::supervisor)? - .ok_or_else(|| Error::missing_counterparty_channel_id(chan_conn_cli.channel))?; + let counterparty_channel = channel_on_destination(&channel, &connection, &chains.dst) + .map_err(Error::supervisor)? + .ok_or_else(|| Error::missing_counterparty_channel_id(channel))?; let dst_summary = pending_packet_summary(&chains.dst, &chains.src, &counterparty_channel) .map_err(Error::supervisor)?; diff --git a/crates/relayer-cli/src/commands/query/packet/pending_acks.rs b/crates/relayer-cli/src/commands/query/packet/pending_acks.rs index d946a32422..9834ed0d96 100644 --- a/crates/relayer-cli/src/commands/query/packet/pending_acks.rs +++ b/crates/relayer-cli/src/commands/query/packet/pending_acks.rs @@ -51,14 +51,14 @@ impl QueryPendingAcksCmd { fn execute(&self) -> Result, Error> { let config = app_config(); - let (chains, chan_conn_cli) = spawn_chain_counterparty::( + let (chains, chan_conn_client) = spawn_chain_counterparty::( &config, &self.chain_id, &self.port_id, &self.channel_id, )?; - let channel = chan_conn_cli.channel; + let channel = chan_conn_client.channel(); debug!( "fetched from source chain {} the following channel {:?}", @@ -66,7 +66,7 @@ impl QueryPendingAcksCmd { ); let path_identifiers = PathIdentifiers::from_channel_end(channel.clone()) - .ok_or_else(|| Error::missing_counterparty_channel_id(channel))?; + .ok_or_else(|| Error::missing_counterparty_channel_id(channel.clone()))?; let acks = unreceived_acknowledgements(&chains.src, &chains.dst, &path_identifiers) .map_err(Error::supervisor)?; diff --git a/crates/relayer-cli/src/commands/query/packet/pending_sends.rs b/crates/relayer-cli/src/commands/query/packet/pending_sends.rs index c83576f7da..3d24dd0759 100644 --- a/crates/relayer-cli/src/commands/query/packet/pending_sends.rs +++ b/crates/relayer-cli/src/commands/query/packet/pending_sends.rs @@ -51,14 +51,14 @@ impl QueryPendingSendsCmd { fn execute(&self) -> Result, Error> { let config = app_config(); - let (chains, chan_conn_cli) = spawn_chain_counterparty::( + let (chains, chan_conn_client) = spawn_chain_counterparty::( &config, &self.chain_id, &self.port_id, &self.channel_id, )?; - let channel = chan_conn_cli.channel; + let channel = chan_conn_client.channel(); debug!( "fetched from source chain {} the following channel {:?}", @@ -66,7 +66,7 @@ impl QueryPendingSendsCmd { ); let path_identifiers = PathIdentifiers::from_channel_end(channel.clone()) - .ok_or_else(|| Error::missing_counterparty_channel_id(channel))?; + .ok_or_else(|| Error::missing_counterparty_channel_id(channel.clone()))?; unreceived_packets(&chains.src, &chains.dst, &path_identifiers) .map_err(Error::supervisor) diff --git a/crates/relayer-cli/src/commands/start.rs b/crates/relayer-cli/src/commands/start.rs index cc9c9193f7..49b4aab576 100644 --- a/crates/relayer-cli/src/commands/start.rs +++ b/crates/relayer-cli/src/commands/start.rs @@ -1,3 +1,4 @@ +use ibc_relayer::registry::get_global_registry; use ibc_relayer::supervisor::SupervisorOptions; use ibc_relayer::util::debug_section::DebugSection; use std::error::Error; @@ -6,9 +7,7 @@ use std::io; use abscissa_core::clap::Parser; use crossbeam_channel::Sender; -use ibc_relayer::chain::handle::{CachingChainHandle, ChainHandle}; use ibc_relayer::config::Config; -use ibc_relayer::registry::SharedRegistry; use ibc_relayer::rest; use ibc_relayer::supervisor::{cmd::SupervisorCmd, spawn_supervisor, SupervisorHandle}; @@ -59,10 +58,9 @@ impl Runnable for StartCmd { health_check: true, }; - let supervisor_handle = make_supervisor::(config, options) - .unwrap_or_else(|e| { - Output::error(format!("Hermes failed to start, last error: {e}")).exit() - }); + let supervisor_handle = make_supervisor(config, options).unwrap_or_else(|e| { + Output::error(format!("Hermes failed to start, last error: {e}")).exit() + }); match crate::config::config_path() { Some(_) => { @@ -202,11 +200,11 @@ fn spawn_telemetry_server(config: &Config) { }); } -fn make_supervisor( +fn make_supervisor( config: Config, options: SupervisorOptions, ) -> Result> { - let registry = SharedRegistry::::new(config.clone()); + let registry = get_global_registry(); spawn_telemetry_server(&config); diff --git a/crates/relayer-cli/src/commands/tx/channel.rs b/crates/relayer-cli/src/commands/tx/channel.rs index 7db47011e7..7fad835577 100644 --- a/crates/relayer-cli/src/commands/tx/channel.rs +++ b/crates/relayer-cli/src/commands/tx/channel.rs @@ -4,16 +4,22 @@ use abscissa_core::clap::Parser; use abscissa_core::Command; use ibc_relayer::chain::handle::ChainHandle; -use ibc_relayer::chain::requests::{IncludeProof, QueryConnectionRequest, QueryHeight}; -use ibc_relayer::channel::{Channel, ChannelSide}; -use ibc_relayer_types::core::ics03_connection::connection::ConnectionEnd; +use ibc_relayer::chain::requests::{ + IncludeProof, QueryChannelRequest, QueryClientStateRequest, QueryConnectionRequest, QueryHeight, +}; +use ibc_relayer::channel::{Channel, ChannelError, ChannelSide}; +use ibc_relayer::connection::ConnectionError; +use ibc_relayer_types::core::ics03_connection::connection::{ + ConnectionEnd, IdentifiedConnectionEnd, +}; use ibc_relayer_types::core::ics04_channel::channel::Ordering; use ibc_relayer_types::core::ics24_host::identifier::{ - ChainId, ChannelId, ClientId, ConnectionId, PortId, + ChainId, ChannelId, ClientId, ConnectionId, ConnectionIds, PortId, }; +use ibc_relayer_types::core::ics33_multihop::channel_path::{ConnectionHop, ConnectionHops}; use ibc_relayer_types::events::IbcEvent; -use crate::cli_utils::ChainHandlePair; +use crate::cli_utils::{spawn_chain_runtime, ChainHandlePair}; use crate::conclude::Output; use crate::error::Error; use crate::prelude::*; @@ -122,37 +128,187 @@ pub struct TxChanOpenInitCmd { help = "The channel ordering, valid options 'unordered' (default) and 'ordered'" )] order: Ordering, + + // --connection-hops receives a list of ConnectionId of intermediate connections between two chains + // if they are to be connected via a multihop channel. The list of connection identifiers passed to + // `--connection-hops` starts with the identifier of the connection that comes after `--dst-connection` + // in the channel path from `--dst-chain` towards `--src-chain`. For example, given the following + // channel path, where `--src-chain` is Chain-A and `--dst-chain` is Chain-D: + // + // +---------+ connection-3 +---------+ connection-2 +---------+ connection-1 +---------+ + // | Chain-A | <---------------- | Chain-B | <---------------- | Chain-C | <---------------- | Chain-D | + // +---------+ +---------+ +---------+ +---------+ + // + // The --connection-hops parameter should receive 'connection-2/connection-3' as argument. + #[clap( + long = "connection-hops", + value_name = "CONNECTION_HOPS", + help = "A list of identifiers of the intermediate connections between \ + a destination and a source chain for a multi-hop channel, separated by slashes, \ + e.g, 'connection-1/connection-0' (optional)" + )] + conn_hop_ids: Option, } impl Runnable for TxChanOpenInitCmd { fn run(&self) { - tx_chan_cmd!( - "ChanOpenInit", - build_chan_open_init_and_send, - self, - |chains: ChainHandlePair, dst_connection: ConnectionEnd| { - Channel { - connection_delay: Default::default(), - ordering: self.order, - a_side: ChannelSide::new( - chains.src, - ClientId::default(), - ConnectionId::default(), - self.src_port_id.clone(), - None, - None, - ), - b_side: ChannelSide::new( - chains.dst, - dst_connection.client_id().clone(), - self.dst_conn_id.clone(), - self.dst_port_id.clone(), - None, - None, - ), - } + let config = app_config(); + + let chains = match ChainHandlePair::spawn(&config, &self.src_chain_id, &self.dst_chain_id) { + Ok(chains) => chains, + Err(e) => Output::error(e).exit(), + }; + + // Attempt to retrieve --dst-connection + let dst_connection = match chains.dst.query_connection( + QueryConnectionRequest { + connection_id: self.dst_conn_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + // There always exists at least one hop, even in single-hop channels. + // A number of hops greater than one indicates a multi-hop channel. + // See: https://github.com/cosmos/ibc/blob/main/spec/core/ics-033-multi-hop/README.md?plain=1#L62 + // + // Start building the connection hops in reverse order, starting from --dst-connection and + // moving towards the source. + let dst_conn_client_state = match chains.dst.query_client_state( + QueryClientStateRequest { + client_id: dst_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + // Connection hops from b_side (--dst-chain) to a_side (--src-chain) + let mut b_side_hops = Vec::new(); + + // Build the first connection_hop, represented by --dst-connection + b_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + self.dst_conn_id.clone(), + dst_connection.clone(), + ), + src_chain_id: chains.dst.id(), + dst_chain_id: dst_conn_client_state.chain_id().clone(), + }); + + // FIXME(MULTIHOP): We are not currently checking for cycles in channel paths, e.g, the following channel hops are valid: + // ChainA -> ChainB -> ChainA -> ChainB. Still unsure if this should be allowed or not. Need to think about + // possible ramifications. + + // Check if connection IDs were provided via --connection-hops, indicating a multi-hop channel + if let Some(connection_ids) = &self.conn_hop_ids { + // Retrieve information for each of the remaining hops until the other end of the channel is reached + for connection_id in connection_ids.as_slice().iter() { + // Retrieve the ChainId of the chain to which the last hop pointed to + let chain_id = &b_side_hops + .last() + .expect("b_side_hops is never empty") + .dst_chain_id; + + // Spawn a handle for the chain pointed to by the previous hop + let chain_handle = match spawn_chain_runtime(&config, chain_id) { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the connection associated with the next hop + let hop_connection = match chain_handle.query_connection( + QueryConnectionRequest { + connection_id: connection_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the state of the client underlying the hop connection + let hop_conn_client_state = match chain_handle.query_client_state( + QueryClientStateRequest { + client_id: hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + b_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new(connection_id.clone(), hop_connection), + src_chain_id: chain_id.clone(), + dst_chain_id: hop_conn_client_state.chain_id().clone(), + }); } - ); + } + + let last_hop = &b_side_hops.last().expect("b_side_hops is never empty"); + + // Ensure that the channel path leads to the chain passed to --src-chain + if last_hop.dst_chain_id != chains.src.id() { + Output::error(Error::ics33_hops_destination_mismatch( + chains.src.id(), + chains.dst.id(), + last_hop.dst_chain_id.clone(), + )) + .exit() + } + + // FIXME(MULTIHOP): For now, pass Some(_) to connection_hops if there are multiple hops and None if there is a single one. + // This allows us to keep using existing structs as they are defined (with the single `connection_id` field) while also including + // the new `connection_hops` field. When multiple hops are present, pass Some(_) to connection_hops and use that. + // When a single hop is present, pass None to connection_hops and use the connection_id stored in `ChannelSide`. + let b_side_hops = match b_side_hops.len() { + 0 => Output::error("At least one connection hop is required for opening a channel.") + .exit(), + 1 => None, + _ => Some(ConnectionHops::new(b_side_hops)), + }; + + let channel = Channel { + ordering: self.order, + a_side: ChannelSide::new( + chains.src, + ClientId::default(), + ConnectionId::default(), + None, + self.src_port_id.clone(), + None, + None, + ), + b_side: ChannelSide::new( + chains.dst, + dst_connection.client_id().clone(), + self.dst_conn_id.clone(), + b_side_hops, + self.dst_port_id.clone(), + None, + None, + ), + connection_delay: Default::default(), + }; + + info!("message {}: {}", "ChanOpenInit", channel); + + let res: Result = channel + .build_chan_open_init_and_send() + .map_err(Error::channel); + + match res { + Ok(receipt) => Output::success(receipt).exit(), + Err(e) => Output::error(e).exit(), + } } } @@ -225,33 +381,278 @@ pub struct TxChanOpenTryCmd { impl Runnable for TxChanOpenTryCmd { fn run(&self) { - tx_chan_cmd!( - "ChanOpenTry", - build_chan_open_try_and_send, - self, - |chains: ChainHandlePair, dst_connection: ConnectionEnd| { - Channel { - connection_delay: Default::default(), - ordering: Ordering::default(), - a_side: ChannelSide::new( - chains.src, - ClientId::default(), - ConnectionId::default(), - self.src_port_id.clone(), - Some(self.src_chan_id.clone()), - None, + let config = app_config(); + + let chains = match ChainHandlePair::spawn(&config, &self.src_chain_id, &self.dst_chain_id) { + Ok(chains) => chains, + Err(e) => Output::error(e).exit(), + }; + + // Attempt to retrieve --dst-connection + let dst_connection = match chains.dst.query_connection( + QueryConnectionRequest { + connection_id: self.dst_conn_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the ChannelEnd in INIT state (--src-channel) from --src-chain + let init_channel = match chains.src.query_channel( + QueryChannelRequest { + port_id: self.src_port_id.clone(), + channel_id: self.src_chan_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((channel, _)) => channel, + Err(e) => Output::error(e).exit(), + }; + + let mut a_side_hops = Vec::new(); // Hops from --src-chain towards --dst-chain + let mut b_side_hops = Vec::new(); // Hops from --dst-chain towards --src-chain + + // Determine if the channel is a multihop channel, i.e, connection_hops > 1 + if init_channel.connection_hops().len() > 1 { + // In the case of multihop channels, we must build the channel path from --dst-chain + // to --src-chain by leveraging the information stored in the existing ChannelEnd (which contains + // the path from --src-chain to --dst-chain). We must traverse the path from --src-chain to + // --dst-chain, and, for each connection hop, obtain the counterparty connection and use it + // to build the same hop from the opposite direction. + let mut chain_id = chains.src.id().clone(); + + for a_side_connection_id in init_channel.connection_hops() { + let chain_handle = match spawn_chain_runtime(&config, &chain_id) { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_connection = match chain_handle.query_connection( + QueryConnectionRequest { + connection_id: a_side_connection_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_conn_client_state = match chain_handle.query_client_state( + QueryClientStateRequest { + client_id: a_side_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + // Obtain the counterparty ConnectionId and ChainId for the current connection hop + // towards b_side + let counterparty_conn_id = a_side_hop_connection + .counterparty() + .connection_id() + .unwrap_or_else(|| { + Output::error(ConnectionError::missing_counterparty_connection_id()).exit() + }); + + let counterparty_chain_id = a_side_hop_conn_client_state.chain_id().clone(); + + let counterparty_handle = match spawn_chain_runtime(&config, &counterparty_chain_id) + { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the counterparty connection + let counterparty_connection = match counterparty_handle.query_connection( + QueryConnectionRequest { + connection_id: counterparty_conn_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + a_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + a_side_connection_id.clone(), + a_side_hop_connection.clone(), ), - b_side: ChannelSide::new( - chains.dst, - dst_connection.client_id().clone(), - self.dst_conn_id.clone(), - self.dst_port_id.clone(), - self.dst_chan_id.clone(), - None, + src_chain_id: chain_id.clone(), + dst_chain_id: a_side_hop_conn_client_state.chain_id(), + }); + + // Build the current hop from the opposite direction + b_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + counterparty_conn_id.clone(), + counterparty_connection, ), - } + src_chain_id: a_side_hop_conn_client_state.chain_id(), + dst_chain_id: chain_id.clone(), + }); + + // Update chain_id to point to the next chain in the channel path + // from a_side towards b_side + chain_id = a_side_hop_conn_client_state.chain_id().clone(); } - ); + } else { + // If the channel path corresponds to a single-hop channel, there is only one + // connection hop from --dst-chain to --src-chain and vice-versa. + + // Get the single ConnectionId from a_side (--src-chain) to b_side (--dst-chain) + // from the connection_hops field of the ChannelEnd in INIT state + let a_side_connection_id = + init_channel.connection_hops().first().unwrap_or_else(|| { + Output::error(ChannelError::missing_connection_hops( + self.src_chan_id.clone(), + self.src_chain_id.clone(), + )) + .exit() + }); + + let a_side_hop_connection = match chains.src.query_connection( + QueryConnectionRequest { + connection_id: a_side_connection_id.clone().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_conn_client_state = match chains.src.query_client_state( + QueryClientStateRequest { + client_id: a_side_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + a_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + a_side_connection_id.clone(), + a_side_hop_connection, + ), + src_chain_id: chains.src.id(), + dst_chain_id: a_side_hop_conn_client_state.chain_id(), + }); + + let b_side_hop_connection = match chains.dst.query_connection( + QueryConnectionRequest { + connection_id: self.dst_conn_id.clone().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the state of the client underlying the hop connection + let b_side_hop_conn_client_state = match chains.dst.query_client_state( + QueryClientStateRequest { + client_id: b_side_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + // Build the single hop from --dst-chain to --src-chain + b_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + self.dst_conn_id.clone(), + b_side_hop_connection.clone(), + ), + src_chain_id: chains.dst.id(), + dst_chain_id: b_side_hop_conn_client_state.chain_id().clone(), + }); + } + + // The connection hops were assembled while traversing from --src-chain towards --dst-chain. + // Reverse them to obtain the path from --dst-chain to --src-chain. Single hops remain unchanged + // when reversed. + b_side_hops.reverse(); + + // Ensure that the reverse channel path that starts at --dst-chain correctly leads to --src-chain + if let Some(last_hop) = &b_side_hops.last() { + if last_hop.dst_chain_id != chains.src.id() { + Output::error(Error::ics33_hops_destination_mismatch( + chains.src.id(), + chains.dst.id(), + last_hop.dst_chain_id.clone(), + )) + .exit() + } + } + + // FIXME(MULTIHOP): For now, pass Some(_) to connection_hops if there are multiple hops and None if there is a single one. + // This allows us to keep using existing structs as they are defined (with the single `connection_id` field) while also including + // the new `connection_hops` field. When multiple hops are present, pass Some(_) to connection_hops and use that. + // When a single hop is present, pass None to connection_hops and use the connection_id stored in `ChannelSide`. + let a_side_hops = match a_side_hops.len() { + 0 => Output::error("At least one connection hop is required for opening a channel.") + .exit(), + 1 => None, + _ => Some(ConnectionHops::new(a_side_hops)), + }; + + let b_side_hops = match b_side_hops.len() { + 0 => Output::error("At least one connection hop is required for opening a channel.") + .exit(), + 1 => None, + _ => Some(ConnectionHops::new(b_side_hops)), + }; + + let channel = Channel { + ordering: Ordering::default(), + a_side: ChannelSide::new( + chains.src, + ClientId::default(), + ConnectionId::default(), + a_side_hops, + self.src_port_id.clone(), + Some(self.src_chan_id.clone()), + None, + ), + b_side: ChannelSide::new( + chains.dst, + dst_connection.client_id().clone(), + self.dst_conn_id.clone(), + b_side_hops, + self.dst_port_id.clone(), + self.dst_chan_id.clone(), + None, + ), + connection_delay: Default::default(), + }; + + info!("message {}: {}", "ChanOpenTry", channel); + + let res: Result = channel + .build_chan_open_try_and_send() + .map_err(Error::channel); + + match res { + Ok(receipt) => Output::success(receipt).exit(), + Err(e) => Output::error(e).exit(), + } } } @@ -326,33 +727,281 @@ pub struct TxChanOpenAckCmd { impl Runnable for TxChanOpenAckCmd { fn run(&self) { - tx_chan_cmd!( - "ChanOpenAck", - build_chan_open_ack_and_send, - self, - |chains: ChainHandlePair, dst_connection: ConnectionEnd| { - Channel { - connection_delay: Default::default(), - ordering: Ordering::default(), - a_side: ChannelSide::new( - chains.src, - ClientId::default(), - ConnectionId::default(), - self.src_port_id.clone(), - Some(self.src_chan_id.clone()), - None, + let config = app_config(); + + let chains = match ChainHandlePair::spawn(&config, &self.src_chain_id, &self.dst_chain_id) { + Ok(chains) => chains, + Err(e) => Output::error(e).exit(), + }; + + // Attempt to retrieve --dst-connection + let dst_connection = match chains.dst.query_connection( + QueryConnectionRequest { + connection_id: self.dst_conn_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the ChannelEnd in TRYOPEN state (--src-channel) from --src-chain + let tryopen_channel = match chains.src.query_channel( + QueryChannelRequest { + port_id: self.src_port_id.clone(), + channel_id: self.src_chan_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((channel, _)) => channel, + Err(e) => Output::error(e).exit(), + }; + + let mut a_side_hops = Vec::new(); // Hops from --src-chain towards --dst-chain + let mut b_side_hops = Vec::new(); // Hops from --dst-chain towards --src-chain + + // Determine if the channel is a multihop channel, i.e, connection_hops > 1 + if tryopen_channel.connection_hops().len() > 1 { + // In the case of multihop channels, we must build the channel path from --dst-chain + // to --src-chain by leveraging the information stored in the ChannelEnd in TRYOPEN state, + // which contains the path from --src-chain to --dst-chain. We must traverse the path from + // --src-chain to --dst-chain, and, for each connection hop, obtain the counterparty + // connection and use it to build the same hop from the opposite direction. + let mut chain_id = chains.src.id().clone(); + + for a_side_connection_id in tryopen_channel.connection_hops() { + let chain_handle = match spawn_chain_runtime(&config, &chain_id) { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_connection = match chain_handle.query_connection( + QueryConnectionRequest { + connection_id: a_side_connection_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_conn_client_state = match chain_handle.query_client_state( + QueryClientStateRequest { + client_id: a_side_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + // Obtain the counterparty ConnectionId and ChainId for the current connection hop + // towards b_side/destination + let counterparty_conn_id = a_side_hop_connection + .counterparty() + .connection_id() + .unwrap_or_else(|| { + Output::error(ConnectionError::missing_counterparty_connection_id()).exit() + }); + + let counterparty_chain_id = a_side_hop_conn_client_state.chain_id().clone(); + + let counterparty_handle = match spawn_chain_runtime(&config, &counterparty_chain_id) + { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the counterparty connection + let counterparty_connection = match counterparty_handle.query_connection( + QueryConnectionRequest { + connection_id: counterparty_conn_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + a_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + a_side_connection_id.clone(), + a_side_hop_connection.clone(), ), - b_side: ChannelSide::new( - chains.dst, - dst_connection.client_id().clone(), - self.dst_conn_id.clone(), - self.dst_port_id.clone(), - Some(self.dst_chan_id.clone()), - None, + src_chain_id: chain_id.clone(), + dst_chain_id: a_side_hop_conn_client_state.chain_id(), + }); + + // Build the current hop from the opposite direction + b_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + counterparty_conn_id.clone(), + counterparty_connection, ), - } + src_chain_id: a_side_hop_conn_client_state.chain_id(), + dst_chain_id: chain_id.clone(), + }); + + // Update chain_id to point to the next chain in the channel path + // from a_side towards b_side + chain_id = a_side_hop_conn_client_state.chain_id().clone(); } - ); + } else { + // If the channel path corresponds to a single-hop channel, there is only one + // connection hop from --dst-chain to --src-chain and vice-versa. + + // Get the single ConnectionId from a_side (--src-chain) to b_side (--dst-chain) + // from the connection_hops field of the ChannelEnd in TRYOPEN state + let a_side_connection_id = + tryopen_channel + .connection_hops() + .first() + .unwrap_or_else(|| { + Output::error(ChannelError::missing_connection_hops( + self.src_chan_id.clone(), + self.src_chain_id.clone(), + )) + .exit() + }); + + let a_side_hop_connection = match chains.src.query_connection( + QueryConnectionRequest { + connection_id: a_side_connection_id.clone().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_conn_client_state = match chains.src.query_client_state( + QueryClientStateRequest { + client_id: a_side_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + a_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + a_side_connection_id.clone(), + a_side_hop_connection, + ), + src_chain_id: chains.src.id(), + dst_chain_id: a_side_hop_conn_client_state.chain_id(), + }); + + let b_side_hop_connection = match chains.dst.query_connection( + QueryConnectionRequest { + connection_id: self.dst_conn_id.clone().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the state of the client underlying the hop connection + let b_side_hop_conn_client_state = match chains.dst.query_client_state( + QueryClientStateRequest { + client_id: b_side_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + // Build the single hop from --dst-chain to --src-chain + b_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + self.dst_conn_id.clone(), + b_side_hop_connection.clone(), + ), + src_chain_id: chains.dst.id(), + dst_chain_id: b_side_hop_conn_client_state.chain_id().clone(), + }); + } + + // The connection hops were assembled while traversing from --src-chain(a_side) --dst-chain(b_side). + // Reverse them to obtain the path from --dst-chain to --src-chain. Single hops remain unchanged + // when reversed. + b_side_hops.reverse(); + + // Ensure that the reverse channel path that starts at --dst-chain correctly leads to --src-chain + if let Some(last_hop) = &b_side_hops.last() { + if last_hop.dst_chain_id != chains.src.id() { + Output::error(Error::ics33_hops_destination_mismatch( + chains.src.id(), + chains.dst.id(), + last_hop.dst_chain_id.clone(), + )) + .exit() + } + } + + // FIXME(MULTIHOP): For now, pass Some(_) to connection_hops if there are multiple hops and None if there is a single one. + // This allows us to keep using existing structs as they are defined (with the single `connection_id` field) while also including + // the new `connection_hops` field. When multiple hops are present, pass Some(_) to connection_hops and use that. + // When a single hop is present, pass None to connection_hops and use the connection_id stored in `ChannelSide`. + let a_side_hops = match a_side_hops.len() { + 0 => Output::error("At least one connection hop is required for opening a channel.") + .exit(), + 1 => None, + _ => Some(ConnectionHops::new(a_side_hops)), + }; + + let b_side_hops = match b_side_hops.len() { + 0 => Output::error("At least one connection hop is required for opening a channel.") + .exit(), + 1 => None, + _ => Some(ConnectionHops::new(b_side_hops)), + }; + + let channel = Channel { + ordering: Ordering::default(), + a_side: ChannelSide::new( + chains.src, + ClientId::default(), + ConnectionId::default(), + a_side_hops, + self.src_port_id.clone(), + Some(self.src_chan_id.clone()), + None, + ), + b_side: ChannelSide::new( + chains.dst, + dst_connection.client_id().clone(), + self.dst_conn_id.clone(), + b_side_hops, + self.dst_port_id.clone(), + Some(self.dst_chan_id.clone()), + None, + ), + connection_delay: Default::default(), + }; + + info!("message {}: {}", "ChanOpenAck", channel); + + let res: Result = channel + .build_chan_open_ack_and_send() + .map_err(Error::channel); + + match res { + Ok(receipt) => Output::success(receipt).exit(), + Err(e) => Output::error(e).exit(), + } } } @@ -427,33 +1076,266 @@ pub struct TxChanOpenConfirmCmd { impl Runnable for TxChanOpenConfirmCmd { fn run(&self) { - tx_chan_cmd!( - "ChanOpenConfirm", - build_chan_open_confirm_and_send, - self, - |chains: ChainHandlePair, dst_connection: ConnectionEnd| { - Channel { - connection_delay: Default::default(), - ordering: Ordering::default(), - a_side: ChannelSide::new( - chains.src, - ClientId::default(), - ConnectionId::default(), - self.src_port_id.clone(), - Some(self.src_chan_id.clone()), - None, + let config = app_config(); + + let chains = match ChainHandlePair::spawn(&config, &self.src_chain_id, &self.dst_chain_id) { + Ok(chains) => chains, + Err(e) => Output::error(e).exit(), + }; + + // Attempt to retrieve --dst-connection + let dst_connection = match chains.dst.query_connection( + QueryConnectionRequest { + connection_id: self.dst_conn_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the ChannelEnd in OPEN state (--src-channel) from --src-chain + let open_channel = match chains.src.query_channel( + QueryChannelRequest { + port_id: self.src_port_id.clone(), + channel_id: self.src_chan_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((channel, _)) => channel, + Err(e) => Output::error(e).exit(), + }; + + let mut a_side_hops = Vec::new(); // Hops from --src-chain towards --dst-chain + let mut b_side_hops = Vec::new(); // Hops from --dst-chain towards --src-chain + + // Determine if the channel is a multihop channel, i.e, connection_hops > 1 + if open_channel.connection_hops().len() > 1 { + let mut chain_id = chains.src.id().clone(); + + for a_side_connection_id in open_channel.connection_hops() { + let chain_handle = match spawn_chain_runtime(&config, &chain_id) { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_connection = match chain_handle.query_connection( + QueryConnectionRequest { + connection_id: a_side_connection_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_conn_client_state = match chain_handle.query_client_state( + QueryClientStateRequest { + client_id: a_side_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + // Obtain the counterparty ConnectionId and ChainId for the current connection hop + // towards b_side/destination + let counterparty_conn_id = a_side_hop_connection + .counterparty() + .connection_id() + .unwrap_or_else(|| { + Output::error(ConnectionError::missing_counterparty_connection_id()).exit() + }); + + let counterparty_chain_id = a_side_hop_conn_client_state.chain_id().clone(); + + let counterparty_handle = match spawn_chain_runtime(&config, &counterparty_chain_id) + { + Ok(handle) => handle, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the counterparty connection + let counterparty_connection = match counterparty_handle.query_connection( + QueryConnectionRequest { + connection_id: counterparty_conn_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + a_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + a_side_connection_id.clone(), + a_side_hop_connection.clone(), ), - b_side: ChannelSide::new( - chains.dst, - dst_connection.client_id().clone(), - self.dst_conn_id.clone(), - self.dst_port_id.clone(), - Some(self.dst_chan_id.clone()), - None, + src_chain_id: chain_id.clone(), + dst_chain_id: a_side_hop_conn_client_state.chain_id(), + }); + + // Build the current hop from the opposite direction + b_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + counterparty_conn_id.clone(), + counterparty_connection, ), - } + src_chain_id: a_side_hop_conn_client_state.chain_id(), + dst_chain_id: chain_id.clone(), + }); + + // Update chain_id to point to the next chain in the channel path + // from a_side towards b_side + chain_id = a_side_hop_conn_client_state.chain_id().clone(); } - ); + } else { + // Get the single ConnectionId from a_side (--src-chain) to b_side (--dst-chain) + // from the connection_hops field of the ChannelEnd in OPEN state + let a_side_connection_id = + open_channel.connection_hops().first().unwrap_or_else(|| { + Output::error(ChannelError::missing_connection_hops( + self.src_chan_id.clone(), + self.src_chain_id.clone(), + )) + .exit() + }); + + let a_side_hop_connection = match chains.src.query_connection( + QueryConnectionRequest { + connection_id: a_side_connection_id.clone().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + let a_side_hop_conn_client_state = match chains.src.query_client_state( + QueryClientStateRequest { + client_id: a_side_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + a_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + a_side_connection_id.clone(), + a_side_hop_connection, + ), + src_chain_id: chains.src.id(), + dst_chain_id: a_side_hop_conn_client_state.chain_id(), + }); + + let b_side_hop_connection = match chains.dst.query_connection( + QueryConnectionRequest { + connection_id: self.dst_conn_id.clone().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((connection, _)) => connection, + Err(e) => Output::error(e).exit(), + }; + + // Retrieve the state of the client underlying the hop connection + let b_side_hop_conn_client_state = match chains.dst.query_client_state( + QueryClientStateRequest { + client_id: b_side_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) { + Ok((client_state, _)) => client_state, + Err(e) => Output::error(e).exit(), + }; + + // Build the single hop from --dst-chain to --src-chain + b_side_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new( + self.dst_conn_id.clone(), + b_side_hop_connection.clone(), + ), + src_chain_id: chains.dst.id(), + dst_chain_id: b_side_hop_conn_client_state.chain_id().clone(), + }); + } + + b_side_hops.reverse(); + + if let Some(last_hop) = &b_side_hops.last() { + if last_hop.dst_chain_id != chains.src.id() { + Output::error(Error::ics33_hops_destination_mismatch( + chains.src.id(), + chains.dst.id(), + last_hop.dst_chain_id.clone(), + )) + .exit() + } + } + + // FIXME(MULTIHOP): For now, pass Some(_) to connection_hops if there are multiple hops and None if there is a single one. + // This allows us to keep using existing structs as they are defined (with the single `connection_id` field) while also including + // the new `connection_hops` field. When multiple hops are present, pass Some(_) to connection_hops and use that. + // When a single hop is present, pass None to connection_hops and use the connection_id stored in `ChannelSide`. + let a_side_hops = match a_side_hops.len() { + 0 => Output::error("At least one connection hop is required for opening a channel.") + .exit(), + 1 => None, + _ => Some(ConnectionHops::new(a_side_hops)), + }; + + let b_side_hops = match b_side_hops.len() { + 0 => Output::error("At least one connection hop is required for opening a channel.") + .exit(), + 1 => None, + _ => Some(ConnectionHops::new(b_side_hops)), + }; + + let channel = Channel { + ordering: Ordering::default(), + a_side: ChannelSide::new( + chains.src, + ClientId::default(), + ConnectionId::default(), + a_side_hops, + self.src_port_id.clone(), + Some(self.src_chan_id.clone()), + None, + ), + b_side: ChannelSide::new( + chains.dst, + dst_connection.client_id().clone(), + self.dst_conn_id.clone(), + b_side_hops, + self.dst_port_id.clone(), + Some(self.dst_chan_id.clone()), + None, + ), + connection_delay: Default::default(), + }; + + info!("message {}: {}", "ChanOpenConfirm", channel); + + let res: Result = channel + .build_chan_open_confirm_and_send() + .map_err(Error::channel); + + match res { + Ok(receipt) => Output::success(receipt).exit(), + Err(e) => Output::error(e).exit(), + } } } @@ -540,6 +1422,7 @@ impl Runnable for TxChanCloseInitCmd { chains.src, ClientId::default(), ConnectionId::default(), + None, self.src_port_id.clone(), Some(self.src_chan_id.clone()), None, @@ -548,6 +1431,7 @@ impl Runnable for TxChanCloseInitCmd { chains.dst, dst_connection.client_id().clone(), self.dst_conn_id.clone(), + None, self.dst_port_id.clone(), Some(self.dst_chan_id.clone()), None, @@ -641,6 +1525,7 @@ impl Runnable for TxChanCloseConfirmCmd { chains.src, ClientId::default(), ConnectionId::default(), + None, self.src_port_id.clone(), Some(self.src_chan_id.clone()), None, @@ -649,6 +1534,7 @@ impl Runnable for TxChanCloseConfirmCmd { chains.dst, dst_connection.client_id().clone(), self.dst_conn_id.clone(), + None, self.dst_port_id.clone(), Some(self.dst_chan_id.clone()), None, @@ -764,6 +1650,7 @@ impl Runnable for TxChanUpgradeTryCmd { chains.src, ClientId::default(), ConnectionId::default(), + None, self.src_port_id.clone(), Some(self.src_chan_id.clone()), None, @@ -772,6 +1659,7 @@ impl Runnable for TxChanUpgradeTryCmd { chains.dst, dst_connection.client_id().clone(), self.dst_conn_id.clone(), + None, self.dst_port_id.clone(), self.dst_chan_id.clone(), None, @@ -895,6 +1783,7 @@ impl Runnable for TxChanUpgradeAckCmd { chains.src, ClientId::default(), ConnectionId::default(), + None, self.src_port_id.clone(), Some(self.src_chan_id.clone()), None, @@ -903,6 +1792,7 @@ impl Runnable for TxChanUpgradeAckCmd { chains.dst, dst_connection.client_id().clone(), self.dst_conn_id.clone(), + None, self.dst_port_id.clone(), self.dst_chan_id.clone(), None, @@ -1026,6 +1916,7 @@ impl Runnable for TxChanUpgradeConfirmCmd { chains.src, ClientId::default(), ConnectionId::default(), + None, self.src_port_id.clone(), Some(self.src_chan_id.clone()), None, @@ -1034,6 +1925,7 @@ impl Runnable for TxChanUpgradeConfirmCmd { chains.dst, dst_connection.client_id().clone(), self.dst_conn_id.clone(), + None, self.dst_port_id.clone(), self.dst_chan_id.clone(), None, @@ -1156,6 +2048,7 @@ impl Runnable for TxChanUpgradeOpenCmd { chains.src, ClientId::default(), ConnectionId::default(), + None, self.src_port_id.clone(), Some(self.src_chan_id.clone()), None, @@ -1164,6 +2057,7 @@ impl Runnable for TxChanUpgradeOpenCmd { chains.dst, dst_connection.client_id().clone(), self.dst_conn_id.clone(), + None, self.dst_port_id.clone(), self.dst_chan_id.clone(), None, @@ -1286,6 +2180,7 @@ impl Runnable for TxChanUpgradeCancelCmd { chains.src, ClientId::default(), ConnectionId::default(), + None, self.src_port_id.clone(), Some(self.src_chan_id.clone()), None, @@ -1294,6 +2189,7 @@ impl Runnable for TxChanUpgradeCancelCmd { chains.dst, dst_connection.client_id().clone(), self.dst_conn_id.clone(), + None, self.dst_port_id.clone(), self.dst_chan_id.clone(), None, @@ -1416,6 +2312,7 @@ impl Runnable for TxChanUpgradeTimeoutCmd { chains.src, ClientId::default(), ConnectionId::default(), + None, self.src_port_id.clone(), Some(self.src_chan_id.clone()), None, @@ -1424,6 +2321,7 @@ impl Runnable for TxChanUpgradeTimeoutCmd { chains.dst, dst_connection.client_id().clone(), self.dst_conn_id.clone(), + None, self.dst_port_id.clone(), self.dst_chan_id.clone(), None, @@ -1445,12 +2343,12 @@ impl Runnable for TxChanUpgradeTimeoutCmd { #[cfg(test)] mod tests { - use abscissa_core::clap::Parser; use std::str::FromStr; + use abscissa_core::clap::Parser; use ibc_relayer_types::core::{ ics04_channel::channel::Ordering, - ics24_host::identifier::{ChainId, ChannelId, ConnectionId, PortId}, + ics24_host::identifier::{ChainId, ChannelId, ConnectionId, ConnectionIds, PortId}, }; use crate::commands::tx::channel::{ @@ -1467,7 +2365,8 @@ mod tests { dst_conn_id: ConnectionId::from_str("connection_b").unwrap(), dst_port_id: PortId::from_str("port_b").unwrap(), src_port_id: PortId::from_str("port_a").unwrap(), - order: Ordering::Unordered + order: Ordering::Unordered, + conn_hop_ids: None, }, TxChanOpenInitCmd::parse_from([ "test", @@ -1485,6 +2384,38 @@ mod tests { ) } + #[test] + fn test_chan_open_init_connection_hops() { + assert_eq!( + TxChanOpenInitCmd { + dst_chain_id: ChainId::from_string("chain_b"), + src_chain_id: ChainId::from_string("chain_a"), + dst_conn_id: ConnectionId::from_str("connection_b").unwrap(), + dst_port_id: PortId::from_str("port_b").unwrap(), + src_port_id: PortId::from_str("port_a").unwrap(), + order: Ordering::Unordered, + conn_hop_ids: Some( + ConnectionIds::from_str("connection_a/connection_b/connection_c").unwrap() + ) + }, + TxChanOpenInitCmd::parse_from([ + "test", + "--dst-chain", + "chain_b", + "--src-chain", + "chain_a", + "--dst-connection", + "connection_b", + "--dst-port", + "port_b", + "--src-port", + "port_a", + "--connection-hops", + "connection_a/connection_b/connection_c", + ]) + ) + } + #[test] fn test_chan_open_init_order() { assert_eq!( @@ -1494,7 +2425,8 @@ mod tests { dst_conn_id: ConnectionId::from_str("connection_b").unwrap(), dst_port_id: PortId::from_str("port_b").unwrap(), src_port_id: PortId::from_str("port_a").unwrap(), - order: Ordering::Ordered + order: Ordering::Ordered, + conn_hop_ids: None, }, TxChanOpenInitCmd::parse_from([ "test", @@ -1523,7 +2455,8 @@ mod tests { dst_conn_id: ConnectionId::from_str("connection_b").unwrap(), dst_port_id: PortId::from_str("port_b").unwrap(), src_port_id: PortId::from_str("port_a").unwrap(), - order: Ordering::Unordered + order: Ordering::Unordered, + conn_hop_ids: None, }, TxChanOpenInitCmd::parse_from([ "test", @@ -1621,6 +2554,26 @@ mod tests { .is_err()) } + #[test] + fn test_chan_open_init_wrong_conn_hops_separator() { + assert!(TxChanOpenInitCmd::try_parse_from([ + "test", + "--dst-chain", + "chain_b", + "--src-chain", + "chain_a", + "--dst-connection", + "connection_b", + "--dst-port", + "port_b", + "--src-port", + "port_a", + "--connection-hops", + "connection-0,connection-1" + ]) + .is_err()) + } + #[test] fn test_chan_open_try_required_only() { assert_eq!( diff --git a/crates/relayer-cli/src/error.rs b/crates/relayer-cli/src/error.rs index a87c15a5d4..fdad195dd2 100644 --- a/crates/relayer-cli/src/error.rs +++ b/crates/relayer-cli/src/error.rs @@ -7,7 +7,7 @@ use tendermint::Error as TendermintError; use ibc_relayer_types::applications::ics29_fee::error::Error as FeeError; use ibc_relayer_types::core::ics04_channel::channel::IdentifiedChannelEnd; -use ibc_relayer_types::core::ics24_host::identifier::ChainId; +use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ConnectionIds}; use ibc_relayer_types::signer::SignerError; use ibc_relayer::channel::ChannelError; @@ -119,5 +119,26 @@ define_error! { KeyRing [ KeyRingError ] |_| { "keyring error" }, + + Ics33HopsDestinationMismatch + { + src_chain: ChainId, + dst_chain: ChainId, + reference_chain: ChainId, + } + | e | { + format_args!("expected a channel path from chain '{}' to chain '{}', \ + but the received connection identifier(s) lead to chain '{}' ", e.dst_chain, e.src_chain, + e.reference_chain) + }, + + Ics33HopsReturnToSource + { + channel_path: ConnectionIds, + src_chain: ChainId, + } + | e | { + format_args!("the connection hops '{}' form a channel path that starts and ends on the same chain ('{}')", e.channel_path, e.src_chain) + }, } } diff --git a/crates/relayer-types/src/core/ics03_connection/connection.rs b/crates/relayer-types/src/core/ics03_connection/connection.rs index d9c3ce9c60..99c8ce57c9 100644 --- a/crates/relayer-types/src/core/ics03_connection/connection.rs +++ b/crates/relayer-types/src/core/ics03_connection/connection.rs @@ -80,6 +80,12 @@ impl From for RawIdentifiedConnection { } } +impl From for Vec { + fn from(value: IdentifiedConnectionEnd) -> Self { + vec![value] + } +} + #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ConnectionEnd { pub state: State, diff --git a/crates/relayer-types/src/core/ics04_channel/events.rs b/crates/relayer-types/src/core/ics04_channel/events.rs index 85e2493fe5..d1a029e08f 100644 --- a/crates/relayer-types/src/core/ics04_channel/events.rs +++ b/crates/relayer-types/src/core/ics04_channel/events.rs @@ -10,7 +10,7 @@ use crate::core::ics02_client::height::Height; use crate::core::ics04_channel::error::Error; use crate::core::ics04_channel::packet::Packet; use crate::core::ics04_channel::packet::Sequence; -use crate::core::ics24_host::identifier::{ChannelId, ConnectionId, PortId}; +use crate::core::ics24_host::identifier::{ChannelId, ConnectionIds, PortId}; use crate::events::{Error as EventError, IbcEvent, IbcEventType}; use crate::timestamp::Timestamp; use crate::utils::pretty::PrettySlice; @@ -43,7 +43,7 @@ pub const UPGRADE_ERROR_RECEIPT: &str = "error_receipt"; pub struct Attributes { pub port_id: PortId, pub channel_id: Option, - pub connection_id: ConnectionId, + pub connection_id: ConnectionIds, pub counterparty_port_id: PortId, pub counterparty_channel_id: Option, } @@ -78,7 +78,7 @@ impl From for Vec { let channel_id = (CHANNEL_ID_ATTRIBUTE_KEY, channel_id.as_str()).into(); attributes.push(channel_id); } - let connection_id = (CONNECTION_ID_ATTRIBUTE_KEY, a.connection_id.as_str()).into(); + let connection_id = (CONNECTION_ID_ATTRIBUTE_KEY, a.connection_id.to_string()).into(); attributes.push(connection_id); let counterparty_port_id = ( COUNTERPARTY_PORT_ID_ATTRIBUTE_KEY, @@ -163,7 +163,7 @@ impl From for Vec { pub struct OpenInit { pub port_id: PortId, pub channel_id: Option, - pub connection_id: ConnectionId, + pub connection_id: ConnectionIds, pub counterparty_port_id: PortId, pub counterparty_channel_id: Option, } @@ -216,7 +216,7 @@ impl EventType for OpenInit { pub struct OpenTry { pub port_id: PortId, pub channel_id: Option, - pub connection_id: ConnectionId, + pub connection_id: ConnectionIds, pub counterparty_port_id: PortId, pub counterparty_channel_id: Option, } @@ -269,7 +269,7 @@ pub struct OpenAck { pub port_id: PortId, pub channel_id: Option, pub counterparty_channel_id: Option, - pub connection_id: ConnectionId, + pub connection_id: ConnectionIds, pub counterparty_port_id: PortId, } @@ -325,7 +325,7 @@ impl EventType for OpenAck { pub struct OpenConfirm { pub port_id: PortId, pub channel_id: Option, - pub connection_id: ConnectionId, + pub connection_id: ConnectionIds, pub counterparty_port_id: PortId, pub counterparty_channel_id: Option, } @@ -378,7 +378,7 @@ impl EventType for OpenConfirm { pub struct CloseInit { pub port_id: PortId, pub channel_id: ChannelId, - pub connection_id: ConnectionId, + pub connection_id: ConnectionIds, pub counterparty_port_id: PortId, pub counterparty_channel_id: Option, } @@ -455,7 +455,7 @@ impl EventType for CloseInit { pub struct CloseConfirm { pub channel_id: Option, pub port_id: PortId, - pub connection_id: ConnectionId, + pub connection_id: ConnectionIds, pub counterparty_port_id: PortId, pub counterparty_channel_id: Option, } diff --git a/crates/relayer-types/src/core/ics24_host/error.rs b/crates/relayer-types/src/core/ics24_host/error.rs index 331d578a69..a2bb868bd3 100644 --- a/crates/relayer-types/src/core/ics24_host/error.rs +++ b/crates/relayer-types/src/core/ics24_host/error.rs @@ -38,7 +38,10 @@ define_error! { | e | { format_args!("chain identifiers are expected to be in epoch format {0}", e.id) }, InvalidCounterpartyChannelId - |_| { "invalid channel id in counterparty" } + |_| { "invalid channel id in counterparty" }, + + EmptyConnectionHops + |_| { "cannot parse an empty list of connection IDs into connection hops" }, } } diff --git a/crates/relayer-types/src/core/ics24_host/identifier.rs b/crates/relayer-types/src/core/ics24_host/identifier.rs index 8c472d8947..864d9362bc 100644 --- a/crates/relayer-types/src/core/ics24_host/identifier.rs +++ b/crates/relayer-types/src/core/ics24_host/identifier.rs @@ -2,6 +2,7 @@ use std::convert::Infallible; use std::fmt::{Debug, Display, Error as FmtError, Formatter}; use std::str::FromStr; +use itertools::Itertools; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -292,6 +293,43 @@ impl PartialEq for ConnectionId { } } +// A Vec wrapper for the multi-hop channels PoC implementation +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct ConnectionIds(Vec); + +impl ConnectionIds { + pub fn as_slice(&self) -> &[ConnectionId] { + &self.0 + } + + pub fn into_vec(self) -> Vec { + self.0 + } +} + +impl FromStr for ConnectionIds { + type Err = ValidationError; + + fn from_str(s: &str) -> Result { + let connection_ids = s + .split('/') + .map(|conn_id| conn_id.parse()) + .collect::, _>>()?; + + if connection_ids.is_empty() { + return Err(ValidationError::empty_connection_hops()); + } + + Ok(Self(connection_ids)) + } +} + +impl Display for ConnectionIds { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { + write!(f, "{}", self.0.iter().join(",")) + } +} + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct PortId(String); diff --git a/crates/relayer-types/src/core/ics33_multihop/channel_path.rs b/crates/relayer-types/src/core/ics33_multihop/channel_path.rs new file mode 100644 index 0000000000..3a3633fb9e --- /dev/null +++ b/crates/relayer-types/src/core/ics33_multihop/channel_path.rs @@ -0,0 +1,54 @@ +use crate::core::ics03_connection::connection::{ConnectionEnd, IdentifiedConnectionEnd}; +use crate::core::ics24_host::identifier::{ChainId, ConnectionId}; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ConnectionHop { + pub connection: IdentifiedConnectionEnd, + pub src_chain_id: ChainId, + pub dst_chain_id: ChainId, +} + +impl ConnectionHop { + pub fn new( + connection: IdentifiedConnectionEnd, + src_chain_id: ChainId, + dst_chain_id: ChainId, + ) -> Self { + Self { + connection, + src_chain_id, + dst_chain_id, + } + } + + pub fn connection_id(&self) -> &ConnectionId { + self.connection.id() + } + pub fn connection(&self) -> &ConnectionEnd { + &self.connection.connection_end + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ConnectionHops { + pub hops: Vec, +} + +impl ConnectionHops { + pub fn new(hops: Vec) -> Self { + ConnectionHops { hops } + } + + pub fn connection_ids(&self) -> Vec { + self.hops + .iter() + .map(|hop| hop.connection.id().clone()) + .collect() + } + + pub fn hops_as_slice(&self) -> &[ConnectionHop] { + &self.hops + } +} diff --git a/crates/relayer-types/src/core/ics33_multihop/error.rs b/crates/relayer-types/src/core/ics33_multihop/error.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/relayer-types/src/core/ics33_multihop/error.rs @@ -0,0 +1 @@ + diff --git a/crates/relayer-types/src/core/ics33_multihop/mod.rs b/crates/relayer-types/src/core/ics33_multihop/mod.rs new file mode 100644 index 0000000000..d43f082967 --- /dev/null +++ b/crates/relayer-types/src/core/ics33_multihop/mod.rs @@ -0,0 +1,6 @@ +//! ICS 33: Multi-hop implementation that facilitates communication between +//! chains without a direct communication channel by leveraging intermediate connections. + +pub mod channel_path; +pub mod error; +pub mod proofs; diff --git a/crates/relayer-types/src/core/ics33_multihop/proofs.rs b/crates/relayer-types/src/core/ics33_multihop/proofs.rs new file mode 100644 index 0000000000..2c180c0d47 --- /dev/null +++ b/crates/relayer-types/src/core/ics33_multihop/proofs.rs @@ -0,0 +1,28 @@ +use crate::Height; + +#[derive(Clone, Debug, Copy, PartialEq, Eq)] +// This struct stores the heights necessary for querying multihop channel proofs. +// The first/sending chain in a channel path has no preceding chain and need not be queried +// to check if it stores a consensus state for a previous chain. Hence, 'previous_chain_consensus_height` +// is an optional field. +pub struct MultihopProofHeights { + // This is the height at which the proof(s) should be queried. Different chains along the + // channel path require different types of proofs, all of which must be queried at this height. + pub proof_query_height: Height, + + // If a proof for the consensus state of the previous chain in the channel path needs to be + // obtained, it should prove the existence of the consensus state for 'previous_chain_consensus_height'. + pub previous_chain_consensus_height: Option, +} + +impl MultihopProofHeights { + pub fn new( + proof_query_height: Height, + previous_chain_consensus_height: Option, + ) -> Self { + Self { + proof_query_height, + previous_chain_consensus_height, + } + } +} diff --git a/crates/relayer-types/src/core/mod.rs b/crates/relayer-types/src/core/mod.rs index 606a58287b..4ac47d5d04 100644 --- a/crates/relayer-types/src/core/mod.rs +++ b/crates/relayer-types/src/core/mod.rs @@ -7,3 +7,4 @@ pub mod ics04_channel; pub mod ics23_commitment; pub mod ics24_host; pub mod ics26_routing; +pub mod ics33_multihop; diff --git a/crates/relayer/src/chain/counterparty.rs b/crates/relayer/src/chain/counterparty.rs index 0710f25a6e..1cff7b6d60 100644 --- a/crates/relayer/src/chain/counterparty.rs +++ b/crates/relayer/src/chain/counterparty.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use ibc_relayer_types::{ core::{ + ics02_client::client_state::ClientState, ics03_connection::connection::{ ConnectionEnd, IdentifiedConnectionEnd, State as ConnectionState, }, @@ -31,8 +32,10 @@ use crate::chain::requests::QueryHeight; use crate::channel::ChannelError; use crate::client_state::IdentifiedAnyClientState; use crate::path::PathIdentifiers; +use crate::registry::get_global_registry; use crate::supervisor::Error; use crate::telemetry; +use crate::util::multihop::build_hops_from_connection_ids; pub fn counterparty_chain_from_connection( src_chain: &impl ChainHandle, @@ -133,13 +136,13 @@ pub fn connection_state_on_destination( } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ChannelConnectionClient { +pub struct ChannelConnectionClientSingleHop { pub channel: IdentifiedChannelEnd, pub connection: IdentifiedConnectionEnd, pub client: IdentifiedAnyClientState, } -impl ChannelConnectionClient { +impl ChannelConnectionClientSingleHop { pub fn new( channel: IdentifiedChannelEnd, connection: IdentifiedConnectionEnd, @@ -151,6 +154,141 @@ impl ChannelConnectionClient { client, } } + + pub fn dst_chain_id(&self) -> ChainId { + self.client.client_state.chain_id() + } +} + +pub struct ChannelConnectionClientMultihop { + pub channel: IdentifiedChannelEnd, + pub connections: Vec, + pub clients: Vec, +} + +impl ChannelConnectionClientMultihop { + pub fn new( + channel: IdentifiedChannelEnd, + connections: Vec, + clients: Vec, + ) -> Result { + // Multihop channels must have at least two connections in the channel path + if connections.len() < 2 { + return Err( + Error::channel_connection_client_multihop_constructor_missing_connections( + channel.channel_id.clone(), + ), + ); + } + + // Multihop channels must have at least two clients along the channel path + if clients.len() < 2 { + return Err( + Error::channel_connection_client_multihop_constructor_missing_connections( + channel.channel_id.clone(), + ), + ); + } + + // The number of connections and clients along the channel path must match + if connections.len() != clients.len() { + return Err( + Error::channel_connection_client_multihop_constructor_length_mismatch( + channel.channel_id.clone(), + ), + ); + } + + Ok(Self { + channel, + connections, + clients, + }) + } + + pub fn dst_chain_id(&self) -> Result { + let last_hop_client = self.clients.last().ok_or_else(|| { + Error::channel_connection_client_multihop_missing_client( + self.channel.channel_id.clone(), + ) + })?; + + Ok(last_hop_client.client_state.chain_id()) + } +} + +#[allow(clippy::large_enum_variant)] +pub enum ChannelConnectionClient { + SingleHop(ChannelConnectionClientSingleHop), + Multihop(ChannelConnectionClientMultihop), +} + +impl ChannelConnectionClient { + pub fn new( + channel: IdentifiedChannelEnd, + connection: impl Into>, + client: impl Into>, + ) -> Result { + let connection_vec: Vec = connection.into(); + let client_vec: Vec = client.into(); + + match (connection_vec.len(), client_vec.len()) { + // ChannelConnectionClient requires at least one connection + (0, _) => Err(Error::channel_connection_client_missing_connection( + channel.channel_id.clone(), + )), + + // ChannelConnectionClient requires at least one client + (_, 0) => Err(Error::channel_connection_client_missing_client( + channel.channel_id.clone(), + )), + + // A ChannelConnectionClient with exactly one connection and one client corresponds + // to a single-hop channel + (1, 1) => Ok(Self::SingleHop(ChannelConnectionClientSingleHop::new( + channel, + connection_vec + .first() + .expect("connection_vec is never empty") + .clone(), + client_vec + .first() + .expect("client_vec is never empty") + .clone(), + ))), + + // A ChannelConnectionClient with multiple connections and clients corresponds to + // a multihop channel + (_, _) => { + let chan_conn_client_multihop = + ChannelConnectionClientMultihop::new(channel, connection_vec, client_vec)?; + + Ok(Self::Multihop(chan_conn_client_multihop)) + } + } + } + + pub fn channel(&self) -> &IdentifiedChannelEnd { + match self { + ChannelConnectionClient::SingleHop(chan_conn_client) => &chan_conn_client.channel, + ChannelConnectionClient::Multihop(chan_conn_client) => &chan_conn_client.channel, + } + } + + /// Checks if any client associated with the channel is in a frozen state. + /// Returns `true` if the channel contains one or more frozen clients and + /// returns `false` otherwise. + pub fn has_frozen_client(&self) -> bool { + match self { + ChannelConnectionClient::SingleHop(chan_conn_client) => { + chan_conn_client.client.client_state.is_frozen() + } + ChannelConnectionClient::Multihop(chan_conn_client) => chan_conn_client + .clients + .iter() + .any(|client| client.client_state.is_frozen()), + } + } } /// Returns the [`ChannelConnectionClient`] associated with the @@ -179,22 +317,21 @@ pub fn channel_connection_client_no_checks( )); } - let connection_id = channel_end - .connection_hops() + let channel_hops = build_hops_from_connection_ids(chain, channel_end.connection_hops()) + .map_err(Error::relayer)?; + + let channel = IdentifiedChannelEnd::new(port_id.clone(), channel_id.clone(), channel_end); + let mut connections = Vec::new(); + let mut clients = Vec::new(); + + // Retrieve the first connection hop in the channel + let first_hop = channel_hops + .hops_as_slice() .first() - .ok_or_else(|| Error::missing_connection_hops(channel_id.clone(), chain.id()))?; + .ok_or_else(|| Error::missing_connection_hops(channel_id.clone(), chain.id().clone()))?; - let (connection_end, _) = chain - .query_connection( - QueryConnectionRequest { - connection_id: connection_id.clone(), - height: QueryHeight::Latest, - }, - IncludeProof::No, - ) - .map_err(Error::relayer)?; + let client_id = first_hop.connection().client_id().clone(); - let client_id = connection_end.client_id(); let (client_state, _) = chain .query_client_state( QueryClientStateRequest { @@ -205,16 +342,53 @@ pub fn channel_connection_client_no_checks( ) .map_err(Error::relayer)?; - let client = IdentifiedAnyClientState::new(client_id.clone(), client_state); - let connection = IdentifiedConnectionEnd::new(connection_id.clone(), connection_end); - let channel = IdentifiedChannelEnd::new(port_id.clone(), channel_id.clone(), channel_end); + connections.push(first_hop.connection.clone()); + + clients.push(IdentifiedAnyClientState::new( + client_id.clone(), + client_state, + )); + + let registry = get_global_registry(); + + for connection_hop in channel_hops.hops.iter().skip(1) { + // Retrieve the identifier of the chain to which the previous hop leads to + let hop_chain_id = clients + .last() + .expect("clients is never empty") + .client_state + .chain_id() + .clone(); + + let hop_chain = registry.get_or_spawn(&hop_chain_id).map_err(Error::spawn)?; - Ok(ChannelConnectionClient::new(channel, connection, client)) + let client_id = connection_hop.connection().client_id().clone(); + + let (client_state, _) = hop_chain + .query_client_state( + QueryClientStateRequest { + client_id: client_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map_err(Error::relayer)?; + + connections.push(connection_hop.connection.clone()); + clients.push(IdentifiedAnyClientState::new( + client_id.clone(), + client_state, + )); + } + + let chan_conn_client = ChannelConnectionClient::new(channel, connections, clients)?; + Ok(chan_conn_client) } /// Returns the [`ChannelConnectionClient`] associated with the /// provided port and channel id. -/// It checks that the connection is open. +/// It checks that the connection(s) are open and that the client(s) +/// are not frozen. pub fn channel_connection_client( chain: &impl ChainHandle, port_id: &PortId, @@ -223,17 +397,32 @@ pub fn channel_connection_client( let channel_connection_client = channel_connection_client_no_checks(chain, port_id, channel_id)?; - if !channel_connection_client - .connection - .connection_end - .is_open() - { - return Err(Error::connection_not_open( - channel_connection_client.connection.connection_id, - channel_id.clone(), - chain.id(), - )); + match &channel_connection_client { + ChannelConnectionClient::SingleHop(chan_conn_client) => { + // Ensure the channel's connection is open + if !chan_conn_client.connection.connection_end.is_open() { + return Err(Error::connection_not_open( + chan_conn_client.connection.connection_id.clone(), + channel_id.clone(), + chain.id(), + )); + } + } + + ChannelConnectionClient::Multihop(chan_conn_client) => { + // Ensure all connections along the channel path are open + for connection in chan_conn_client.connections.iter() { + if !connection.connection_end.is_open() { + return Err(Error::connection_not_open( + connection.connection_id.clone(), + channel_id.clone(), + chain.id().clone(), + )); + } + } + } } + Ok(channel_connection_client) } @@ -242,8 +431,17 @@ pub fn counterparty_chain_from_channel( src_channel_id: &ChannelId, src_port_id: &PortId, ) -> Result { - channel_connection_client(src_chain, src_port_id, src_channel_id) - .map(|c| c.client.client_state.chain_id()) + let channel_connection_client = + channel_connection_client(src_chain, src_port_id, src_channel_id)?; + + match channel_connection_client { + ChannelConnectionClient::SingleHop(chan_conn_client) => Ok(chan_conn_client.dst_chain_id()), + + ChannelConnectionClient::Multihop(chan_conn_client) => { + let chain_id = chan_conn_client.dst_chain_id()?; + Ok(chain_id) + } + } } fn fetch_channel_on_destination( @@ -342,6 +540,7 @@ pub fn check_channel_counterparty( .map_err(|e| ChannelError::query(target_chain.id(), e))?; let counterparty = channel_end_dst.remote; + match counterparty.channel_id { Some(actual_channel_id) => { let actual = PortChannelId { diff --git a/crates/relayer/src/chain/handle.rs b/crates/relayer/src/chain/handle.rs index 2137821e61..6ec29152b5 100644 --- a/crates/relayer/src/chain/handle.rs +++ b/crates/relayer/src/chain/handle.rs @@ -60,6 +60,8 @@ mod counting; pub use base::BaseChainHandle; pub use counting::CountingChainHandle; +pub type DefaultChainHandle = CountingAndCachingChainHandle; + pub type CachingChainHandle = cache::CachingChainHandle; pub type CountingAndCachingChainHandle = cache::CachingChainHandle>; diff --git a/crates/relayer/src/channel.rs b/crates/relayer/src/channel.rs index eb9a5a70a6..7e7b794324 100644 --- a/crates/relayer/src/channel.rs +++ b/crates/relayer/src/channel.rs @@ -5,7 +5,11 @@ use ibc_proto::google::protobuf::Any; use serde::Serialize; use tracing::{debug, error, info, warn}; -use ibc_proto::ibc::core::channel::v1::{QueryUpgradeErrorRequest, QueryUpgradeRequest}; +use ibc_proto::ibc::core::channel::v1::{ + MsgMultihopProofs, MultihopProof, QueryUpgradeErrorRequest, QueryUpgradeRequest, +}; +use ibc_proto::Protobuf; +use ibc_relayer_types::core::ics03_connection::connection::IdentifiedConnectionEnd; use ibc_relayer_types::core::ics04_channel::msgs::chan_upgrade_ack::MsgChannelUpgradeAck; use ibc_relayer_types::core::ics04_channel::msgs::chan_upgrade_cancel::MsgChannelUpgradeCancel; use ibc_relayer_types::core::ics04_channel::msgs::chan_upgrade_confirm::MsgChannelUpgradeConfirm; @@ -24,23 +28,31 @@ use ibc_relayer_types::core::ics04_channel::msgs::chan_open_init::MsgChannelOpen use ibc_relayer_types::core::ics04_channel::msgs::chan_open_try::MsgChannelOpenTry; use ibc_relayer_types::core::ics04_channel::msgs::chan_upgrade_try::MsgChannelUpgradeTry; use ibc_relayer_types::core::ics23_commitment::commitment::CommitmentProofBytes; +use ibc_relayer_types::core::ics23_commitment::merkle::apply_prefix; use ibc_relayer_types::core::ics24_host::identifier::{ ChainId, ChannelId, ClientId, ConnectionId, PortId, }; +use ibc_relayer_types::core::ics24_host::path::{ + ChannelEndsPath, ClientConsensusStatePath, ConnectionsPath, Path, +}; +use ibc_relayer_types::core::ics33_multihop::channel_path::ConnectionHops; +use ibc_relayer_types::core::ics33_multihop::proofs::MultihopProofHeights; use ibc_relayer_types::events::IbcEvent; use ibc_relayer_types::tx_msg::Msg; use ibc_relayer_types::Height; +use crate::chain::counterparty::ChannelConnectionClient; use crate::chain::counterparty::{channel_connection_client, channel_state_on_destination}; use crate::chain::handle::ChainHandle; use crate::chain::requests::{ IncludeProof, PageRequest, QueryChannelRequest, QueryConnectionChannelsRequest, - QueryConnectionRequest, QueryHeight, + QueryConnectionRequest, QueryConsensusStateRequest, QueryHeight, }; use crate::chain::tracking::TrackedMsgs; use crate::connection::Connection; use crate::foreign_client::{ForeignClient, HasExpiredOrFrozenError}; use crate::object::Channel as WorkerChannelObject; +use crate::registry::get_global_registry; use crate::supervisor::error::Error as SupervisorError; use crate::util::pretty::{PrettyDuration, PrettyOption}; use crate::util::retry::retry_with_index; @@ -98,6 +110,7 @@ pub struct ChannelSide { pub chain: Chain, client_id: ClientId, connection_id: ConnectionId, + connection_hops: Option, port_id: PortId, channel_id: Option, version: Option, @@ -119,6 +132,7 @@ impl ChannelSide { chain: Chain, client_id: ClientId, connection_id: ConnectionId, + connection_hops: Option, port_id: PortId, channel_id: Option, version: Option, @@ -127,6 +141,7 @@ impl ChannelSide { chain, client_id, connection_id, + connection_hops, port_id, channel_id, version, @@ -145,6 +160,10 @@ impl ChannelSide { &self.connection_id } + pub fn connection_hops(&self) -> Option<&ConnectionHops> { + self.connection_hops.as_ref() + } + pub fn port_id(&self) -> &PortId { &self.port_id } @@ -165,6 +184,7 @@ impl ChannelSide { chain: mapper(self.chain), client_id: self.client_id, connection_id: self.connection_id, + connection_hops: self.connection_hops, port_id: self.port_id, channel_id: self.channel_id, version: self.version, @@ -189,6 +209,7 @@ impl Display for Channel Channel { ordering: Ordering, a_port: PortId, b_port: PortId, + a_side_hops: Option, + b_side_hops: Option, version: Option, ) -> Result { let src_connection_id = connection @@ -216,7 +239,8 @@ impl Channel { a_side: ChannelSide::new( connection.src_chain(), connection.src_client_id().clone(), - src_connection_id.clone(), + src_connection_id.clone(), // FIXME(MULTIHOP): We may want to remove this in favor of using only a_side_hops + a_side_hops, a_port, Default::default(), version.clone(), @@ -224,7 +248,8 @@ impl Channel { b_side: ChannelSide::new( connection.dst_chain(), connection.dst_client_id().clone(), - dst_connection_id.clone(), + dst_connection_id.clone(), // FIXME(MULTIHOP): We may want to remove this in favor of using only b_side_hops + b_side_hops, b_port, Default::default(), version, @@ -237,6 +262,47 @@ impl Channel { Ok(channel) } + pub fn new_multihop( + a_chain: ChainA, + b_chain: ChainB, + a_side_connection: IdentifiedConnectionEnd, + b_side_connection: IdentifiedConnectionEnd, + ordering: Ordering, + a_port: PortId, + b_port: PortId, + a_side_hops: Option, + b_side_hops: Option, + version: Option, + connection_delay: Duration, + ) -> Result { + let mut channel = Self { + ordering, + a_side: ChannelSide::new( + a_chain, + a_side_connection.end().client_id().clone(), + a_side_connection.id().clone(), // FIXME(MULTIHOP): We may want to remove this in favor of using only a_side_hops + a_side_hops, + a_port, + Default::default(), + version.clone(), + ), + b_side: ChannelSide::new( + b_chain, + b_side_connection.end().client_id().clone(), + b_side_connection.id().clone(), // FIXME(MULTIHOP): We may want to remove this in favor of using only b_side_hops + b_side_hops, + b_port, + Default::default(), + version, + ), + connection_delay, + }; + + channel.handshake()?; + + Ok(channel) + } + pub fn restore_from_event( chain: ChainA, counterparty_chain: ChainB, @@ -250,11 +316,15 @@ impl Channel { let port_id = channel_event_attributes.port_id.clone(); let channel_id = channel_event_attributes.channel_id; - let connection_id = channel_event_attributes.connection_id.clone(); + // FIXME(MULTIHOP): connection_id is an instance of ConnectionIds(Vec), but ChannelSide::new() requires + // a single ConnectionId. To avoid further changes in ChannelSide, get only the 0th element for now. + // In the future, modify ChannelSide to use a Vec. + let connection_id = channel_event_attributes.connection_id.as_slice()[0].clone(); + let (connection, _) = chain .query_connection( QueryConnectionRequest { - connection_id: connection_id.clone(), + connection_id: connection_id.clone(), // FIXME(MULTIHOP): Add support for multihop connections queries. height: QueryHeight::Latest, }, IncludeProof::No, @@ -275,7 +345,8 @@ impl Channel { a_side: ChannelSide::new( chain, connection.client_id().clone(), - connection_id, + connection_id.clone(), + None, // FIXME(MULTIHOP): Unsure what to add here ('None' for now), can we get the hops from the event? port_id, channel_id, // The event does not include the version. @@ -286,6 +357,7 @@ impl Channel { counterparty_chain, connection.counterparty().client_id().clone(), counterparty_connection_id.clone(), + None, // FIXME(MULTIHOP): Unsure what to add here ('None' for now), can we get the hops from the event? channel_event_attributes.counterparty_port_id.clone(), channel_event_attributes.counterparty_channel_id, None, @@ -352,6 +424,7 @@ impl Channel { chain.clone(), a_connection.client_id().clone(), a_connection_id.clone(), + None, // FIXME(MULTIHOP): Unsure about what to add here ('None' for now) channel.src_port_id.clone(), Some(channel.src_channel_id.clone()), None, @@ -360,6 +433,7 @@ impl Channel { counterparty_chain.clone(), a_connection.counterparty().client_id().clone(), b_connection_id.clone(), + None, // FIXME(MULTIHOP): Unsure about what to add here ('None' for now) a_channel.remote.port_id.clone(), a_channel.remote.channel_id.clone(), None, @@ -753,12 +827,30 @@ impl Channel { channel_connection_client(self.src_chain(), self.src_port_id(), channel_id) .map_err(|e| ChannelError::query_channel(channel_id.clone(), e))?; - channel_state_on_destination( - &channel_deps.channel, - &channel_deps.connection, - self.dst_chain(), - ) - .map_err(|e| ChannelError::query_channel(channel_id.clone(), e)) + match channel_deps { + ChannelConnectionClient::SingleHop(chan_conn_client) => channel_state_on_destination( + &chan_conn_client.channel, + &chan_conn_client.connection, + self.dst_chain(), + ) + .map_err(|e| ChannelError::query_channel(channel_id.clone(), e)), + + ChannelConnectionClient::Multihop(chan_conn_client) => { + let last_hop_connection = + &chan_conn_client.connections.last().ok_or_else(|| { + ChannelError::channel_connection_client_multihop_missing_connections( + channel_id.clone(), + ) + })?; + + channel_state_on_destination( + &chan_conn_client.channel, + last_hop_connection, + self.dst_chain(), + ) + .map_err(|e| ChannelError::query_channel(channel_id.clone(), e)) + } + } } pub fn handshake_step( @@ -891,6 +983,53 @@ impl Channel { }) } + pub fn build_update_client_on_last_hop( + &self, + height: Height, + ) -> Result, ChannelError> { + let channel_id = self + .a_side + .channel_id() + .ok_or(ChannelError::missing_local_channel_id())?; + + // Ensure connection_hops is not empty + let connection_hops = self.a_side.connection_hops().ok_or_else(|| { + ChannelError::missing_local_connection_hops( + channel_id.clone(), + self.a_side.chain_id().clone(), + ) + })?; + + // Get the last connection hop in the channel path + let last_hop = connection_hops.hops.iter().last().ok_or_else(|| { + ChannelError::missing_local_connection_hops( + channel_id.clone(), + self.a_side.chain_id().clone(), + ) + })?; + + // Get access to the registry to retrieve or spawn chain handles + let registry = get_global_registry(); + + let last_hop_src_chain = registry + .get_or_spawn(&last_hop.src_chain_id) + .map_err(ChannelError::spawn)?; + + // Restore the client that is hosted by the channel path's (from a_side towards b_side) + // destination chain to track the state of the penultimate chain. + let client = ForeignClient::restore( + self.dst_client_id().clone(), + self.dst_chain().clone(), + last_hop_src_chain.clone(), + ); + + // Build and return a MsgUpdateClient to update the client hosted by the channel path's + // destination chain to track the channel path's penultimate chain. + client.wait_and_build_update_client(height).map_err(|e| { + ChannelError::client_operation(self.dst_client_id().clone(), self.dst_chain().id(), e) + }) + } + pub fn build_chan_open_init(&self) -> Result, ChannelError> { let signer = self .dst_chain() @@ -921,7 +1060,11 @@ impl Channel { State::Init, self.ordering, counterparty, - vec![self.dst_connection_id().clone()], + self.b_side + .connection_hops + .as_ref() + .map(|hops| hops.connection_ids()) + .unwrap_or_else(|| vec![self.dst_connection_id().clone()]), version, Sequence::from(0), ); @@ -984,8 +1127,8 @@ impl Channel { .dst_channel_id() .ok_or_else(ChannelError::missing_counterparty_channel_id)?; - // If there is a channel present on the destination chain, - // the counterparty should look like this: + // If there is a channel end on the destination chain, the channel end's + // counterparty should match the specified source chain, i.e, the following: let counterparty = Counterparty::new(self.src_port_id().clone(), self.src_channel_id().cloned()); @@ -997,16 +1140,24 @@ impl Channel { _ => State::Uninitialized, }; + // The configuration of the channel end in the destination chain should match the following let dst_expected_channel = ChannelEnd::new( highest_state, self.ordering, counterparty, - vec![self.dst_connection_id().clone()], + // FIXME(MULTIHOP): This is a temporary workaround while --connection-hops has not yet replaced + // --dst-connection in the tx CLI. If connection_hops are 'None' pass '--dst-connection' + // to the field 'ChannelEnd.connection_hops' for now. + self.b_side + .connection_hops + .as_ref() + .map(|hops| hops.connection_ids()) + .unwrap_or_else(|| vec![self.dst_connection_id().clone()]), Version::empty(), Sequence::from(0), ); - // Retrieve existing channel + // Retrieve existing channel from the destination let (dst_channel, _) = self .dst_chain() .query_channel( @@ -1030,6 +1181,458 @@ impl Channel { Ok(dst_expected_channel) } + pub fn update_channel_path_clients(&self) -> Result, ChannelError> { + let channel_id = self + .a_side + .channel_id() + .ok_or(ChannelError::missing_local_channel_id())?; + + // Make sure the connection_hops are not 'None' + let connection_hops = self.a_side.connection_hops().ok_or_else(|| { + ChannelError::missing_local_connection_hops( + channel_id.clone(), + self.a_side.chain_id().clone(), + ) + })?; + + // Get the sending chain's latest height. This height will be used to query the key proof. + let query_height = self + .src_chain() + .query_latest_height() + .map_err(|e| ChannelError::query(self.src_chain().id(), e))?; + + // This height will be provided as the target to update the client hosted by the next + // chain in this channel path. The height must be equal to 'query_height' + 1, as any proofs + // queried at height `query_height` can only be verified by having access to the application's + // state Merkle Root (the AppHash) for height 'query_height', which is only included in the + // subsequent block. + let mut target_client_height = query_height.increment(); + + // Store the heights at which the proofs must be queried after the clients in the channel + // path are updated. Here, for the first chain in the channel path, store only the height + // at which to query the proofs since the chain does not have to be queried for a consensus + // state from a previous chain in the path. + let mut proof_heights = vec![MultihopProofHeights::new(query_height, None)]; + + // Get access to the registry to get or spawn chain handles + let registry = get_global_registry(); + + // Update the clients along the channel path from the sending chain (a_side) + // towards the receiving chain (b_side), except for the client on the destination. + // The client hosted by the destination to track the penultimate chain in the channel path + // will receive the MsgUpdateClient together with the main message being sent. + for conn_hop in connection_hops + .hops + .iter() + .take(connection_hops.hops.len() - 1) + { + let hop_src_chain = registry + .get_or_spawn(&conn_hop.src_chain_id) + .map_err(ChannelError::spawn)?; + + let hop_dst_chain = registry + .get_or_spawn(&conn_hop.dst_chain_id) + .map_err(ChannelError::spawn)?; + + // Restore the client hosted by hop_dst_chain to track the state of hop_src_chain + let client = ForeignClient::restore( + conn_hop.connection().counterparty().client_id().clone(), + hop_dst_chain.clone(), + hop_src_chain.clone(), + ); + + // Build and send a MsgUpdateClient to update the client so that it tracks the + // consensus state for the height 'target_client_height' + client + .build_update_client_and_send(QueryHeight::Specific(target_client_height), None) + .map_err(|e| { + ChannelError::client_operation(client.id().clone(), hop_dst_chain.id(), e) + })?; + + // Fetch the UpdateClient event which updates the client to track 'target_client_height' + let maybe_update = client + .fetch_update_client_event(target_client_height) + .map_err(|e| { + ChannelError::client_operation(client.id().clone(), hop_dst_chain.id(), e) + })?; + + // Retrieve the height at which the UpdateClient message was included in the chain. + // This height can be used to query for the consensus state that corresponds to + // `target_client_height`. + let update_event_height = match maybe_update { + Some((_, height)) => height, + None => { + return Err(ChannelError::failed_channel_path_client_update( + client.id().clone(), + hop_dst_chain.id(), + channel_id.clone(), + self.a_side.chain_id().clone(), + )) + } + }; + + proof_heights.push(MultihopProofHeights::new( + // The height at which the consensus state for the previous chain was included + // in this chain. Consensus proofs and other required proofs will be queried at + // this height. + update_event_height, + // The previous chain's consensus height received in a client update that happened + // at height 'update_event_height'. This height should be used when querying for the + // existence of a consensus state for the previous chain at height 'update_event_height'. + Some(target_client_height), + )); + + // The height to use for updating the client that tracks the current chain on the next + // chain in the channel path. Allows the next chain to verify proofs queried at height + // 'update_event_height' in the current chain. Not used if the next chain is the channel + // path's destination chain. + target_client_height = update_event_height.increment(); + } + + Ok(proof_heights) + } + + pub fn build_multihop_channel_proofs( + &self, + proof_heights: &[MultihopProofHeights], + ) -> Result { + let src_channel_id = self + .a_side + .channel_id() + .ok_or(ChannelError::missing_local_channel_id())?; + + let connection_hops = &self + .a_side + .connection_hops() + .ok_or_else(|| { + ChannelError::missing_local_connection_hops( + src_channel_id.clone(), + self.a_side.chain_id().clone(), + ) + })? + .hops; + + // Ensure the number of proof heights matches the number of connection hops in the channel path + if proof_heights.len() != connection_hops.len() { + return Err(ChannelError::missing_multihop_proof_heights( + src_channel_id.clone(), + self.src_chain().id().clone(), + )); + } + + // Get the first 'proof_query_height' in 'proof_heights' and use it to query for the key proof + // in the sending/source chain + let query_height = QueryHeight::Specific( + proof_heights + .first() + .ok_or_else(|| { + ChannelError::missing_multihop_proof_heights( + src_channel_id.clone(), + self.src_chain().id().clone(), + ) + })? + .proof_query_height, + ); + + let (src_channel, maybe_channel_proof) = self + .src_chain() + .query_channel( + QueryChannelRequest { + port_id: self.src_port_id().clone(), + channel_id: src_channel_id.clone(), + height: query_height, + }, + IncludeProof::Yes, + ) + .map_err(|e| ChannelError::query(self.src_chain().id(), e))?; + + let Some(channel_proof) = maybe_channel_proof else { + return Err(ChannelError::queried_proof_not_found()); + }; + + let channel_proof_bytes = + CommitmentProofBytes::try_from(channel_proof).map_err(ChannelError::malformed_proof)?; + + let key_path = vec![Path::ChannelEnds(ChannelEndsPath( + self.src_port_id().clone(), + src_channel_id.clone(), + )) + .to_string()]; + + let store_prefix = self + .src_chain() + .query_commitment_prefix() + .map_err(|e| ChannelError::chain_query(self.src_chain().id(), e))?; + + let prefixed_key = apply_prefix(&store_prefix, key_path); + + let key_proof = MultihopProof { + proof: channel_proof_bytes.into_bytes(), + value: src_channel.encode_vec(), + prefixed_key: Some(prefixed_key), + }; + + let mut connection_proofs = Vec::new(); + let mut consensus_proofs = Vec::new(); + + // Get access to registry to retrieve or spawn chain handles + let registry = get_global_registry(); + + // Iterate through the connection hops from the sending chain towards the receiving chain, + // except for the last hop (from penultimate chain to receiving/destination chain). + // For every connection hop: + // 1. Query the proof of connection from the hop's destination chain towards the hop's + // source chain. + // 2. Query the proof of the hop's source chain consensus state on the client that tracks + // the hop's source chain in the the hop's destination chain. + for (conn_hop, proof_height) in connection_hops + .iter() + .take(connection_hops.len() - 1) + .zip(proof_heights.iter().skip(1)) + { + // The height to be used when querying for proofs in the hop's destination chain. + let query_height = QueryHeight::Specific(proof_height.proof_query_height); + + // The consensus height of the hop's source chain that must exist and be proved in the + // correspondent client in the hop's destination chain + let consensus_height_to_prove = proof_height + .previous_chain_consensus_height + .ok_or_else(|| { + ChannelError::missing_multihop_proof_heights( + src_channel_id.clone(), + self.src_chain().id(), + ) + })?; + + let hop_dst_chain = registry + .get_or_spawn(&conn_hop.dst_chain_id) + .map_err(ChannelError::spawn)?; + + // Get ConnectionId for the connection leaving the hop's destination chain towards the + // hop's source chain + let hop_dst_connection_id = conn_hop + .connection() + .counterparty() + .connection_id() + .ok_or(ChannelError::missing_counterparty_connection_id())?; + + // On the connection hop's destination chain, query for the connection leading back to + // the connection hop's source chain. Retrieve the connection proof as well. This is + // required because the receiving chain needs to validate the channel path connections + // from the the receiving chain's side towards the sending chain. + let (hop_dst_connection, maybe_conn_proof) = hop_dst_chain + .query_connection( + QueryConnectionRequest { + connection_id: hop_dst_connection_id.clone(), + height: query_height, + }, + IncludeProof::Yes, + ) + .map_err(|e| ChannelError::query(hop_dst_chain.id(), e))?; + + let Some(conn_proof) = maybe_conn_proof else { + return Err(ChannelError::queried_proof_not_found()); + }; + + let conn_proof_bytes = CommitmentProofBytes::try_from(conn_proof) + .map_err(ChannelError::malformed_proof)?; + + let connection_path = + vec![Path::Connections(ConnectionsPath(hop_dst_connection_id.clone())).to_string()]; + + let prefixed_key = apply_prefix(&store_prefix, connection_path); + + let hop_dst_connection_proof = MultihopProof { + proof: conn_proof_bytes.into_bytes(), + value: hop_dst_connection.encode_vec(), + prefixed_key: Some(prefixed_key), + }; + + // Query the client on the connection hop's destination for the consensus state of the + // connection hop's source chain, denoted by 'consensus_height_to_prove' + let (consensus_state, maybe_consensus_state_proof) = hop_dst_chain + .query_consensus_state( + QueryConsensusStateRequest { + client_id: conn_hop.connection().counterparty().client_id().clone(), + consensus_height: consensus_height_to_prove, + query_height, + }, + IncludeProof::Yes, + ) + .map_err(|e| ChannelError::query(hop_dst_chain.id(), e))?; + + let Some(consensus_state_proof) = maybe_consensus_state_proof else { + return Err(ChannelError::queried_proof_not_found()); + }; + + let consensus_state_proof_bytes = CommitmentProofBytes::try_from(consensus_state_proof) + .map_err(ChannelError::malformed_proof)?; + + let consensus_state_path = vec![Path::ClientConsensusState(ClientConsensusStatePath { + client_id: conn_hop.connection().counterparty().client_id().clone(), + epoch: consensus_height_to_prove.revision_number(), + height: consensus_height_to_prove.revision_height(), + }) + .to_string()]; + + let prefixed_key = apply_prefix(&store_prefix, consensus_state_path); + + let hop_consensus_proof = MultihopProof { + proof: consensus_state_proof_bytes.into_bytes(), + value: consensus_state.encode_vec(), + prefixed_key: Some(prefixed_key), + }; + + connection_proofs.push(hop_dst_connection_proof); + consensus_proofs.push(hop_consensus_proof); + } + + // The receiving chain will validate the proofs from the receiving side towards the sending + // chain's side. Since the proofs were constructed while iterating through the path from the + // sending side towards the receiving side, reverse them to match the order expected by the + // receiving chain. + connection_proofs.reverse(); + consensus_proofs.reverse(); + + Ok(MsgMultihopProofs { + key_proof: Some(key_proof), + connection_proofs, + consensus_proofs, + }) + } + + pub fn build_multihop_chan_open_try(&self) -> Result, ChannelError> { + // Source channel ID must be specified + let src_channel_id = self + .src_channel_id() + .ok_or_else(ChannelError::missing_local_channel_id)?; + + // Channel must exist on source + let (src_channel, _) = self + .src_chain() + .query_channel( + QueryChannelRequest { + port_id: self.src_port_id().clone(), + channel_id: src_channel_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map_err(|e| ChannelError::query(self.src_chain().id(), e))?; + + // The channel end in src_chain must be in 'INIT' state to send a 'MsgChannelOpenTry' + if !src_channel.state_matches(&State::Init) { + return Err(ChannelError::unexpected_channel_state( + src_channel_id.clone(), + self.src_chain().id().clone(), + State::Init, + *src_channel.state(), + )); + } + + if src_channel.counterparty().port_id() != self.dst_port_id() { + return Err(ChannelError::mismatch_port( + self.dst_chain().id(), + self.dst_port_id().clone(), + self.src_chain().id(), + src_channel.counterparty().port_id().clone(), + src_channel_id.clone(), + )); + } + + self.dst_chain() + .query_connection( + QueryConnectionRequest { + connection_id: self.dst_connection_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map_err(|e| ChannelError::query(self.dst_chain().id(), e))?; + + // Update the clients along the channel path and store the heights necessary for querying + // multihop proofs. 'proof_heights' contains the height at which proofs should be queried, + // ordered from the sending chain to the penultimate chain in the channel path. In order to + // verify proofs queried at the height 'proof_query_height' stored in 'proof_heights', the + // client on the chain that receives the proof must possess the consensus state corresponding + // to height 'proof_query_height + 1' of the chain on which the proofs were queried. + let proof_heights = self.update_channel_path_clients()?; + + // Get the proof heights for the last hop in the channel path, which connects the penultimate + // chain to the destination. + let last_hop_heights = proof_heights.last().ok_or_else(|| { + ChannelError::missing_multihop_proof_heights( + src_channel_id.clone(), + self.src_chain().id(), + ) + })?; + + // Build the message to update the client on the channel path's destination chain. Because + // proofs are queried at height 'last_hop_heights.proof_query_height' in the penultimate chain, + // update the client that tracks the penultimate chain in the destination using height + // 'last_hop_heights.proof_query_height' + 1 to allow for proof verification. + let mut msgs = + self.build_update_client_on_last_hop(last_hop_heights.proof_query_height.increment())?; + + let multihop_proofs = self.build_multihop_channel_proofs(&proof_heights)?; + + let multihop_proof_bytes = prost::Message::encode_to_vec(&multihop_proofs); + + let counterparty = + Counterparty::new(self.src_port_id().clone(), self.src_channel_id().cloned()); + + // Reuse the version that was either set on ChanOpenInit or overwritten by the application. + let version = src_channel.version().clone(); + + let proofs = ibc_relayer_types::proofs::Proofs::new( + CommitmentProofBytes::try_from(multihop_proof_bytes).unwrap(), + None, + None, + None, + None, + last_hop_heights.proof_query_height.increment(), + ) + .map_err(ChannelError::malformed_proof)?; + + let channel = ChannelEnd::new( + State::TryOpen, + *src_channel.ordering(), + counterparty, + self.b_side + .connection_hops + .as_ref() + .map(|hops| hops.connection_ids()) + .unwrap_or_else(|| vec![self.dst_connection_id().clone()]), + version, + Sequence::from(0), + ); + + // Get signer + let signer = self + .dst_chain() + .get_signer() + .map_err(|e| ChannelError::fetch_signer(self.dst_chain().id(), e))?; + + let previous_channel_id = if src_channel.counterparty().channel_id.is_none() { + self.b_side.channel_id.clone() + } else { + src_channel.counterparty().channel_id.clone() + }; + + // Build the domain type message + let new_msg = MsgChannelOpenTry { + port_id: self.dst_port_id().clone(), + previous_channel_id, + counterparty_version: src_channel.version().clone(), + channel, + proofs, + signer, + }; + + msgs.push(new_msg.to_any()); + Ok(msgs) + } + pub fn build_chan_open_try(&self) -> Result, ChannelError> { // Source channel ID must be specified let src_channel_id = self @@ -1049,6 +1652,16 @@ impl Channel { ) .map_err(|e| ChannelError::query(self.src_chain().id(), e))?; + // The channel end in src_chain must be in 'INIT' state to send a 'MsgChannelOpenTry' + if !src_channel.state_matches(&State::Init) { + return Err(ChannelError::unexpected_channel_state( + src_channel_id.clone(), + self.src_chain().id().clone(), + State::Init, + *src_channel.state(), + )); + } + if src_channel.counterparty().port_id() != self.dst_port_id() { return Err(ChannelError::mismatch_port( self.dst_chain().id(), @@ -1125,7 +1738,11 @@ impl Channel { } pub fn build_chan_open_try_and_send(&self) -> Result { - let dst_msgs = self.build_chan_open_try()?; + let dst_msgs = if self.a_side.connection_hops.is_some() { + self.build_multihop_chan_open_try()? + } else { + self.build_chan_open_try()? + }; let tm = TrackedMsgs::new_static(dst_msgs, "ChannelOpenTry"); @@ -1155,7 +1772,7 @@ impl Channel { } } - pub fn build_chan_open_ack(&self) -> Result, ChannelError> { + pub fn build_multihop_chan_open_ack(&self) -> Result, ChannelError> { // Source and destination channel IDs must be specified let src_channel_id = self .src_channel_id() @@ -1164,9 +1781,99 @@ impl Channel { .dst_channel_id() .ok_or_else(ChannelError::missing_counterparty_channel_id)?; - // Check that the destination chain will accept the Ack message + // Channel must exist on source + let (src_channel, _) = self + .src_chain() + .query_channel( + QueryChannelRequest { + port_id: self.src_port_id().clone(), + channel_id: src_channel_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map_err(|e| ChannelError::query(self.src_chain().id(), e))?; + + // Check that the destination chain will accept the MsgChannelOpenAck self.validated_expected_channel(ChannelMsgType::OpenAck)?; + self.dst_chain() + .query_connection( + QueryConnectionRequest { + connection_id: self.dst_connection_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map_err(|e| ChannelError::query(self.dst_chain().id(), e))?; + + // Update the clients along the channel path and store the heights necessary for querying + // multihop proofs. 'proof_heights' contains the height at which proofs should be queried, + // ordered from the sending chain to the penultimate chain in the channel path. In order to + // verify proofs queried at the height 'proof_query_height' stored in 'proof_heights', the + // client on the chain that receives the proof must possess the consensus state corresponding + // to height 'proof_query_height + 1' of the chain on which the proofs were queried. + let proof_heights = self.update_channel_path_clients()?; + + // Get the proof heights for the last hop in the channel path, which connects the penultimate + // chain to the destination. + let last_hop_heights = proof_heights.last().ok_or_else(|| { + ChannelError::missing_multihop_proof_heights( + src_channel_id.clone(), + self.src_chain().id(), + ) + })?; + + // Build the message to update the client on the channel path's destination chain. Because + // proofs are queried at height 'last_hop_heights.proof_query_height' in the penultimate chain, + // update the client that tracks the penultimate chain in the destination using height + // 'last_hop_heights.proof_query_height' + 1 to allow for proof verification. + let mut msgs = + self.build_update_client_on_last_hop(last_hop_heights.proof_query_height.increment())?; + + let multihop_proofs = self.build_multihop_channel_proofs(&proof_heights)?; + + let multihop_proof_bytes = prost::Message::encode_to_vec(&multihop_proofs); + + let proofs = ibc_relayer_types::proofs::Proofs::new( + CommitmentProofBytes::try_from(multihop_proof_bytes).unwrap(), + None, + None, + None, + None, + last_hop_heights.proof_query_height.increment(), + ) + .map_err(ChannelError::malformed_proof)?; + + // Get signer + let signer = self + .dst_chain() + .get_signer() + .map_err(|e| ChannelError::fetch_signer(self.dst_chain().id(), e))?; + + // Build the domain type message + let new_msg = MsgChannelOpenAck { + port_id: self.dst_port_id().clone(), + channel_id: dst_channel_id.clone(), + counterparty_channel_id: src_channel_id.clone(), + counterparty_version: src_channel.version().clone(), + proofs, + signer, + }; + + msgs.push(new_msg.to_any()); + Ok(msgs) + } + + pub fn build_chan_open_ack(&self) -> Result, ChannelError> { + // Source and destination channel IDs must be specified + let src_channel_id = self + .src_channel_id() + .ok_or_else(ChannelError::missing_local_channel_id)?; + let dst_channel_id = self + .dst_channel_id() + .ok_or_else(ChannelError::missing_counterparty_channel_id)?; + // Channel must exist on source let (src_channel, _) = self .src_chain() @@ -1180,6 +1887,9 @@ impl Channel { ) .map_err(|e| ChannelError::query(self.src_chain().id(), e))?; + // Check that the destination chain will accept the MsgChannelOpenAck + self.validated_expected_channel(ChannelMsgType::OpenAck)?; + // Connection must exist on destination self.dst_chain() .query_connection( @@ -1228,7 +1938,11 @@ impl Channel { fn do_build_chan_open_ack_and_send( channel: &Channel, ) -> Result { - let dst_msgs = channel.build_chan_open_ack()?; + let dst_msgs = if channel.a_side.connection_hops.is_some() { + channel.build_multihop_chan_open_ack()? + } else { + channel.build_chan_open_ack()? + }; let tm = TrackedMsgs::new_static(dst_msgs, "ChannelOpenAck"); @@ -1264,6 +1978,97 @@ impl Channel { }) } + pub fn build_multihop_chan_open_confirm(&self) -> Result, ChannelError> { + // Source and destination channel IDs must be specified + let src_channel_id = self + .src_channel_id() + .ok_or_else(ChannelError::missing_local_channel_id)?; + let dst_channel_id = self + .dst_channel_id() + .ok_or_else(ChannelError::missing_counterparty_channel_id)?; + + // Channel must exist on source + let (_src_channel, _) = self + .src_chain() + .query_channel( + QueryChannelRequest { + port_id: self.src_port_id().clone(), + channel_id: src_channel_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map_err(|e| ChannelError::query(self.src_chain().id(), e))?; + + // Check that the destination chain will accept the MsgChannelOpenConfirm + self.validated_expected_channel(ChannelMsgType::OpenConfirm)?; + + self.dst_chain() + .query_connection( + QueryConnectionRequest { + connection_id: self.dst_connection_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map_err(|e| ChannelError::query(self.dst_chain().id(), e))?; + + // Update the clients along the channel path and store the heights necessary for querying + // multihop proofs. 'proof_heights' contains the height at which proofs should be queried, + // ordered from the sending chain to the penultimate chain in the channel path. In order to + // verify proofs queried at the height 'proof_query_height' stored in 'proof_heights', the + // client on the chain that receives the proof must possess the consensus state corresponding + // to height 'proof_query_height + 1' of the chain on which the proofs were queried. + let proof_heights = self.update_channel_path_clients()?; + + // Get the proof heights for the last hop in the channel path, which connects the penultimate + // chain to the destination. + let last_hop_heights = proof_heights.last().ok_or_else(|| { + ChannelError::missing_multihop_proof_heights( + src_channel_id.clone(), + self.src_chain().id(), + ) + })?; + + // Build the message to update the client on the channel path's destination chain. Because + // proofs are queried at height 'last_hop_heights.proof_query_height' in the penultimate chain, + // update the client that tracks the penultimate chain in the destination using height + // 'last_hop_heights.proof_query_height' + 1 to allow for proof verification. + let mut msgs = + self.build_update_client_on_last_hop(last_hop_heights.proof_query_height.increment())?; + + let multihop_proofs = self.build_multihop_channel_proofs(&proof_heights)?; + + let multihop_proof_bytes = prost::Message::encode_to_vec(&multihop_proofs); + + let proofs = ibc_relayer_types::proofs::Proofs::new( + CommitmentProofBytes::try_from(multihop_proof_bytes).unwrap(), + None, + None, + None, + None, + last_hop_heights.proof_query_height.increment(), + ) + .map_err(ChannelError::malformed_proof)?; + + // Get signer + let signer = self + .dst_chain() + .get_signer() + .map_err(|e| ChannelError::fetch_signer(self.dst_chain().id(), e))?; + + // Build the domain type message + let new_msg = MsgChannelOpenConfirm { + port_id: self.dst_port_id().clone(), + channel_id: dst_channel_id.clone(), + proofs, + signer, + }; + + msgs.push(new_msg.to_any()); + Ok(msgs) + } + pub fn build_chan_open_confirm(&self) -> Result, ChannelError> { // Source and destination channel IDs must be specified let src_channel_id = self @@ -1334,9 +2139,14 @@ impl Channel { fn do_build_chan_open_confirm_and_send( channel: &Channel, ) -> Result { - let dst_msgs = channel.build_chan_open_confirm()?; + let dst_msgs = if channel.a_side.connection_hops.is_some() { + channel.build_multihop_chan_open_confirm()? + } else { + channel.build_chan_open_confirm()? + }; let tm = TrackedMsgs::new_static(dst_msgs, "ChannelOpenConfirm"); + let events = channel .dst_chain() .send_messages_and_wait_commit(tm) diff --git a/crates/relayer/src/channel/error.rs b/crates/relayer/src/channel/error.rs index e25b1898bf..6b034bdf85 100644 --- a/crates/relayer/src/channel/error.rs +++ b/crates/relayer/src/channel/error.rs @@ -12,6 +12,7 @@ use ibc_relayer_types::proofs::ProofError; use crate::error::Error as RelayerError; use crate::foreign_client::{ForeignClientError, HasExpiredOrFrozenError}; +use crate::spawn::SpawnError; use crate::supervisor::Error as SupervisorError; define_error! { @@ -28,6 +29,10 @@ define_error! { [ ClientError ] |_| { "ICS02 client error" }, + Spawn + [ SpawnError ] + | _ | { "spawn error" }, + InvalidChannel { reason: String } | e | { @@ -36,7 +41,7 @@ define_error! { }, InvalidChannelUpgradeOrdering - |_| { "attempted to upgrade a channel to a more strict ordring, which is not allowed" }, + |_| { "attempted to upgrade a channel to a more strict ordering, which is not allowed" }, InvalidChannelUpgradeState { expected: String, actual: String } @@ -61,6 +66,9 @@ define_error! { MissingCounterpartyConnection |_| { "failed due to missing counterparty connection" }, + MissingCounterpartyConnectionId + |_| { "failed due to missing counterparty connection" }, + MissingChannelOnDestination |_| { "missing channel on destination chain" }, @@ -73,10 +81,6 @@ define_error! { MissingUpgradeErrorReceiptProof |_| { "missing upgrade error receipt proof" }, - MalformedProof - [ ProofError ] - |_| { "malformed proof" }, - ChannelProof [ RelayerError ] |_| { "failed to build channel proofs" }, @@ -112,6 +116,9 @@ define_error! { [ RelayerError ] |e| { format_args!("failed during a query to chain '{0}'", e.chain_id) }, + QueriedProofNotFound + |_| { "Requested proof with query but no proof was returned." }, + ChainQuery { chain_id: ChainId } [ RelayerError ] @@ -154,7 +161,19 @@ define_error! { ChannelAlreadyExist { channel_id: ChannelId } - |e| { format_args!("channel '{}' already exist in an incompatible state", e.channel_id) }, + |e| { format_args!("channel '{}' already exists in an incompatible state", e.channel_id) }, + + UnexpectedChannelState + { channel_id: ChannelId, + chain_id: ChainId, + expected_state: State, + channel_state: State, + } + | e | { format_args!("expected state of channel '{}' on chain '{}' to be '{}', but found '{}'", e.channel_id, e.chain_id, e.expected_state, e.channel_state) }, + + MalformedProof + [ ProofError ] + |_| { "malformed proof" }, MismatchChannelEnds { @@ -226,6 +245,58 @@ define_error! { format_args!("error after maximum retry of {} and total delay of {}s: {}", e.tries, e.total_delay.as_secs(), e.description) }, + + MissingConnectionHops + { + channel_id: ChannelId, + chain_id: ChainId, + } + |e| { + format_args!("channel {} on chain {} has no connection hops specified", + e.channel_id, e.chain_id) + }, + + MissingLocalConnectionHops + { + channel_id: ChannelId, + chain_id: ChainId, + } + | e | { + format_args!("failed due to missing local connection hops data for channel '{}' on \ + chain '{}'", e.channel_id, e.chain_id) + }, + + FailedChannelPathClientUpdate + { + client_id: ClientId, + client_host_chain_id: ChainId, + channel_id: ChannelId, + chain_id: ChainId, + } + | e | { + format_args!("failed to update client '{}' on chain '{}' while updating the channel path \ + for channel '{}' on chain '{}'", e.client_id, e.client_host_chain_id, e.channel_id, e.chain_id) + }, + + MissingMultihopProofHeights + { + channel_id: ChannelId, + chain_id: ChainId, + } + | e | { + format_args!("missing proof heights on the path for channel '{}' on chain '{}'", + e.channel_id, e.chain_id) + }, + + ChannelConnectionClientMultihopMissingConnections + { + channel_id: ChannelId, + } + |e| { + format_args!("failed due to missing connections on ChannelConnectionClientMultihop \ + for Channel '{}'", + e.channel_id) + }, } } diff --git a/crates/relayer/src/client_state.rs b/crates/relayer/src/client_state.rs index 85f5b83dc4..43002c0f56 100644 --- a/crates/relayer/src/client_state.rs +++ b/crates/relayer/src/client_state.rs @@ -173,3 +173,9 @@ impl From for IdentifiedClientState { } } } + +impl From for Vec { + fn from(value: IdentifiedAnyClientState) -> Self { + vec![value] + } +} diff --git a/crates/relayer/src/error.rs b/crates/relayer/src/error.rs index 537470552c..4d562dc022 100644 --- a/crates/relayer/src/error.rs +++ b/crates/relayer/src/error.rs @@ -630,6 +630,13 @@ define_error! { InvalidChannelString { channel: String } |e| { format!("invalid channel string {}", e.channel) }, + + SpawnError + { chain_id: ChainId } + |e| { format!("failed to spawn chain runtime for chain '{}'", e.chain_id)}, + + EmptyConnectionHopIds + |_| { "cannot build connection hops from an empty list of connection ids".to_string() }, } } diff --git a/crates/relayer/src/foreign_client.rs b/crates/relayer/src/foreign_client.rs index 8fded33524..5fb5592b6d 100644 --- a/crates/relayer/src/foreign_client.rs +++ b/crates/relayer/src/foreign_client.rs @@ -1397,7 +1397,7 @@ impl ForeignClient Result, ForeignClientError> { + ) -> Result, ForeignClientError> { crate::time!( "fetch_update_client_event", { @@ -1456,6 +1456,7 @@ impl ForeignClient IbcEvent::UpdateClient).ok_or_else(|| { ForeignClientError::unexpected_event( self.id().clone(), @@ -1464,7 +1465,7 @@ impl ForeignClient ForeignClient Link { let b_port_id = a_channel.counterparty().port_id.clone(); + let (b_channel, _) = b_chain + .query_channel( + QueryChannelRequest { + port_id: b_port_id.clone(), + channel_id: b_channel_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map_err(|e| { + LinkError::channel_not_found( + b_port_id.clone(), + b_channel_id.clone(), + b_chain.id(), + e, + ) + })?; + if a_channel.connection_hops().is_empty() { - return Err(LinkError::no_connection_hop( + return Err(LinkError::no_connection_hops( a_channel_id.clone(), a_chain.id(), )); } + if b_channel.connection_hops().is_empty() { + return Err(LinkError::no_connection_hops( + b_channel_id.clone(), + b_chain.id(), + )); + } + // Check that the counterparty details on the destination chain matches the source chain check_channel_counterparty( b_chain.clone(), @@ -126,44 +152,85 @@ impl Link { ) .map_err(LinkError::initialization)?; - // Check the underlying connection - let a_connection_id = a_channel.connection_hops()[0].clone(); - let (a_connection, _) = a_chain - .query_connection( - QueryConnectionRequest { - connection_id: a_connection_id.clone(), - height: QueryHeight::Latest, - }, - IncludeProof::No, - ) + // Build connection hops and query all the connections along the channel path + let a_side_hops = build_hops_from_connection_ids(&a_chain, a_channel.connection_hops()) .map_err(LinkError::relayer)?; - if !a_connection.state_matches(&ConnectionState::Open) { - return Err(LinkError::channel_not_opened( - a_channel_id.clone(), - a_chain.id(), - )); + let b_side_hops = build_hops_from_connection_ids(&b_chain, b_channel.connection_hops()) + .map_err(LinkError::relayer)?; + + // Ensure all connections along the channel path are in the Open state + for connection_hop in a_side_hops.hops_as_slice() { + if !connection_hop + .connection() + .state_matches(&ConnectionState::Open) + { + return Err(LinkError::channel_not_opened( + a_channel_id.clone(), + a_chain.id(), + )); + } } + let a_connection = a_side_hops + .hops_as_slice() + .first() + .ok_or_else(|| LinkError::no_connection_hops(a_channel_id.clone(), a_chain.id()))? + .clone(); + + let b_connection = b_side_hops + .hops_as_slice() + .first() + .ok_or_else(|| LinkError::no_connection_hops(b_channel_id.clone(), b_chain.id()))? + .clone(); + + // FIXME(MULTIHOP): For now, pass Some(_) to connection_hops if there are multiple hops and None if there is a single one. + // This allows us to keep using existing structs as they are defined (with the single `connection_id` field) while also including + // the new `connection_hops` field. When multiple hops are present, pass Some(_) to a_side_hops and b_side_hops and use that. + // When a single hop is present, pass None to a_side_hops and b_side_hops and use the connection_id stored in `ChannelSide`. + let a_side_hops = match a_side_hops.hops_as_slice().len() { + 0 => { + return Err(LinkError::no_connection_hops( + a_channel_id.clone(), + a_chain.id(), + )) + } + 1 => None, + _ => Some(a_side_hops), + }; + + let b_side_hops = match b_side_hops.hops_as_slice().len() { + 0 => { + return Err(LinkError::no_connection_hops( + b_channel_id.clone(), + b_chain.id(), + )) + } + 1 => None, + _ => Some(b_side_hops), + }; + let channel = Channel { ordering: a_channel.ordering, a_side: ChannelSide::new( a_chain.clone(), - a_connection.client_id().clone(), - a_connection_id, + a_connection.connection().client_id().clone(), + a_connection.connection_id().clone(), + a_side_hops, opts.src_port_id.clone(), Some(opts.src_channel_id.clone()), None, ), b_side: ChannelSide::new( b_chain.clone(), - a_connection.counterparty().client_id().clone(), - a_connection.counterparty().connection_id().unwrap().clone(), + b_connection.connection().client_id().clone(), + b_connection.connection_id().clone(), + b_side_hops, a_channel.counterparty().port_id.clone(), Some(b_channel_id.clone()), None, ), - connection_delay: a_connection.delay_period(), + connection_delay: a_connection.connection().delay_period(), }; if auto_register_counterparty_payee && a_channel.version.supports_fee() { diff --git a/crates/relayer/src/link/error.rs b/crates/relayer/src/link/error.rs index 694bc22f70..665fbca4d8 100644 --- a/crates/relayer/src/link/error.rs +++ b/crates/relayer/src/link/error.rs @@ -136,7 +136,7 @@ define_error! { e.channel_id) }, - NoConnectionHop + NoConnectionHops { channel_id: ChannelId, chain_id: ChainId, diff --git a/crates/relayer/src/link/relay_path.rs b/crates/relayer/src/link/relay_path.rs index 790571f482..40a891571a 100644 --- a/crates/relayer/src/link/relay_path.rs +++ b/crates/relayer/src/link/relay_path.rs @@ -214,6 +214,22 @@ impl RelayPath { &self.channel } + pub fn is_multihop(&self) -> bool { + if let Some(connection_hops) = self.channel.a_side.connection_hops() { + if connection_hops.hops.len() > 1 { + return true; + } + } + + if let Some(connection_hops) = self.channel.b_side.connection_hops() { + if connection_hops.hops.len() > 1 { + return true; + } + } + + false + } + fn src_channel(&self, height_query: QueryHeight) -> Result { self.src_chain() .query_channel( diff --git a/crates/relayer/src/object.rs b/crates/relayer/src/object.rs index aac3efb257..c5ff8d3d9a 100644 --- a/crates/relayer/src/object.rs +++ b/crates/relayer/src/object.rs @@ -13,6 +13,7 @@ use ibc_relayer_types::core::{ ics24_host::identifier::{ChainId, ChannelId, ClientId, ConnectionId, PortId}, }; +use crate::chain::counterparty::ChannelConnectionClient; use crate::chain::{ counterparty::{ channel_connection_client, channel_connection_client_no_checks, @@ -376,9 +377,19 @@ impl Object { .channel_id() .ok_or_else(|| ObjectError::missing_channel_id(e.clone()))?; - let client = channel_connection_client(chain, e.port_id(), channel_id) - .map_err(ObjectError::supervisor)? - .client; + let chan_conn_client = channel_connection_client(chain, e.port_id(), channel_id) + .map_err(ObjectError::supervisor)?; + + let client = match chan_conn_client { + ChannelConnectionClient::SingleHop(chan_conn_client) => chan_conn_client.client, + + // FIXME(MULTIHOP): Figure out how the multihop case should be handled. In the single hop case + // the only client from ChannelConnectionClient is returned. For multihop, there are + // multiple clients along the channel path. + ChannelConnectionClient::Multihop(_chan_conn_client) => { + panic!("Not yet implemented for multihop!") + } + }; Ok(Client { dst_client_id: client.client_id.clone(), @@ -429,13 +440,27 @@ impl Object { // This is to support the optimistic channel handshake by allowing the channel worker to get // the channel events while the connection is being established. // The channel worker will eventually finish the channel handshake via the retry mechanism. - channel_connection_client_no_checks(src_chain, port_id, channel_id) - .map(|c| c.client.client_state.chain_id()) - .map_err(ObjectError::supervisor)? + match channel_connection_client_no_checks(src_chain, port_id, channel_id) { + Ok(ChannelConnectionClient::SingleHop(chan_conn_client)) => { + Ok(chan_conn_client.dst_chain_id()) + } + Ok(ChannelConnectionClient::Multihop(_chan_conn_client)) => { + // FIXME(MULTIHOP): Handle the multihop case + panic!("Not yet implemented for multihop!"); + } + Err(e) => Err(ObjectError::supervisor(e)), + }? } else { - channel_connection_client(src_chain, port_id, channel_id) - .map(|c| c.client.client_state.chain_id()) - .map_err(ObjectError::supervisor)? + match channel_connection_client(src_chain, port_id, channel_id) { + Ok(ChannelConnectionClient::SingleHop(chan_conn_client)) => { + Ok(chan_conn_client.dst_chain_id()) + } + Ok(ChannelConnectionClient::Multihop(_chan_conn_client)) => { + // FIXME(MULTIHOP): Handle the multihop case + panic!("Not yet implemented for multihop!"); + } + Err(e) => Err(ObjectError::supervisor(e)), + }? }; Ok(Channel { @@ -461,13 +486,27 @@ impl Object { // This is to support the optimistic channel handshake by allowing the channel worker to get // the channel events while the connection is being established. // The channel worker will eventually finish the channel handshake via the retry mechanism. - channel_connection_client_no_checks(src_chain, port_id, channel_id) - .map(|c| c.client.client_state.chain_id()) - .map_err(ObjectError::supervisor)? + match channel_connection_client_no_checks(src_chain, port_id, channel_id) { + Ok(ChannelConnectionClient::SingleHop(chan_conn_client)) => { + Ok(chan_conn_client.dst_chain_id()) + } + Ok(ChannelConnectionClient::Multihop(_chan_conn_client)) => { + // FIXME(MULTIHOP): Handle the multihop case + panic!("Not yet implemented for multihop!"); + } + Err(e) => Err(ObjectError::supervisor(e)), + }? } else { - channel_connection_client(src_chain, port_id, channel_id) - .map(|c| c.client.client_state.chain_id()) - .map_err(ObjectError::supervisor)? + match channel_connection_client(src_chain, port_id, channel_id) { + Ok(ChannelConnectionClient::SingleHop(chan_conn_client)) => { + Ok(chan_conn_client.dst_chain_id()) + } + Ok(ChannelConnectionClient::Multihop(_chan_conn_client)) => { + // FIXME(MULTIHOP): Handle the multihop case + panic!("Not yet implemented for multihop!"); + } + Err(e) => Err(ObjectError::supervisor(e)), + }? }; Ok(Channel { diff --git a/crates/relayer/src/registry.rs b/crates/relayer/src/registry.rs index a9b2be27e3..8e6f9081e8 100644 --- a/crates/relayer/src/registry.rs +++ b/crates/relayer/src/registry.rs @@ -2,6 +2,7 @@ use alloc::collections::btree_map::BTreeMap as HashMap; use alloc::sync::Arc; +use once_cell::sync::OnceCell; use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::runtime::Runtime as TokioRuntime; @@ -10,10 +11,10 @@ use tracing::{trace, warn}; use ibc_relayer_types::core::ics24_host::identifier::ChainId; use crate::{ - chain::handle::ChainHandle, + chain::handle::{ChainHandle, DefaultChainHandle}, config::Config, spawn::{spawn_chain_runtime, SpawnError}, - util::lock::RwArc, + util::lock::{LockExt, RwArc}, }; /// Registry for keeping track of [`ChainHandle`]s indexed by a `ChainId`. @@ -26,11 +27,6 @@ pub struct Registry { rt: Arc, } -#[derive(Clone)] -pub struct SharedRegistry { - pub registry: RwArc>, -} - impl Registry { /// Construct a new [`Registry`] using the provided [`Config`] pub fn new(config: Config) -> Self { @@ -91,7 +87,27 @@ impl Registry { } } -impl SharedRegistry { +static GLOBAL_REGISTRY: OnceCell = OnceCell::new(); + +pub fn set_global_registry(registry: SharedRegistry) { + if GLOBAL_REGISTRY.set(registry).is_err() { + panic!("global registry already set"); + } +} + +pub fn get_global_registry() -> SharedRegistry { + GLOBAL_REGISTRY + .get() + .expect("global registry not set") + .clone() +} + +#[derive(Clone)] +pub struct SharedRegistry { + pub registry: RwArc>, +} + +impl SharedRegistry { pub fn new(config: Config) -> Self { let registry = Registry::new(config); @@ -100,23 +116,52 @@ impl SharedRegistry { } } - pub fn get_or_spawn(&self, chain_id: &ChainId) -> Result { - self.registry.write().unwrap().get_or_spawn(chain_id) + pub fn is_empty(&self) -> bool { + self.read().size() == 0 } - pub fn spawn(&self, chain_id: &ChainId) -> Result { - self.write().spawn(chain_id) + pub fn get_or_spawn(&self, chain_id: &ChainId) -> Result { + let read_reg = self.read(); + + if let Some(handle) = read_reg.handles.get(chain_id) { + Ok(handle.clone()) + } else { + drop(read_reg); + self.spawn(chain_id) + } + } + + pub fn spawn(&self, chain_id: &ChainId) -> Result { + let mut write_reg = self.write(); + + if let Some(handle) = write_reg.handles.get(chain_id) { + return Ok(handle.clone()); + } + + let rt = Arc::clone(&write_reg.rt); + let handle: DefaultChainHandle = spawn_chain_runtime(&write_reg.config, chain_id, rt)?; + + write_reg.handles.insert(chain_id.clone(), handle.clone()); + drop(write_reg); + + trace!(chain = %chain_id, "spawned chain runtime"); + + Ok(handle) } pub fn shutdown(&self, chain_id: &ChainId) { - self.write().shutdown(chain_id) + if let Some(handle) = self.write().handles.remove(chain_id) { + if let Err(e) = handle.shutdown() { + warn!(chain = %chain_id, "chain runtime might have failed to shutdown properly: {}", e); + } + } } - pub fn write(&self) -> RwLockWriteGuard<'_, Registry> { - self.registry.write().unwrap() + pub fn write(&self) -> RwLockWriteGuard<'_, Registry> { + self.registry.acquire_write() } - pub fn read(&self) -> RwLockReadGuard<'_, Registry> { - self.registry.read().unwrap() + pub fn read(&self) -> RwLockReadGuard<'_, Registry> { + self.registry.acquire_read() } } diff --git a/crates/relayer/src/supervisor.rs b/crates/relayer/src/supervisor.rs index c994ac6585..a994d73fa7 100644 --- a/crates/relayer/src/supervisor.rs +++ b/crates/relayer/src/supervisor.rs @@ -15,6 +15,7 @@ use ibc_relayer_types::{ Height, }; +use crate::chain::handle::DefaultChainHandle; use crate::{ chain::{endpoint::HealthCheck, handle::ChainHandle, tracking::TrackingId}, config::Config, @@ -85,7 +86,7 @@ pub struct SupervisorOptions { */ pub fn spawn_supervisor( config: Config, - registry: SharedRegistry, + registry: SharedRegistry, rest_rx: Option, options: SupervisorOptions, ) -> Result { @@ -149,15 +150,15 @@ fn should_scan(config: &Config, options: &SupervisorOptions) -> bool { && (config.mode.clients.misbehaviour || config.mode.clients.refresh)) } -pub fn spawn_supervisor_tasks( +pub fn spawn_supervisor_tasks( config: Config, - registry: SharedRegistry, + registry: SharedRegistry, rest_rx: Option, cmd_rx: Receiver, options: SupervisorOptions, ) -> Result, Error> { if options.health_check { - health_check(&config, &mut registry.write()); + health_check(&config, ®istry); } // If telemetry is enabled, for each chain register the relayer's address @@ -177,7 +178,7 @@ pub fn spawn_supervisor_tasks( if should_scan(&config, &options) { let scan = chain_scanner( &config, - &mut registry.write(), + ®istry, &mut client_state_filter.acquire_write(), if options.force_full_scan { ScanMode::Full @@ -190,11 +191,10 @@ pub fn spawn_supervisor_tasks( info!("scanned chains:"); info!("{}", scan); - spawn_context(&config, &mut registry.write(), &mut workers.acquire_write()) - .spawn_workers(scan); + spawn_context(&config, ®istry, &mut workers.acquire_write()).spawn_workers(scan); } - let subscriptions = init_subscriptions(&config, &mut registry.write())?; + let subscriptions = init_subscriptions(&config, ®istry)?; let batch_tasks = spawn_batch_workers( &config, @@ -220,12 +220,12 @@ pub fn spawn_supervisor_tasks( Ok(tasks) } -fn spawn_batch_workers( +fn spawn_batch_workers( config: &Config, - registry: SharedRegistry, + registry: SharedRegistry, client_state_filter: Arc>, workers: Arc>, - subscriptions: Vec<(Chain, Subscription)>, + subscriptions: Vec<(DefaultChainHandle, Subscription)>, ) -> Vec { let mut handles = Vec::with_capacity(subscriptions.len()); @@ -242,7 +242,7 @@ fn spawn_batch_workers( if let Ok(batch) = subscription.try_recv() { handle_batch( &config, - &mut registry.write(), + ®istry, &mut client_state_filter.acquire_write(), &mut workers.acquire_write(), chain.clone(), @@ -260,8 +260,8 @@ fn spawn_batch_workers( handles } -pub fn spawn_cmd_worker( - registry: SharedRegistry, +pub fn spawn_cmd_worker( + registry: SharedRegistry, workers: Arc>, cmd_rx: Receiver, ) -> TaskHandle { @@ -282,9 +282,9 @@ pub fn spawn_cmd_worker( ) } -pub fn spawn_rest_worker( +pub fn spawn_rest_worker( config: Config, - registry: SharedRegistry, + registry: SharedRegistry, workers: Arc>, rest_rx: rest::Receiver, ) -> TaskHandle { @@ -344,9 +344,9 @@ fn is_channel_allowed( /// Whether or not the relayer should relay packets /// or complete handshakes for the given [`Object`]. -fn relay_on_object( +fn relay_on_object( config: &Config, - registry: &mut Registry, + registry: &SharedRegistry, client_state_filter: &mut FilterPolicy, chain_id: &ChainId, object: &Object, @@ -622,25 +622,25 @@ pub fn collect_events( } /// Create a new `SpawnContext` for spawning workers. -fn spawn_context<'a, Chain: ChainHandle>( +fn spawn_context<'a>( config: &'a Config, - registry: &'a mut Registry, + registry: &'a SharedRegistry, workers: &'a mut WorkerMap, -) -> SpawnContext<'a, Chain> { +) -> SpawnContext<'a> { SpawnContext::new(config, registry, workers) } -fn chain_scanner<'a, Chain: ChainHandle>( +fn chain_scanner<'a>( config: &'a Config, - registry: &'a mut Registry, + registry: &'a SharedRegistry, client_state_filter: &'a mut FilterPolicy, full_scan: ScanMode, -) -> ChainScanner<'a, Chain> { +) -> ChainScanner<'a> { ChainScanner::new(config, registry, client_state_filter, full_scan) } /// Perform a health check on all connected chains -fn health_check(config: &Config, registry: &mut Registry) { +fn health_check(config: &Config, registry: &SharedRegistry) { use HealthCheck::*; let chains = &config.chains; @@ -669,10 +669,10 @@ fn health_check(config: &Config, registry: &mut Registry( +fn init_subscriptions( config: &Config, - registry: &mut Registry, -) -> Result, Error> { + registry: &SharedRegistry, +) -> Result, Error> { let chains = &config.chains; let mut subscriptions = Vec::with_capacity(chains.len()); @@ -703,7 +703,7 @@ fn init_subscriptions( // At least one chain runtime should be available, otherwise the supervisor // cannot do anything and will hang indefinitely. - if registry.size() == 0 { + if registry.is_empty() { return Err(Error::no_chains_available()); } @@ -799,7 +799,7 @@ fn clear_pending_packets(workers: &WorkerMap, chain_id: &ChainId) -> Result<(), ] fn process_batch( config: &Config, - registry: &mut Registry, + registry: &SharedRegistry, client_state_filter: &mut FilterPolicy, workers: &mut WorkerMap, src_chain: Chain, @@ -889,7 +889,7 @@ fn process_batch( /// The labels `chain_id` represents the chain sending the event, and `counterparty_chain_id` represents /// the chain receiving the event. /// So successfully sending a packet from chain A to chain B will result in first a SendPacket -/// event with `chain_id = A` and `counterparty_chain_id = B` and then a WriteAcknowlegment +/// event with `chain_id = A` and `counterparty_chain_id = B` and then a WriteAcknowledgment /// event with `chain_id = B` and `counterparty_chain_id = A`. fn send_telemetry( src: &Src, @@ -947,7 +947,7 @@ fn send_telemetry( )] fn handle_batch( config: &Config, - registry: &mut Registry, + registry: &SharedRegistry, client_state_filter: &mut FilterPolicy, workers: &mut WorkerMap, chain: Chain, diff --git a/crates/relayer/src/supervisor/client_state_filter.rs b/crates/relayer/src/supervisor/client_state_filter.rs index 646d628710..f1bbab5d6a 100644 --- a/crates/relayer/src/supervisor/client_state_filter.rs +++ b/crates/relayer/src/supervisor/client_state_filter.rs @@ -17,7 +17,7 @@ use crate::chain::requests::{ use crate::client_state::AnyClientState; use crate::error::Error as RelayerError; use crate::object; -use crate::registry::Registry; +use crate::registry::SharedRegistry; use crate::spawn::SpawnError; /// The lower bound trust threshold value. Clients with a trust threshold less @@ -93,9 +93,9 @@ impl FilterPolicy { /// /// May encounter errors caused by failed queries. Any such error /// is propagated and nothing is cached. - pub fn control_connection_end_and_client( + pub fn control_connection_end_and_client( &mut self, - registry: &mut Registry, + registry: &SharedRegistry, chain_id: &ChainId, // Chain hosting the client & connection client_state: &AnyClientState, connection: &ConnectionEnd, @@ -222,9 +222,9 @@ impl FilterPolicy { permission } - pub fn control_client_object( + pub fn control_client_object( &mut self, - registry: &mut Registry, + registry: &SharedRegistry, obj: &object::Client, ) -> Result { let identifier = CacheKey::Client(obj.dst_chain_id.clone(), obj.dst_client_id.clone()); @@ -259,9 +259,9 @@ impl FilterPolicy { Ok(self.control_client(&obj.dst_chain_id, &obj.dst_client_id, &client_state)) } - pub fn control_conn_object( + pub fn control_conn_object( &mut self, - registry: &mut Registry, + registry: &SharedRegistry, obj: &object::Connection, ) -> Result { let identifier = @@ -313,9 +313,9 @@ impl FilterPolicy { ) } - fn control_channel( + fn control_channel( &mut self, - registry: &mut Registry, + registry: &SharedRegistry, chain_id: &ChainId, port_id: &PortId, channel_id: &ChannelId, @@ -391,9 +391,9 @@ impl FilterPolicy { Ok(permission) } - pub fn control_chan_object( + pub fn control_chan_object( &mut self, - registry: &mut Registry, + registry: &SharedRegistry, obj: &object::Channel, ) -> Result { self.control_channel( @@ -404,9 +404,9 @@ impl FilterPolicy { ) } - pub fn control_packet_object( + pub fn control_packet_object( &mut self, - registry: &mut Registry, + registry: &SharedRegistry, obj: &object::Packet, ) -> Result { self.control_channel( diff --git a/crates/relayer/src/supervisor/error.rs b/crates/relayer/src/supervisor/error.rs index a21aa3bcef..9fddef76aa 100644 --- a/crates/relayer/src/supervisor/error.rs +++ b/crates/relayer/src/supervisor/error.rs @@ -1,6 +1,7 @@ use flex_error::define_error; use ibc_relayer_types::core::ics03_connection::connection::Counterparty; +use ibc_relayer_types::core::ics24_host::identifier::ClientId; use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ChannelId, ConnectionId, PortId}; use crate::error::Error as RelayerError; @@ -42,6 +43,16 @@ define_error! { e.connection_id, e.channel_id, e.chain_id) }, + ClientIsFrozen + { + client_id: ClientId, + channel_id: ChannelId, + chain_id: ChainId, + } + |e| { + format_args!("client '{}' on the path for channel '{}' on chain '{}' is frozen", e.client_id, e.channel_id, e.chain_id) + }, + MissingConnectionHops { channel_id: ChannelId, @@ -75,6 +86,72 @@ define_error! { HandleRecv |_| { "failed to receive the result of a command from the supervisor through a channel" }, + + ChannelConnectionClientMissingConnection + { + channel_id: ChannelId, + } + |e| { + format_args!("ChannelConnectionClient constructor failed due to a missing \ + value for the connection field of channel '{}'", + e.channel_id) + }, + + ChannelConnectionClientMissingClient + { + channel_id: ChannelId, + } + |e| { + format_args!("ChannelConnectionClient constructor failed due to a missing \ + value for the client field of channel '{}'", + e.channel_id) + }, + + ChannelConnectionClientMultihopMissingClient + { + channel_id: ChannelId, + } + |e| { + format_args!("failed due to missing clients for channel '{}'", e.channel_id) + }, + + ChannelConnectionClientMultihopMissingConnection + { + channel_id: ChannelId, + } + |e| { + format_args!("failed due to missing connections for channel '{}'", e.channel_id) + }, + + ChannelConnectionClientMultihopConstructorMissingClients + { + channel_id: ChannelId, + } + |e| { + format_args!("ChannelConnectionClientMultihop constructor failed due to missing + values for the client field of channel '{}'", + e.channel_id) + }, + + ChannelConnectionClientMultihopConstructorMissingConnections + { + channel_id: ChannelId, + } + |e| { + format_args!("ChannelConnectionClientMultihop constructor failed due to a missing \ + value for the client field of channel '{}'", + e.channel_id) + }, + + ChannelConnectionClientMultihopConstructorLengthMismatch + { + channel_id: ChannelId, + } + |e| { + format_args!("ChannelConnectionClientMultihop constructor failed due to a mismatch \ + in the number of connections and clients in the channel path for channel '{}'", + e.channel_id) + }, } } diff --git a/crates/relayer/src/supervisor/scan.rs b/crates/relayer/src/supervisor/scan.rs index d5dd77d364..c1e4136144 100644 --- a/crates/relayer/src/supervisor/scan.rs +++ b/crates/relayer/src/supervisor/scan.rs @@ -16,7 +16,7 @@ use ibc_relayer_types::core::{ use crate::{ chain::{ counterparty::{channel_on_destination, connection_state_on_destination}, - handle::ChainHandle, + handle::{ChainHandle, DefaultChainHandle}, requests::{ IncludeProof, PageRequest, QueryChannelRequest, QueryClientConnectionsRequest, QueryClientStateRequest, QueryClientStatesRequest, QueryConnectionChannelsRequest, @@ -29,7 +29,7 @@ use crate::{ ChainConfig, Config, }, path::PathIdentifiers, - registry::Registry, + registry::SharedRegistry, supervisor::client_state_filter::{FilterPolicy, Permission}, }; @@ -266,17 +266,17 @@ pub enum ScanMode { Full, } -pub struct ChainScanner<'a, Chain: ChainHandle> { +pub struct ChainScanner<'a> { config: &'a Config, - registry: &'a mut Registry, + registry: &'a SharedRegistry, client_state_filter: &'a mut FilterPolicy, scan_mode: ScanMode, } -impl<'a, Chain: ChainHandle> ChainScanner<'a, Chain> { +impl<'a> ChainScanner<'a> { pub fn new( config: &'a Config, - registry: &'a mut Registry, + registry: &'a SharedRegistry, client_state_filter: &'a mut FilterPolicy, scan_mode: ScanMode, ) -> Self { @@ -342,7 +342,7 @@ impl<'a, Chain: ChainHandle> ChainScanner<'a, Chain> { pub fn query_allowed_channels( &mut self, - chain: &Chain, + chain: &DefaultChainHandle, filters: &ChannelFilters, scan: &mut ChainScan, ) -> Result<(), Error> { @@ -397,7 +397,11 @@ impl<'a, Chain: ChainHandle> ChainScanner<'a, Chain> { Ok(()) } - pub fn scan_all_clients(&mut self, chain: &Chain, scan: &mut ChainScan) -> Result<(), Error> { + pub fn scan_all_clients( + &mut self, + chain: &DefaultChainHandle, + scan: &mut ChainScan, + ) -> Result<(), Error> { info!("scanning all clients..."); let clients = query_all_clients(chain)?; @@ -435,7 +439,7 @@ impl<'a, Chain: ChainHandle> ChainScanner<'a, Chain> { fn scan_client( &mut self, - chain: &Chain, + chain: &DefaultChainHandle, client: IdentifiedAnyClientState, ) -> Result, Error> { let span = error_span!("scan.client", client = %client.client_id); @@ -483,7 +487,7 @@ impl<'a, Chain: ChainHandle> ChainScanner<'a, Chain> { fn scan_connection( &mut self, - chain: &Chain, + chain: &DefaultChainHandle, client: &IdentifiedAnyClientState, connection: IdentifiedConnectionEnd, ) -> Result, Error> { @@ -594,7 +598,11 @@ impl<'a, Chain: ChainHandle> ChainScanner<'a, Chain> { } } - fn client_allowed(&mut self, chain: &Chain, client: &IdentifiedAnyClientState) -> bool { + fn client_allowed( + &mut self, + chain: &DefaultChainHandle, + client: &IdentifiedAnyClientState, + ) -> bool { if !self.filtering_enabled() { return true; }; @@ -610,7 +618,7 @@ impl<'a, Chain: ChainHandle> ChainScanner<'a, Chain> { fn connection_allowed( &mut self, - chain: &Chain, + chain: &DefaultChainHandle, client: &IdentifiedAnyClientState, connection: &IdentifiedConnectionEnd, ) -> bool { @@ -653,7 +661,11 @@ impl<'a, Chain: ChainHandle> ChainScanner<'a, Chain> { } } - fn channel_allowed(&mut self, chain: &Chain, channel: &IdentifiedChannelEnd) -> bool { + fn channel_allowed( + &mut self, + chain: &DefaultChainHandle, + channel: &IdentifiedChannelEnd, + ) -> bool { self.config .packets_on_channel_allowed(&chain.id(), &channel.port_id, &channel.channel_id) } @@ -667,9 +679,9 @@ struct ScannedChannel { client: IdentifiedAnyClientState, } -fn scan_allowed_channel( - registry: &'_ mut Registry, - chain: &Chain, +fn scan_allowed_channel( + registry: &'_ SharedRegistry, + chain: &DefaultChainHandle, port_id: &PortId, channel_id: &ChannelId, ) -> Result { diff --git a/crates/relayer/src/supervisor/spawn.rs b/crates/relayer/src/supervisor/spawn.rs index 769ac2b9d3..400c7199e4 100644 --- a/crates/relayer/src/supervisor/spawn.rs +++ b/crates/relayer/src/supervisor/spawn.rs @@ -6,11 +6,14 @@ use ibc_relayer_types::core::{ }; use crate::{ - chain::{counterparty::connection_state_on_destination, handle::ChainHandle}, + chain::{ + counterparty::connection_state_on_destination, + handle::{ChainHandle, DefaultChainHandle}, + }, client_state::IdentifiedAnyClientState, config::Config, object::{Channel, Client, Connection, Object, Packet, Wallet}, - registry::Registry, + registry::SharedRegistry, supervisor::error::Error as SupervisorError, telemetry, worker::WorkerMap, @@ -22,16 +25,16 @@ use super::{ }; /// A context for spawning workers within the supervisor. -pub struct SpawnContext<'a, Chain: ChainHandle> { +pub struct SpawnContext<'a> { config: &'a Config, - registry: &'a mut Registry, + registry: &'a SharedRegistry, workers: &'a mut WorkerMap, } -impl<'a, Chain: ChainHandle> SpawnContext<'a, Chain> { +impl<'a> SpawnContext<'a> { pub fn new( config: &'a Config, - registry: &'a mut Registry, + registry: &'a SharedRegistry, workers: &'a mut WorkerMap, ) -> Self { Self { @@ -77,7 +80,7 @@ impl<'a, Chain: ChainHandle> SpawnContext<'a, Chain> { telemetry!(self.spawn_wallet_worker(chain)); } - pub fn spawn_wallet_worker(&mut self, chain: Chain) { + pub fn spawn_wallet_worker(&mut self, chain: DefaultChainHandle) { let wallet_object = Object::Wallet(Wallet { chain_id: chain.id(), }); @@ -89,7 +92,7 @@ impl<'a, Chain: ChainHandle> SpawnContext<'a, Chain> { }); } - pub fn spawn_workers_for_client(&mut self, chain: Chain, client_scan: ClientScan) { + pub fn spawn_workers_for_client(&mut self, chain: DefaultChainHandle, client_scan: ClientScan) { let _span = tracing::error_span!("client", client = %client_scan.id()).entered(); for (_, connection_scan) in client_scan.connections { @@ -99,7 +102,7 @@ impl<'a, Chain: ChainHandle> SpawnContext<'a, Chain> { pub fn spawn_workers_for_connection( &mut self, - chain: Chain, + chain: DefaultChainHandle, client: &IdentifiedAnyClientState, connection_scan: ConnectionScan, ) { @@ -155,7 +158,7 @@ impl<'a, Chain: ChainHandle> SpawnContext<'a, Chain> { fn spawn_connection_workers( &mut self, - chain: Chain, + chain: DefaultChainHandle, client: IdentifiedAnyClientState, connection: IdentifiedConnectionEnd, ) -> Result { @@ -216,7 +219,7 @@ impl<'a, Chain: ChainHandle> SpawnContext<'a, Chain> { /// handle a given channel for a given source chain. pub fn spawn_workers_for_channel( &mut self, - chain: Chain, + chain: DefaultChainHandle, client: &IdentifiedAnyClientState, channel_scan: ChannelScan, ) -> Result { diff --git a/crates/relayer/src/util.rs b/crates/relayer/src/util.rs index 852764ca50..92d0a1c6ce 100644 --- a/crates/relayer/src/util.rs +++ b/crates/relayer/src/util.rs @@ -8,6 +8,7 @@ pub mod diff; pub mod excluded_sequences; pub mod iter; pub mod lock; +pub mod multihop; pub mod pretty; pub mod profiling; pub mod queue; diff --git a/crates/relayer/src/util/multihop.rs b/crates/relayer/src/util/multihop.rs new file mode 100644 index 0000000000..4bdde47d1b --- /dev/null +++ b/crates/relayer/src/util/multihop.rs @@ -0,0 +1,85 @@ +use ibc_relayer_types::core::ics03_connection::connection::IdentifiedConnectionEnd; +use ibc_relayer_types::core::ics24_host::identifier::ConnectionId; +pub use ibc_relayer_types::core::ics33_multihop::channel_path::{ConnectionHop, ConnectionHops}; + +use crate::chain::handle::ChainHandle; +use crate::chain::requests::{ + IncludeProof, QueryClientStateRequest, QueryConnectionRequest, QueryHeight, +}; +use crate::error::Error; +use crate::registry::get_global_registry; + +pub fn build_hops_from_connection_ids( + src_chain: &impl ChainHandle, + connection_ids: &[ConnectionId], +) -> Result { + let registry = get_global_registry(); + + let mut connection_hops = Vec::new(); + + let src_connection_id = connection_ids + .first() + .ok_or(Error::empty_connection_hop_ids())? + .clone(); + + let (src_hop_connection, _) = src_chain.query_connection( + QueryConnectionRequest { + connection_id: src_connection_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + )?; + + let (src_hop_conn_client_state, _) = src_chain.query_client_state( + QueryClientStateRequest { + client_id: src_hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + )?; + + connection_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new(src_connection_id.clone(), src_hop_connection), + src_chain_id: src_chain.id(), + dst_chain_id: src_hop_conn_client_state.chain_id().clone(), + }); + + for hop_connection_id in connection_ids.iter().skip(1) { + let hop_chain_id = connection_hops + .last() + .expect("connection hops is never empty") + .dst_chain_id + .clone(); + + let hop_chain = match registry.get_or_spawn(&hop_chain_id) { + Ok(chain) => chain, + Err(_) => return Err(Error::spawn_error(hop_chain_id.clone())), + }; + + let (hop_connection, _) = hop_chain.query_connection( + QueryConnectionRequest { + connection_id: hop_connection_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + )?; + + let (hop_conn_client_state, _) = hop_chain.query_client_state( + QueryClientStateRequest { + client_id: hop_connection.client_id().clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + )?; + + connection_hops.push(ConnectionHop { + connection: IdentifiedConnectionEnd::new(hop_connection_id.clone(), hop_connection), + src_chain_id: hop_chain.id().clone(), + dst_chain_id: hop_conn_client_state.chain_id().clone(), + }); + } + + let connection_hops = ConnectionHops::new(connection_hops); + + Ok(connection_hops) +} diff --git a/e2e/e2e/packet.py b/e2e/e2e/packet.py index 5914c7922d..8b20cf4d0a 100644 --- a/e2e/e2e/packet.py +++ b/e2e/e2e/packet.py @@ -137,7 +137,7 @@ class QueryUnreceivedPackets(Cmd[List[int]]): channel: ChannelId def args(self) -> List[str]: - return ["--chain", self.chain, "--port", self.port, "--chan", self.channel] + return ["--chain", self.chain, "--port", self.port, "--channel", self.channel] def process(self, result: Any) -> List[int]: return from_dict(List[int], result) diff --git a/flake.lock b/flake.lock index 024b9948d6..1ab5df9a29 100644 --- a/flake.lock +++ b/flake.lock @@ -172,6 +172,7 @@ "ibc-go-v5-src": "ibc-go-v5-src", "ibc-go-v6-src": "ibc-go-v6-src", "ibc-go-v7-src": "ibc-go-v7-src", + "ibc-go-v8-polymer-multihop-src": "ibc-go-v8-polymer-multihop-src", "ibc-go-v8-src": "ibc-go-v8-src", "ibc-rs-src": "ibc-rs-src", "ica-src": "ica-src", @@ -216,15 +217,16 @@ "wasmvm_2_0_0-src": "wasmvm_2_0_0-src" }, "locked": { - "lastModified": 1718033821, - "narHash": "sha256-Y7zF1PWl6W6pSj2Hh6dl+n+udu/6jJV+ODinxk44YqU=", + "lastModified": 1720087574, + "narHash": "sha256-UOm9jm99qiiddjliICybVWGuLUAMwf8MFXLiKKYQWU4=", "owner": "informalsystems", "repo": "cosmos.nix", - "rev": "0475f2c4fff872218f70bae9c0c2ea49e2cfe785", + "rev": "759c1f0771897d4857c3f853257bddb603c10f41", "type": "github" }, "original": { "owner": "informalsystems", + "ref": "simd-polymer-multihop", "repo": "cosmos.nix", "type": "github" } @@ -948,6 +950,23 @@ "type": "github" } }, + "ibc-go-v8-polymer-multihop-src": { + "flake": false, + "locked": { + "lastModified": 1701789024, + "narHash": "sha256-vGujPpdEZO0SvgifKmN+SWntJEkistj4xjtfkkJImgw=", + "owner": "polymerdao", + "repo": "ibc-go", + "rev": "b5d5877fbe63c187896c5185920a7382340dc30f", + "type": "github" + }, + "original": { + "owner": "polymerdao", + "ref": "polymer/multihop-main", + "repo": "ibc-go", + "type": "github" + } + }, "ibc-go-v8-src": { "flake": false, "locked": { @@ -1360,11 +1379,11 @@ }, "nixpkgs_6": { "locked": { - "lastModified": 1717893485, - "narHash": "sha256-WMU6ZRZrBgEUDIF0siu2aIyVAXcxfElSwzZtS/mSpN4=", + "lastModified": 1720058333, + "narHash": "sha256-gM2RCi5XkxmcsZ44pUkKIYBiBMfZ6u7MdcZcykmccrs=", "owner": "nixos", "repo": "nixpkgs", - "rev": "3bcedce9f4de37570242faf16e1e143583407eab", + "rev": "6842b061970bf96965d66fcc86a28e1f719aae95", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index fd4d4f6fee..f90d5d01b7 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,7 @@ inputs = { nixpkgs.url = github:nixos/nixpkgs/nixpkgs-unstable; flake-utils.url = github:numtide/flake-utils; - cosmos-nix.url = github:informalsystems/cosmos.nix; + cosmos-nix.url = github:informalsystems/cosmos.nix/simd-polymer-multihop; }; outputs = inputs: let @@ -40,6 +40,7 @@ ibc-go-v6-simapp ibc-go-v7-simapp ibc-go-v8-simapp + ibc-go-v8-polymer-multihop-simapp interchain-security migaloo neutron diff --git a/guide/src/templates/commands/hermes/create/channel_3.md b/guide/src/templates/commands/hermes/create/channel_3.md new file mode 100644 index 0000000000..9201e0c726 --- /dev/null +++ b/guide/src/templates/commands/hermes/create/channel_3.md @@ -0,0 +1 @@ +[[#BINARY hermes]][[#GLOBALOPTIONS]] create channel[[#OPTIONS]] --a-chain [[#A_CHAIN_ID]] --a-connection [[#A_CONNECTION_ID]] --connection-hops [[#CONNECTION_HOP_IDS]] --a-port [[#A_PORT_ID]] --b-port [[#B_PORT_ID]] diff --git a/guide/src/templates/commands/hermes/create/channel_4.md b/guide/src/templates/commands/hermes/create/channel_4.md new file mode 100644 index 0000000000..a2b214b69a --- /dev/null +++ b/guide/src/templates/commands/hermes/create/channel_4.md @@ -0,0 +1 @@ +NOTE: The `--new-client-connection` option does not support connection hops. To open a multi-hop channel, please provide existing connections or initialize them manually before invoking this command. diff --git a/guide/src/templates/help_templates/create/channel.md b/guide/src/templates/help_templates/create/channel.md index 957e4ad9e5..4e3c98e673 100644 --- a/guide/src/templates/help_templates/create/channel.md +++ b/guide/src/templates/help_templates/create/channel.md @@ -9,6 +9,10 @@ USAGE: hermes create channel [OPTIONS] --a-chain --b-chain --a-port --b-port --new-client-connection + hermes create channel [OPTIONS] --a-chain --a-connection --connection-hops --a-port --b-port + + NOTE: The `--new-client-connection` option does not support connection hops. To open a multi-hop channel, please provide existing connections or initialize them manually before invoking this command. + OPTIONS: --channel-version The version for the new channel @@ -48,3 +52,9 @@ FLAGS: --b-port Identifier of the side `b` port for the new channel + + --connection-hops + A list of identifiers of the intermediate connections between side `a` and side `b` for + a multi-hop channel, separated by slashes, e.g, 'connection-1/connection-0' (optional). + + [aliases: conn-hops] diff --git a/guide/src/templates/help_templates/tx/chan-open-init.md b/guide/src/templates/help_templates/tx/chan-open-init.md index 661056c422..2bae34362a 100644 --- a/guide/src/templates/help_templates/tx/chan-open-init.md +++ b/guide/src/templates/help_templates/tx/chan-open-init.md @@ -5,9 +5,17 @@ USAGE: hermes tx chan-open-init [OPTIONS] --dst-chain --src-chain --dst-connection --dst-port --src-port OPTIONS: - -h, --help Print help information - --order The channel ordering, valid options 'unordered' (default) and 'ordered' - [default: ORDER_UNORDERED] + --connection-hops + A list of identifiers of the intermediate connections between a destination and a source + chain for a multi-hop channel, separated by slashes, e.g, 'connection-1/connection-0' + (optional) + + -h, --help + Print help information + + --order + The channel ordering, valid options 'unordered' (default) and 'ordered' [default: + ORDER_UNORDERED] REQUIRED: --dst-chain diff --git a/tools/check-guide/Cargo.toml b/tools/check-guide/Cargo.toml index e5a77be25f..ce9345c734 100644 --- a/tools/check-guide/Cargo.toml +++ b/tools/check-guide/Cargo.toml @@ -11,3 +11,7 @@ lazy_static = "1.4.0" mdbook-template = "1.1.0" regex = "1" walkdir = "2.3.3" + +[patch.crates-io] +ibc-proto = { git = "https://github.com/cosmos/ibc-proto-rs.git", branch = "multihop" } + diff --git a/tools/integration-test/Cargo.toml b/tools/integration-test/Cargo.toml index c28ce0ba62..feb40008ba 100644 --- a/tools/integration-test/Cargo.toml +++ b/tools/integration-test/Cargo.toml @@ -49,6 +49,7 @@ juno = [] dynamic-gas-fee = [] new-register-interchain-account = [] authz = [] +multihop = [] [[bin]] name = "test_setup_with_binary_channel" diff --git a/tools/integration-test/src/tests/mod.rs b/tools/integration-test/src/tests/mod.rs index 34534b57f7..3f64fbe61c 100644 --- a/tools/integration-test/src/tests/mod.rs +++ b/tools/integration-test/src/tests/mod.rs @@ -74,3 +74,6 @@ pub mod interchain_security; #[cfg(any(doc, feature = "dynamic-gas-fee"))] pub mod dynamic_gas_fee; + +#[cfg(any(doc, feature = "multihop"))] +pub mod multihop; diff --git a/tools/integration-test/src/tests/multihop/handshake.rs b/tools/integration-test/src/tests/multihop/handshake.rs new file mode 100644 index 0000000000..f943bd710d --- /dev/null +++ b/tools/integration-test/src/tests/multihop/handshake.rs @@ -0,0 +1,208 @@ +use ibc_relayer::channel::{extract_channel_id, Channel, ChannelSide}; +use ibc_relayer_types::core::ics33_multihop::channel_path::ConnectionHops; +use ibc_test_framework::prelude::*; +use ibc_test_framework::relayer::channel::assert_eventually_multihop_channel_established; +use ibc_test_framework::relayer::connection::create_connection_hop; + +#[test] +fn test_multihop_channel_open_handshake() -> Result<(), Error> { + run_nary_connection_test(&MultihopChannelHandshakeTest) +} + +pub struct MultihopChannelHandshakeTest; + +impl TestOverrides for MultihopChannelHandshakeTest { + fn modify_test_config(&self, config: &mut TestConfig) { + config.bootstrap_with_random_ids = false; + } + + fn modify_relayer_config(&self, config: &mut Config) { + config.mode.clients.misbehaviour = false; + } + + fn should_spawn_supervisor(&self) -> bool { + false + } +} + +/// FIXME(MULTIHOP): Currently in order to have the correct channel at each step +/// the Channel is built manually. +/// Once the restore_from_event has been updated to be compatible with +/// multihop this test can be improved by using restore_from_event at each +/// step. +impl NaryConnectionTest<3> for MultihopChannelHandshakeTest { + fn run( + &self, + _config: &TestConfig, + _relayer: RelayerDriver, + chains: NaryConnectedChains, + connections: ConnectedConnections, + ) -> Result<(), Error> { + let chain_handle_a = chains.chain_handle_at::<0>()?; + let chain_handle_b = chains.chain_handle_at::<1>()?; + let chain_handle_c = chains.chain_handle_at::<2>()?; + + let client_b_on_a = chains.foreign_client_at::<1, 0>()?; + let client_b_on_c = chains.foreign_client_at::<1, 2>()?; + + let connection_a_to_b = connections.connection_at::<0, 1>()?; + let connection_b_to_a = connections.connection_at::<1, 0>()?; + let connection_b_to_c = connections.connection_at::<1, 2>()?; + let connection_c_to_b = connections.connection_at::<2, 1>()?; + + let connection_hop_a1 = create_connection_hop( + chain_handle_a.value(), + &chain_handle_a.value().id(), + &chain_handle_b.value().id(), + connection_a_to_b.connection_id_a.value(), + )?; + let connection_hop_a2 = create_connection_hop( + chain_handle_b.value(), + &chain_handle_b.value().id(), + &chain_handle_c.value().id(), + connection_b_to_c.connection_id_a.value(), + )?; + + let connection_hop_c1 = create_connection_hop( + chain_handle_c.value(), + &chain_handle_c.value().id(), + &chain_handle_b.value().id(), + connection_c_to_b.connection_id_a.value(), + )?; + let connection_hop_c2 = create_connection_hop( + chain_handle_b.value(), + &chain_handle_b.value().id(), + &chain_handle_a.value().id(), + connection_b_to_a.connection_id_a.value(), + )?; + + let connection_hops_a = ConnectionHops::new(vec![connection_hop_a1, connection_hop_a2]); + let connection_hops_b = ConnectionHops::new(vec![connection_hop_c1, connection_hop_c2]); + + let channel_a_to_c = Channel { + connection_delay: Default::default(), + ordering: Ordering::Unordered, + a_side: ChannelSide::new( + chain_handle_a.value().clone(), + client_b_on_a.id.clone(), + connection_a_to_b.connection_id_a.value().clone(), + Some(connection_hops_a.clone()), + PortId::transfer(), + None, + None, + ), + b_side: ChannelSide::new( + chain_handle_c.value().clone(), + client_b_on_c.id.clone(), + connection_c_to_b.connection_id_a.value().clone(), + Some(connection_hops_b.clone()), + PortId::transfer(), + None, + None, + ), + }; + + let event = channel_a_to_c.build_chan_open_init_and_send()?; + // FIXME(MULTIHOP): restore_from_event does not work with multihop + // * Will not fill multihop field + // * Uses counterparty connection for dst connection, but this results in an intermediary connection. + // * Potentially same issue with client + let tmp_channel_c_to_a = Channel::restore_from_event( + chain_handle_c.value().clone(), + chain_handle_a.value().clone(), + event, + )?; + + let channel_id_a = tmp_channel_c_to_a.a_side.channel_id().cloned(); + + // Extracting the channel from the event fails + let channel_c_to_a = Channel { + connection_delay: tmp_channel_c_to_a.connection_delay, + ordering: tmp_channel_c_to_a.ordering, + a_side: ChannelSide::new( + tmp_channel_c_to_a.a_side.chain.clone(), + client_b_on_c.id.clone(), + connection_c_to_b.connection_id_a.value().clone(), + Some(connection_hops_b.clone()), + tmp_channel_c_to_a.a_side.port_id().clone(), + channel_id_a.clone(), + tmp_channel_c_to_a.a_side.version().cloned(), + ), + b_side: ChannelSide::new( + tmp_channel_c_to_a.b_side.chain.clone(), + client_b_on_a.id.clone(), + connection_a_to_b.connection_id_a.value().clone(), + Some(connection_hops_a.clone()), + tmp_channel_c_to_a.b_side.port_id().clone(), + tmp_channel_c_to_a.b_side.channel_id().cloned(), + tmp_channel_c_to_a.b_side.version().cloned(), + ), + }; + + std::thread::sleep(core::time::Duration::from_secs(10)); + let event = channel_c_to_a.build_chan_open_try_and_send()?; + let channel_id_c = extract_channel_id(&event)?.clone(); + + let channel_a_to_c = Channel { + connection_delay: tmp_channel_c_to_a.connection_delay, + ordering: tmp_channel_c_to_a.ordering, + a_side: ChannelSide::new( + chain_handle_a.value().clone(), + client_b_on_a.id.clone(), + connection_a_to_b.connection_id_a.value().clone(), + Some(connection_hops_a.clone()), + tmp_channel_c_to_a.b_side.port_id().clone(), + Some(channel_id_c.clone()), + tmp_channel_c_to_a.b_side.version().cloned(), + ), + b_side: ChannelSide::new( + chain_handle_c.value().clone(), + client_b_on_c.id.clone(), + connection_c_to_b.connection_id_a.value().clone(), + Some(connection_hops_b.clone()), + tmp_channel_c_to_a.a_side.port_id().clone(), + tmp_channel_c_to_a.a_side.channel_id().cloned(), + tmp_channel_c_to_a.a_side.version().cloned(), + ), + }; + + channel_a_to_c.build_chan_open_ack_and_send()?; + std::thread::sleep(core::time::Duration::from_secs(10)); + + // Extracting the channel from the event fails + let channel_c_to_a = Channel { + connection_delay: tmp_channel_c_to_a.connection_delay, + ordering: tmp_channel_c_to_a.ordering, + a_side: ChannelSide::new( + tmp_channel_c_to_a.a_side.chain.clone(), + client_b_on_c.id.clone(), + connection_c_to_b.connection_id_a.value().clone(), + Some(connection_hops_b.clone()), + tmp_channel_c_to_a.a_side.port_id().clone(), + channel_id_a.clone(), + tmp_channel_c_to_a.a_side.version().cloned(), + ), + b_side: ChannelSide::new( + tmp_channel_c_to_a.b_side.chain.clone(), + client_b_on_a.id.clone(), + connection_a_to_b.connection_id_a.value().clone(), + Some(connection_hops_a.clone()), + tmp_channel_c_to_a.b_side.port_id().clone(), + Some(channel_id_c.clone()), + tmp_channel_c_to_a.b_side.version().cloned(), + ), + }; + channel_c_to_a.build_chan_open_confirm_and_send()?; + + assert_eventually_multihop_channel_established( + &chain_handle_a, + &chain_handle_c, + &channel_id_c, + tmp_channel_c_to_a.b_side.port_id(), + &connection_hops_a.connection_ids(), + &connection_hops_b.connection_ids(), + )?; + + Ok(()) + } +} diff --git a/tools/integration-test/src/tests/multihop/mod.rs b/tools/integration-test/src/tests/multihop/mod.rs new file mode 100644 index 0000000000..044564cacd --- /dev/null +++ b/tools/integration-test/src/tests/multihop/mod.rs @@ -0,0 +1,2 @@ +pub mod handshake; +pub mod transfer; diff --git a/tools/integration-test/src/tests/multihop/transfer.rs b/tools/integration-test/src/tests/multihop/transfer.rs new file mode 100644 index 0000000000..b06b5d21fc --- /dev/null +++ b/tools/integration-test/src/tests/multihop/transfer.rs @@ -0,0 +1,262 @@ +use ibc_relayer::channel::{extract_channel_id, Channel, ChannelSide}; +use ibc_relayer_types::core::ics33_multihop::channel_path::ConnectionHops; +use ibc_test_framework::prelude::*; +use ibc_test_framework::relayer::channel::assert_eventually_multihop_channel_established; +use ibc_test_framework::relayer::connection::create_connection_hop; +use ibc_test_framework::util::random::random_u128_range; + +#[test] +fn test_multihop_transfer() -> Result<(), Error> { + run_nary_connection_test(&MultihopTransferTest) +} + +pub struct MultihopTransferTest; + +impl TestOverrides for MultihopTransferTest { + fn modify_test_config(&self, config: &mut TestConfig) { + config.bootstrap_with_random_ids = false; + } + + fn modify_relayer_config(&self, config: &mut Config) { + config.mode.clients.misbehaviour = false; + } + + fn should_spawn_supervisor(&self) -> bool { + false + } +} + +/// FIXME: Currently in order to have the correct channel at each step +/// the Channel is built manually. +/// Once the restore_from_event has been updated to be compatible with +/// multihop this test can be improved by using restore_from_event at each +/// step. +impl NaryConnectionTest<3> for MultihopTransferTest { + fn run( + &self, + _config: &TestConfig, + relayer: RelayerDriver, + chains: NaryConnectedChains, + connections: ConnectedConnections, + ) -> Result<(), Error> { + let chain_handle_a = chains.chain_handle_at::<0>()?; + let chain_handle_b = chains.chain_handle_at::<1>()?; + let chain_handle_c = chains.chain_handle_at::<2>()?; + + let node_a = chains.full_node_at::<0>()?; + let node_c = chains.full_node_at::<2>()?; + + let client_b_on_a = chains.foreign_client_at::<1, 0>()?; + let client_b_on_c = chains.foreign_client_at::<1, 2>()?; + + let connection_a_to_b = connections.connection_at::<0, 1>()?; + let connection_b_to_a = connections.connection_at::<1, 0>()?; + let connection_b_to_c = connections.connection_at::<1, 2>()?; + let connection_c_to_b = connections.connection_at::<2, 1>()?; + + let connection_hop_a1 = create_connection_hop( + chain_handle_a.value(), + &chain_handle_a.value().id(), + &chain_handle_b.value().id(), + connection_a_to_b.connection_id_a.value(), + )?; + let connection_hop_a2 = create_connection_hop( + chain_handle_b.value(), + &chain_handle_b.value().id(), + &chain_handle_c.value().id(), + connection_b_to_c.connection_id_a.value(), + )?; + + let connection_hop_c1 = create_connection_hop( + chain_handle_c.value(), + &chain_handle_c.value().id(), + &chain_handle_b.value().id(), + connection_c_to_b.connection_id_a.value(), + )?; + let connection_hop_c2 = create_connection_hop( + chain_handle_b.value(), + &chain_handle_b.value().id(), + &chain_handle_a.value().id(), + connection_b_to_a.connection_id_a.value(), + )?; + + let connection_hops_a = ConnectionHops::new(vec![connection_hop_a1, connection_hop_a2]); + let connection_hops_b = ConnectionHops::new(vec![connection_hop_c1, connection_hop_c2]); + + let channel_a_to_c = Channel { + connection_delay: Default::default(), + ordering: Ordering::Unordered, + a_side: ChannelSide::new( + chain_handle_a.value().clone(), + client_b_on_a.id.clone(), + connection_a_to_b.connection_id_a.value().clone(), + Some(connection_hops_a.clone()), + PortId::transfer(), + None, + None, + ), + b_side: ChannelSide::new( + chain_handle_c.value().clone(), + client_b_on_c.id.clone(), + connection_c_to_b.connection_id_a.value().clone(), + Some(connection_hops_b.clone()), + PortId::transfer(), + None, + None, + ), + }; + + let event = channel_a_to_c.build_chan_open_init_and_send()?; + // FIXME: restore_from_event does not work with multihop + // * Will not fill multihop field + // * Uses counterparty connection for dst connection, but this results in an intermediary connection. + // * Potentially same issue with client + let tmp_channel_c_to_a = Channel::restore_from_event( + chain_handle_c.value().clone(), + chain_handle_a.value().clone(), + event, + )?; + + let channel_id_a = tmp_channel_c_to_a.a_side.channel_id().cloned(); + + // Extracting the channel from the event fails + let channel_c_to_a = Channel { + connection_delay: tmp_channel_c_to_a.connection_delay, + ordering: tmp_channel_c_to_a.ordering, + a_side: ChannelSide::new( + tmp_channel_c_to_a.a_side.chain.clone(), + client_b_on_c.id.clone(), + connection_c_to_b.connection_id_a.value().clone(), + Some(connection_hops_b.clone()), + tmp_channel_c_to_a.a_side.port_id().clone(), + channel_id_a.clone(), + tmp_channel_c_to_a.a_side.version().cloned(), + ), + b_side: ChannelSide::new( + tmp_channel_c_to_a.b_side.chain.clone(), + client_b_on_a.id.clone(), + connection_a_to_b.connection_id_a.value().clone(), + Some(connection_hops_a.clone()), + tmp_channel_c_to_a.b_side.port_id().clone(), + tmp_channel_c_to_a.b_side.channel_id().cloned(), + tmp_channel_c_to_a.b_side.version().cloned(), + ), + }; + + std::thread::sleep(core::time::Duration::from_secs(10)); + let event = channel_c_to_a.build_chan_open_try_and_send()?; + let channel_id_c = extract_channel_id(&event)?.clone(); + + let channel_a_to_c = Channel { + connection_delay: tmp_channel_c_to_a.connection_delay, + ordering: tmp_channel_c_to_a.ordering, + a_side: ChannelSide::new( + chain_handle_a.value().clone(), + client_b_on_a.id.clone(), + connection_a_to_b.connection_id_a.value().clone(), + Some(connection_hops_a.clone()), + tmp_channel_c_to_a.b_side.port_id().clone(), + Some(channel_id_c.clone()), + tmp_channel_c_to_a.b_side.version().cloned(), + ), + b_side: ChannelSide::new( + chain_handle_c.value().clone(), + client_b_on_c.id.clone(), + connection_c_to_b.connection_id_a.value().clone(), + Some(connection_hops_b.clone()), + tmp_channel_c_to_a.a_side.port_id().clone(), + tmp_channel_c_to_a.a_side.channel_id().cloned(), + tmp_channel_c_to_a.a_side.version().cloned(), + ), + }; + + channel_a_to_c.build_chan_open_ack_and_send()?; + std::thread::sleep(core::time::Duration::from_secs(10)); + + // Extracting the channel from the event fails + let channel_c_to_a = Channel { + connection_delay: tmp_channel_c_to_a.connection_delay, + ordering: tmp_channel_c_to_a.ordering, + a_side: ChannelSide::new( + tmp_channel_c_to_a.a_side.chain.clone(), + client_b_on_c.id.clone(), + connection_c_to_b.connection_id_a.value().clone(), + Some(connection_hops_b.clone()), + tmp_channel_c_to_a.a_side.port_id().clone(), + channel_id_a.clone(), + tmp_channel_c_to_a.a_side.version().cloned(), + ), + b_side: ChannelSide::new( + tmp_channel_c_to_a.b_side.chain.clone(), + client_b_on_a.id.clone(), + connection_a_to_b.connection_id_a.value().clone(), + Some(connection_hops_a.clone()), + tmp_channel_c_to_a.b_side.port_id().clone(), + Some(channel_id_c.clone()), + tmp_channel_c_to_a.b_side.version().cloned(), + ), + }; + channel_c_to_a.build_chan_open_confirm_and_send()?; + + assert_eventually_multihop_channel_established( + &chain_handle_a, + &chain_handle_c, + &channel_id_c, + tmp_channel_c_to_a.b_side.port_id(), + &connection_hops_a.connection_ids(), + &connection_hops_b.connection_ids(), + )?; + + let denom_a = node_a.denom(); + let wallet_a = node_a.wallets().user1().cloned(); + let wallet_c = node_c.wallets().user1().cloned(); + + let balance_a = node_a + .chain_driver() + .query_balance(&wallet_a.address(), &denom_a)?; + + let a_to_b_amount = random_u128_range(1000, 5000); + + info!( + "Sending IBC transfer from chain {} to chain {} with amount of {} {}", + chain_handle_a.id(), + chain_handle_c.id(), + a_to_b_amount, + denom_a + ); + + let channel_id_a = TaggedChannelId::new(channel_a_to_c.a_channel_id().unwrap().clone()); + let port_a = DualTagged::new(channel_a_to_c.a_side.port_id().clone()); + let channel_id_c = TaggedChannelId::new(channel_a_to_c.b_channel_id().unwrap().clone()); + let port_c = DualTagged::new(channel_a_to_c.b_side.port_id().clone()); + + relayer.with_supervisor(|| { + node_a.chain_driver().ibc_transfer_token( + &port_a.as_ref(), + &channel_id_a.as_ref(), + &wallet_a.as_ref(), + &wallet_c.address(), + &denom_a.with_amount(a_to_b_amount).as_ref(), + )?; + + let denom_c = derive_ibc_denom(&port_c.as_ref(), &channel_id_c.as_ref(), &denom_a)?; + + info!( + "Waiting for user on chain B to receive IBC transferred amount of {}", + a_to_b_amount + ); + + node_a.chain_driver().assert_eventual_wallet_amount( + &wallet_a.address(), + &(balance_a - a_to_b_amount).as_ref(), + )?; + + node_c.chain_driver().assert_eventual_wallet_amount( + &wallet_c.address(), + &denom_c.with_amount(a_to_b_amount).as_ref(), + )?; + + Ok(()) + }) + } +} diff --git a/tools/test-framework/src/bootstrap/binary/chain.rs b/tools/test-framework/src/bootstrap/binary/chain.rs index 365c079e8c..d21c3b89f0 100644 --- a/tools/test-framework/src/bootstrap/binary/chain.rs +++ b/tools/test-framework/src/bootstrap/binary/chain.rs @@ -3,20 +3,22 @@ with connected foreign clients. */ +use std::path::Path; +use std::time::Duration; +use std::{fs, thread}; + use eyre::Report as Error; -use ibc_relayer::chain::handle::{ChainHandle, CountingAndCachingChainHandle}; +use tracing::{debug, info}; + +use ibc_relayer::chain::handle::ChainHandle; use ibc_relayer::config::Config; use ibc_relayer::error::ErrorDetail as RelayerErrorDetail; use ibc_relayer::foreign_client::{ extract_client_id, CreateOptions as ClientOptions, ForeignClient, }; use ibc_relayer::keyring::errors::ErrorDetail as KeyringErrorDetail; -use ibc_relayer::registry::SharedRegistry; +use ibc_relayer::registry::{set_global_registry, SharedRegistry}; use ibc_relayer_types::core::ics24_host::identifier::ClientId; -use std::path::Path; -use std::time::Duration; -use std::{fs, thread}; -use tracing::{debug, info}; use crate::relayer::driver::RelayerDriver; use crate::types::binary::chains::ConnectedChains; @@ -66,6 +68,7 @@ pub fn bootstrap_chains_with_full_nodes( save_relayer_config(&config, &config_path)?; let registry = new_registry(config.clone()); + set_global_registry(registry.clone()); // Pass in unique closure expressions `||{}` as the first argument so that // the returned chains are considered different types by Rust. @@ -189,7 +192,7 @@ pub fn pad_client_ids( */ pub fn spawn_chain_handle( _: Seed, - registry: &SharedRegistry, + registry: &SharedRegistry, node: &FullNode, ) -> Result { let chain_id = &node.chain_driver.chain_id; @@ -245,8 +248,8 @@ pub fn add_keys_to_chain_handle( Create a new [`SharedRegistry`] that uses [`CountingAndCachingChainHandle`] as the [`ChainHandle`] implementation. */ -pub fn new_registry(config: Config) -> SharedRegistry { - >::new(config) +pub fn new_registry(config: Config) -> SharedRegistry { + SharedRegistry::new(config) } /** diff --git a/tools/test-framework/src/bootstrap/binary/channel.rs b/tools/test-framework/src/bootstrap/binary/channel.rs index 4b1439568a..f2be601bae 100644 --- a/tools/test-framework/src/bootstrap/binary/channel.rs +++ b/tools/test-framework/src/bootstrap/binary/channel.rs @@ -104,6 +104,8 @@ pub fn bootstrap_channel_with_connection( chain_b.clone(), client_id_b.value().clone(), connection.connection_id_b.value().clone(), + None, port_id.cloned().into_value(), None, None, @@ -184,6 +187,7 @@ pub fn pad_channel_id( chain_a.clone(), client_id_a.value().clone(), connection.connection_id_a.value().clone(), + None, port_id.cloned().into_value(), None, None, diff --git a/tools/test-framework/src/bootstrap/nary/chain.rs b/tools/test-framework/src/bootstrap/nary/chain.rs index 3a758b7446..763a7b17b1 100644 --- a/tools/test-framework/src/bootstrap/nary/chain.rs +++ b/tools/test-framework/src/bootstrap/nary/chain.rs @@ -2,9 +2,9 @@ Functions for bootstrapping N-ary number of chains. */ -use ibc_relayer::chain::handle::ChainHandle; +use ibc_relayer::chain::handle::{ChainHandle, DefaultChainHandle}; use ibc_relayer::config::Config; -use ibc_relayer::registry::SharedRegistry; +use ibc_relayer::registry::{set_global_registry, SharedRegistry}; use crate::bootstrap::binary::chain::{ add_chain_config, add_keys_to_chain_handle, new_registry, save_relayer_config, @@ -81,6 +81,7 @@ pub fn boostrap_chains_with_any_nodes( save_relayer_config(&config, &config_path)?; let registry = new_registry(config.clone()); + set_global_registry(registry.clone()); let mut chain_handles = Vec::new(); @@ -120,10 +121,10 @@ pub fn boostrap_chains_with_any_nodes( Ok((relayer, connected_chains)) } -fn spawn_chain_handle( - registry: &SharedRegistry, +fn spawn_chain_handle( + registry: &SharedRegistry, node: &FullNode, -) -> Result { +) -> Result { let chain_id = &node.chain_driver.chain_id; let handle = registry .get_or_spawn(chain_id) diff --git a/tools/test-framework/src/prelude.rs b/tools/test-framework/src/prelude.rs index cf84da30f0..406f5f25cd 100644 --- a/tools/test-framework/src/prelude.rs +++ b/tools/test-framework/src/prelude.rs @@ -66,6 +66,7 @@ pub use crate::types::config::TestConfig; pub use crate::types::id::*; pub use crate::types::nary::chains::NaryConnectedChains; pub use crate::types::nary::channel::ConnectedChannels as NaryConnectedChannels; +pub use crate::types::nary::connection::ConnectedConnections; pub use crate::types::nary::connection::ConnectedConnections as NaryConnectedConnections; pub use crate::types::single::node::{FullNode, TaggedFullNodeExt}; pub use crate::types::tagged::{DualTagged, MonoTagged}; diff --git a/tools/test-framework/src/relayer/channel.rs b/tools/test-framework/src/relayer/channel.rs index 2ea35886c9..50f9d014af 100644 --- a/tools/test-framework/src/relayer/channel.rs +++ b/tools/test-framework/src/relayer/channel.rs @@ -6,12 +6,13 @@ use ibc_relayer::chain::requests::{ }; use ibc_relayer::channel::version::Version as ChannelEndVersion; use ibc_relayer::channel::{extract_channel_id, Channel, ChannelSide}; +use ibc_relayer::util::pretty::PrettySlice; use ibc_relayer_types::core::ics04_channel::channel::{ ChannelEnd, IdentifiedChannelEnd, Ordering, State as ChannelState, UpgradeState, }; use ibc_relayer_types::core::ics04_channel::packet::Sequence; use ibc_relayer_types::core::ics04_channel::version::Version; -use ibc_relayer_types::core::ics24_host::identifier::ConnectionId; +use ibc_relayer_types::core::ics24_host::identifier::{ChannelId, ConnectionId, PortId}; use crate::error::Error; use crate::types::id::{ @@ -121,6 +122,7 @@ pub fn init_channel( handle_a.clone(), client_id_a.cloned_value(), connection_id_a.cloned_value(), + None, src_port_id.cloned_value(), None, None, @@ -129,6 +131,7 @@ pub fn init_channel( handle_b.clone(), client_id_b.cloned_value(), connection_id_b.cloned_value(), + None, dst_port_id.cloned_value(), None, None, @@ -160,6 +163,7 @@ pub fn init_channel_version( handle_a.clone(), client_id_a.cloned_value(), connection_id_a.cloned_value(), + None, src_port_id.cloned_value(), None, Some(version.clone()), @@ -168,6 +172,7 @@ pub fn init_channel_version( handle_b.clone(), client_id_b.cloned_value(), connection_id_b.cloned_value(), + None, dst_port_id.cloned_value(), None, Some(version), @@ -197,6 +202,7 @@ pub fn init_channel_optimistic( handle_a.clone(), client_id_a.cloned_value(), ConnectionId::default(), + None, src_port_id.cloned_value(), None, None, @@ -205,6 +211,7 @@ pub fn init_channel_optimistic( handle_b.clone(), client_id_b.cloned_value(), connection_id_b.cloned_value(), + None, dst_port_id.cloned_value(), None, None, @@ -341,6 +348,78 @@ pub fn assert_eventually_channel_established( + handle_a: &ChainA, + handle_b: &ChainB, + channel_id_a: &ChannelId, + port_id_a: &PortId, + multi_hop_a: &Vec, + multi_hop_b: &Vec, +) -> Result { + assert_eventually_succeed( + "channel should eventually established", + 20, + Duration::from_secs(2), + || { + let (channel_end_a, _) = handle_a.query_channel( + QueryChannelRequest { + port_id: port_id_a.clone(), + channel_id: channel_id_a.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::Yes, + )?; + + if !channel_end_a.state_matches(&ChannelState::Open(UpgradeState::NotUpgrading)) { + return Err(Error::generic(eyre!( + "expected channel end A to be in open state" + ))); + } + + if channel_end_a.connection_hops.ne(multi_hop_a) { + return Err(Error::generic(eyre!( + "connection_hops for chain {} do not match. Expected {}, Got {}", + handle_a.id(), + PrettySlice(multi_hop_a), + PrettySlice(&channel_end_a.connection_hops) + ))); + } + + let channel_id_b = channel_end_a.counterparty().channel_id().ok_or_else(|| { + eyre!("expected counterparty channel id to present on open channel") + })?; + + let port_id_b = channel_end_a.counterparty().port_id(); + + let (channel_end_b, _) = handle_b.query_channel( + QueryChannelRequest { + port_id: port_id_b.clone(), + channel_id: channel_id_b.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::Yes, + )?; + + if !channel_end_b.state_matches(&ChannelState::Open(UpgradeState::NotUpgrading)) { + return Err(Error::generic(eyre!( + "expected channel end B to be in open state" + ))); + } + + if channel_end_b.connection_hops.ne(multi_hop_b) { + return Err(Error::generic(eyre!( + "connection_hops for chain {} do not match. Expected {}, Got {}", + handle_a.id(), + PrettySlice(multi_hop_b), + PrettySlice(&channel_end_b.connection_hops) + ))); + } + + Ok(channel_id_b.clone()) + }, + ) +} + pub fn assert_eventually_channel_closed( handle_a: &ChainA, handle_b: &ChainB, diff --git a/tools/test-framework/src/relayer/connection.rs b/tools/test-framework/src/relayer/connection.rs index 1c9b3d375c..7b82d1c891 100644 --- a/tools/test-framework/src/relayer/connection.rs +++ b/tools/test-framework/src/relayer/connection.rs @@ -11,6 +11,8 @@ use ibc_relayer_types::core::ics03_connection::connection::State as ConnectionSt use ibc_relayer_types::core::ics03_connection::connection::{ ConnectionEnd, IdentifiedConnectionEnd, }; +use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ConnectionId}; +use ibc_relayer_types::core::ics33_multihop::channel_path::ConnectionHop; use ibc_relayer_types::timestamp::ZERO_DURATION; use crate::error::Error; @@ -194,3 +196,29 @@ pub fn assert_eventually_connection_established( + handle: &Handle, + src_chain_id: &ChainId, + dst_chain_id: &ChainId, + connection_id: &ConnectionId, +) -> Result { + let (connection_end, _) = handle.query_connection( + QueryConnectionRequest { + connection_id: connection_id.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + )?; + + let identified_connection_end = + IdentifiedConnectionEnd::new(connection_id.clone(), connection_end); + + let hop = ConnectionHop::new( + identified_connection_end, + src_chain_id.clone(), + dst_chain_id.clone(), + ); + + Ok(hop) +} diff --git a/tools/test-framework/src/relayer/driver.rs b/tools/test-framework/src/relayer/driver.rs index 967df6355b..4a8f0822d4 100644 --- a/tools/test-framework/src/relayer/driver.rs +++ b/tools/test-framework/src/relayer/driver.rs @@ -2,11 +2,11 @@ Driver for spawning the relayer. */ -use ibc_relayer::chain::handle::CountingAndCachingChainHandle; +use std::path::PathBuf; + use ibc_relayer::config::Config; use ibc_relayer::registry::SharedRegistry; use ibc_relayer::supervisor::{spawn_supervisor, SupervisorHandle, SupervisorOptions}; -use std::path::PathBuf; use crate::error::Error; use crate::types::env::{EnvWriter, ExportEnv}; @@ -41,7 +41,7 @@ pub struct RelayerDriver { Use this shared registry when spawning new supervisor using [`spawn_supervisor`]. */ - pub registry: SharedRegistry, + pub registry: SharedRegistry, /** Whether the driver should hang the test when the continuation