diff --git a/crates/relayer-cli/src/commands/tx.rs b/crates/relayer-cli/src/commands/tx.rs index a50ce867e0..dc5099250c 100644 --- a/crates/relayer-cli/src/commands/tx.rs +++ b/crates/relayer-cli/src/commands/tx.rs @@ -59,6 +59,9 @@ pub enum TxCmd { /// Relay the channel upgrade cancellation (ChannelUpgradeCancel) ChanUpgradeCancel(channel::TxChanUpgradeCancelCmd), + /// Relay the channel upgrade timeout (ChannelUpgradeTimeout) + ChanUpgradeTimeout(channel::TxChanUpgradeTimeoutCmd), + /// Send a fungible token transfer test transaction (ICS20 MsgTransfer) FtTransfer(transfer::TxIcs20MsgTransferCmd), diff --git a/crates/relayer-cli/src/commands/tx/channel.rs b/crates/relayer-cli/src/commands/tx/channel.rs index 103a123927..c43d78f54e 100644 --- a/crates/relayer-cli/src/commands/tx/channel.rs +++ b/crates/relayer-cli/src/commands/tx/channel.rs @@ -1315,6 +1315,136 @@ impl Runnable for TxChanUpgradeCancelCmd { } } +/// Relay channel upgrade timeout when counterparty has not flushed packets before upgrade timeout (ChannelUpgradeTimeout) +/// +/// Build and send a `ChannelUpgradeTimeout` message to timeout +/// the channel upgrade handshake given that the counterparty has not flushed packets before upgrade timeout. +#[derive(Clone, Command, Debug, Parser, PartialEq, Eq)] +pub struct TxChanUpgradeTimeoutCmd { + #[clap( + long = "dst-chain", + required = true, + value_name = "DST_CHAIN_ID", + help_heading = "REQUIRED", + help = "Identifier of the destination chain" + )] + dst_chain_id: ChainId, + + #[clap( + long = "src-chain", + required = true, + value_name = "SRC_CHAIN_ID", + help_heading = "REQUIRED", + help = "Identifier of the source chain" + )] + src_chain_id: ChainId, + + #[clap( + long = "dst-connection", + visible_alias = "dst-conn", + required = true, + value_name = "DST_CONNECTION_ID", + help_heading = "REQUIRED", + help = "Identifier of the destination connection" + )] + dst_conn_id: ConnectionId, + + #[clap( + long = "dst-port", + required = true, + value_name = "DST_PORT_ID", + help_heading = "REQUIRED", + help = "Identifier of the destination port" + )] + dst_port_id: PortId, + + #[clap( + long = "src-port", + required = true, + value_name = "SRC_PORT_ID", + help_heading = "REQUIRED", + help = "Identifier of the source port" + )] + src_port_id: PortId, + + #[clap( + long = "src-channel", + visible_alias = "src-chan", + required = true, + value_name = "SRC_CHANNEL_ID", + help_heading = "REQUIRED", + help = "Identifier of the source channel (required)" + )] + src_chan_id: ChannelId, + + #[clap( + long = "dst-channel", + visible_alias = "dst-chan", + required = true, + help_heading = "REQUIRED", + value_name = "DST_CHANNEL_ID", + help = "Identifier of the destination channel (optional)" + )] + dst_chan_id: Option, +} + +impl Runnable for TxChanUpgradeTimeoutCmd { + fn run(&self) { + 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(format!("{}", e)).exit(), + }; + + // Retrieve the 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(format!("{}", e)).exit(), + }; + + // Fetch the Channel that will facilitate the communication between the channel ends + // being upgraded. This channel is assumed to already exist on the destination chain. + let channel = 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, + ), + 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, + ), + }; + + info!("message ChanUpgradeTimeout: {}", channel); + + let res: Result = channel + .build_chan_upgrade_timeout_and_send() + .map_err(Error::channel); + + match res { + Ok(receipt) => Output::success(receipt).exit(), + Err(e) => Output::error(e).exit(), + } + } +} + #[cfg(test)] mod tests { use abscissa_core::clap::Parser; diff --git a/crates/relayer-types/src/core/ics04_channel/events.rs b/crates/relayer-types/src/core/ics04_channel/events.rs index 090c77ed33..a387850b0c 100644 --- a/crates/relayer-types/src/core/ics04_channel/events.rs +++ b/crates/relayer-types/src/core/ics04_channel/events.rs @@ -9,9 +9,11 @@ use crate::core::ics04_channel::channel::Ordering; use crate::core::ics04_channel::error::Error; use crate::core::ics04_channel::packet::Packet; use crate::core::ics04_channel::packet::Sequence; +use crate::core::ics04_channel::timeout::Timeout; use crate::core::ics04_channel::version::Version; use crate::core::ics24_host::identifier::{ChannelId, ConnectionId, PortId}; use crate::events::{Error as EventError, IbcEvent, IbcEventType}; +use crate::timestamp::Timestamp; use crate::utils::pretty::PrettySlice; /// Channel event attribute keys @@ -145,6 +147,7 @@ pub struct UpgradeAttributes { pub upgrade_version: Version, pub upgrade_sequence: Sequence, pub upgrade_ordering: Ordering, + pub upgrade_timeout: Option, } impl UpgradeAttributes { @@ -609,6 +612,7 @@ impl From for UpgradeAttributes { upgrade_version: ev.upgrade_version, upgrade_sequence: ev.upgrade_sequence, upgrade_ordering: ev.upgrade_ordering, + upgrade_timeout: None, } } } @@ -711,6 +715,7 @@ impl From for UpgradeAttributes { upgrade_version: ev.upgrade_version, upgrade_sequence: ev.upgrade_sequence, upgrade_ordering: ev.upgrade_ordering, + upgrade_timeout: None, } } } @@ -813,6 +818,7 @@ impl From for UpgradeAttributes { upgrade_version: ev.upgrade_version, upgrade_sequence: ev.upgrade_sequence, upgrade_ordering: ev.upgrade_ordering, + upgrade_timeout: None, } } } @@ -915,6 +921,7 @@ impl From for UpgradeAttributes { upgrade_version: ev.upgrade_version, upgrade_sequence: ev.upgrade_sequence, upgrade_ordering: ev.upgrade_ordering, + upgrade_timeout: None, } } } @@ -1017,6 +1024,7 @@ impl From for UpgradeAttributes { upgrade_version: ev.upgrade_version, upgrade_sequence: ev.upgrade_sequence, upgrade_ordering: ev.upgrade_ordering, + upgrade_timeout: None, } } } @@ -1119,6 +1127,7 @@ impl From for UpgradeAttributes { upgrade_version: ev.upgrade_version, upgrade_sequence: ev.upgrade_sequence, upgrade_ordering: ev.upgrade_ordering, + upgrade_timeout: None, } } } @@ -1180,6 +1189,114 @@ impl EventType for UpgradeCancel { } } +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct UpgradeTimeout { + pub port_id: PortId, + pub channel_id: ChannelId, + pub counterparty_port_id: PortId, + pub counterparty_channel_id: Option, + pub upgrade_connection_hops: Vec, + pub upgrade_version: Version, + pub upgrade_sequence: Sequence, + pub upgrade_ordering: Ordering, + pub upgrade_timeout: Timeout, +} + +impl Display for UpgradeTimeout { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { + if let Some(counterparty_channel_id) = &self.counterparty_channel_id { + write!(f, "UpgradeAttributes {{ port_id: {}, channel_id: {}, counterparty_port_id: {}, counterparty_channel_id: {counterparty_channel_id}, upgrade_connection_hops: [", self.port_id, self.channel_id, self.counterparty_port_id)?; + } else { + write!(f, "UpgradeAttributes {{ port_id: {}, channel_id: {}, counterparty_port_id: {}, counterparty_channel_id: None, upgrade_connection_hops: [", self.port_id, self.channel_id, self.counterparty_port_id)?; + } + for hop in self.upgrade_connection_hops.iter() { + write!(f, " {} ", hop)?; + } + write!( + f, + "], upgrade_version: {}, upgrade_sequence: {}, upgrade_ordering: {}, upgrade_timeout: {} }}", + self.upgrade_version, self.upgrade_sequence, self.upgrade_ordering, self.upgrade_timeout, + ) + } +} + +impl From for UpgradeAttributes { + fn from(ev: UpgradeTimeout) -> Self { + Self { + port_id: ev.port_id, + channel_id: ev.channel_id, + counterparty_port_id: ev.counterparty_port_id, + counterparty_channel_id: ev.counterparty_channel_id, + upgrade_connection_hops: ev.upgrade_connection_hops, + upgrade_version: ev.upgrade_version, + upgrade_sequence: ev.upgrade_sequence, + upgrade_ordering: ev.upgrade_ordering, + upgrade_timeout: Some(ev.upgrade_timeout), + } + } +} + +impl From for abci::Event { + fn from(value: UpgradeTimeout) -> Self { + let kind = UpgradeTimeout::event_type().as_str().to_owned(); + Self { + kind, + attributes: UpgradeAttributes::from(value).into(), + } + } +} + +impl UpgradeTimeout { + pub fn channel_id(&self) -> &ChannelId { + &self.channel_id + } + + pub fn port_id(&self) -> &PortId { + &self.port_id + } + + pub fn counterparty_port_id(&self) -> &PortId { + &self.counterparty_port_id + } + + pub fn counterparty_channel_id(&self) -> Option<&ChannelId> { + self.counterparty_channel_id.as_ref() + } +} + +impl TryFrom for UpgradeTimeout { + type Error = EventError; + + fn try_from(attrs: UpgradeAttributes) -> Result { + Ok(Self { + port_id: attrs.port_id, + channel_id: attrs.channel_id, + counterparty_port_id: attrs.counterparty_port_id, + counterparty_channel_id: attrs.counterparty_channel_id, + upgrade_connection_hops: attrs.upgrade_connection_hops, + upgrade_version: attrs.upgrade_version, + upgrade_sequence: attrs.upgrade_sequence, + upgrade_ordering: attrs.upgrade_ordering, + upgrade_timeout: attrs.upgrade_timeout.map_or_else( + || Timeout::Timestamp(Timestamp::default()), + |timeout| timeout, + ), + }) + } +} + +impl From for IbcEvent { + fn from(v: UpgradeTimeout) -> Self { + IbcEvent::UpgradeTimeoutChannel(v) + } +} + +impl EventType for UpgradeTimeout { + fn event_type() -> IbcEventType { + IbcEventType::UpgradeTimeoutChannel + } +} + macro_rules! impl_try_from_attribute_for_event { ($($event:ty),+) => { $(impl TryFrom for $event { diff --git a/crates/relayer-types/src/core/ics04_channel/msgs.rs b/crates/relayer-types/src/core/ics04_channel/msgs.rs index 6a79031c28..c856d76866 100644 --- a/crates/relayer-types/src/core/ics04_channel/msgs.rs +++ b/crates/relayer-types/src/core/ics04_channel/msgs.rs @@ -28,6 +28,7 @@ pub mod chan_upgrade_cancel; pub mod chan_upgrade_confirm; pub mod chan_upgrade_init; pub mod chan_upgrade_open; +pub mod chan_upgrade_timeout; pub mod chan_upgrade_try; // Packet specific messages. diff --git a/crates/relayer-types/src/core/ics04_channel/msgs/chan_upgrade_timeout.rs b/crates/relayer-types/src/core/ics04_channel/msgs/chan_upgrade_timeout.rs new file mode 100644 index 0000000000..e6bbe74fd9 --- /dev/null +++ b/crates/relayer-types/src/core/ics04_channel/msgs/chan_upgrade_timeout.rs @@ -0,0 +1,258 @@ +use ibc_proto::ibc::core::channel::v1::MsgChannelUpgradeTimeout as RawMsgChannelUpgradeTimeout; +use ibc_proto::Protobuf; + +use crate::core::ics04_channel::channel::ChannelEnd; +use crate::core::ics04_channel::error::Error; +use crate::core::ics23_commitment::commitment::CommitmentProofBytes; +use crate::core::ics24_host::identifier::{ChannelId, PortId}; +use crate::signer::Signer; +use crate::tx_msg::Msg; +use crate::Height; + +pub const TYPE_URL: &str = "/ibc.core.channel.v1.MsgChannelUpgradeTimeout"; + +/// Message definition the `ChanUpgradeTimeout` datagram. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MsgChannelUpgradeTimeout { + pub port_id: PortId, + pub channel_id: ChannelId, + pub counterparty_channel: ChannelEnd, + /// The proof of the counterparty channel + pub proof_channel: CommitmentProofBytes, + /// The height at which the proofs were queried. + pub proof_height: Height, + pub signer: Signer, +} + +impl MsgChannelUpgradeTimeout { + #[allow(clippy::too_many_arguments)] + pub fn new( + port_id: PortId, + channel_id: ChannelId, + counterparty_channel: ChannelEnd, + proof_channel: CommitmentProofBytes, + proof_height: Height, + signer: Signer, + ) -> Self { + Self { + port_id, + channel_id, + counterparty_channel, + proof_channel, + proof_height, + signer, + } + } +} + +impl Msg for MsgChannelUpgradeTimeout { + type ValidationError = Error; + type Raw = RawMsgChannelUpgradeTimeout; + + fn route(&self) -> String { + crate::keys::ROUTER_KEY.to_string() + } + + fn type_url(&self) -> String { + TYPE_URL.to_string() + } +} + +impl Protobuf for MsgChannelUpgradeTimeout {} + +impl TryFrom for MsgChannelUpgradeTimeout { + type Error = Error; + + fn try_from(raw_msg: RawMsgChannelUpgradeTimeout) -> Result { + let raw_counterparty_channel = raw_msg + .counterparty_channel + .ok_or(Error::missing_channel())?; + let counterparty_channel = ChannelEnd::try_from(raw_counterparty_channel)?; + + let proof_height = raw_msg + .proof_height + .ok_or_else(Error::missing_proof_height)? + .try_into() + .map_err(|_| Error::invalid_proof_height())?; + + Ok(MsgChannelUpgradeTimeout { + port_id: raw_msg.port_id.parse().map_err(Error::identifier)?, + channel_id: raw_msg.channel_id.parse().map_err(Error::identifier)?, + counterparty_channel, + proof_channel: raw_msg + .proof_channel + .try_into() + .map_err(Error::invalid_proof)?, + proof_height, + signer: raw_msg.signer.parse().map_err(Error::signer)?, + }) + } +} + +impl From for RawMsgChannelUpgradeTimeout { + fn from(domain_msg: MsgChannelUpgradeTimeout) -> Self { + RawMsgChannelUpgradeTimeout { + port_id: domain_msg.port_id.to_string(), + channel_id: domain_msg.channel_id.to_string(), + counterparty_channel: Some(domain_msg.counterparty_channel.into()), + proof_channel: domain_msg.proof_channel.into(), + proof_height: Some(domain_msg.proof_height.into()), + signer: domain_msg.signer.to_string(), + } + } +} + +#[cfg(test)] +pub mod test_util { + use crate::core::ics04_channel::channel::test_util::get_dummy_raw_channel_end; + use ibc_proto::ibc::core::channel::v1::MsgChannelUpgradeTimeout as RawMsgChannelUpgradeTimeout; + use ibc_proto::ibc::core::client::v1::Height as RawHeight; + + use crate::core::ics24_host::identifier::{ChannelId, PortId}; + use crate::test_utils::{get_dummy_bech32_account, get_dummy_proof}; + + /// Returns a dummy `RawMsgChannelUpgradeCnacel`, for testing only! + pub fn get_dummy_raw_msg_chan_upgrade_timeout() -> RawMsgChannelUpgradeTimeout { + RawMsgChannelUpgradeTimeout { + port_id: PortId::default().to_string(), + channel_id: ChannelId::default().to_string(), + counterparty_channel: Some(get_dummy_raw_channel_end()), + proof_channel: get_dummy_proof(), + proof_height: Some(RawHeight { + revision_number: 1, + revision_height: 1, + }), + signer: get_dummy_bech32_account(), + } + } +} + +#[cfg(test)] +mod tests { + use test_log::test; + + use ibc_proto::ibc::core::channel::v1::MsgChannelUpgradeTimeout as RawMsgChannelUpgradeTimeout; + use ibc_proto::ibc::core::client::v1::Height; + + use crate::core::ics04_channel::msgs::chan_upgrade_timeout::test_util::get_dummy_raw_msg_chan_upgrade_timeout; + use crate::core::ics04_channel::msgs::chan_upgrade_timeout::MsgChannelUpgradeTimeout; + + #[test] + fn parse_channel_upgrade_try_msg() { + struct Test { + name: String, + raw: RawMsgChannelUpgradeTimeout, + want_pass: bool, + } + + let default_raw_msg = get_dummy_raw_msg_chan_upgrade_timeout(); + + let tests: Vec = vec![ + Test { + name: "Good parameters".to_string(), + raw: default_raw_msg.clone(), + want_pass: true, + }, + Test { + name: "Correct port ID".to_string(), + raw: RawMsgChannelUpgradeTimeout { + port_id: "p36".to_string(), + ..default_raw_msg.clone() + }, + want_pass: true, + }, + Test { + name: "Port too short".to_string(), + raw: RawMsgChannelUpgradeTimeout { + port_id: "p".to_string(), + ..default_raw_msg.clone() + }, + want_pass: false, + }, + Test { + name: "Port too long".to_string(), + raw: RawMsgChannelUpgradeTimeout { + port_id: "abcdefsdfasdfasdfasdfasdfasdfadsfasdgafsgadfasdfasdfasdfsdfasdfaghijklmnopqrstuabcdefsdfasdfasdfasdfasdfasdfadsfasdgafsgadfasdfasdfasdfsdfasdfaghijklmnopqrstu".to_string(), + ..default_raw_msg.clone() + }, + want_pass: false, + }, + Test { + name: "Correct channel ID".to_string(), + raw: RawMsgChannelUpgradeTimeout { + channel_id: "channel-2".to_string(), + ..default_raw_msg.clone() + }, + want_pass: true, + }, + Test { + name: "Channel name too short".to_string(), + raw: RawMsgChannelUpgradeTimeout { + channel_id: "c".to_string(), + ..default_raw_msg.clone() + }, + want_pass: false, + }, + Test { + name: "Channel name too long".to_string(), + raw: RawMsgChannelUpgradeTimeout { + channel_id: "channel-128391283791827398127398791283912837918273981273987912839".to_string(), + ..default_raw_msg.clone() + }, + want_pass: false, + }, + Test { + name: "Empty proof channel".to_string(), + raw: RawMsgChannelUpgradeTimeout { + proof_channel: vec![], + ..default_raw_msg.clone() + }, + want_pass: false, + }, + Test { + name: "Bad proof height, height = 0".to_string(), + raw: RawMsgChannelUpgradeTimeout { + proof_height: Some(Height { + revision_number: 0, + revision_height: 0, + }), + ..default_raw_msg.clone() + }, + want_pass: false, + }, + Test { + name: "Missing proof height".to_string(), + raw: RawMsgChannelUpgradeTimeout { + proof_height: None, + ..default_raw_msg.clone() + }, + want_pass: false, + }, + ] + .into_iter() + .collect(); + + for test in tests { + let res = MsgChannelUpgradeTimeout::try_from(test.raw.clone()); + + assert_eq!( + test.want_pass, + res.is_ok(), + "RawMsgChannelUpgradeTimeout::try_from failed for test {}, \nraw msg {:?} with err {:?}", + test.name, + test.raw, + res.err() + ); + } + } + + #[test] + fn to_and_from() { + let raw = get_dummy_raw_msg_chan_upgrade_timeout(); + let msg = MsgChannelUpgradeTimeout::try_from(raw.clone()).unwrap(); + let raw_back = RawMsgChannelUpgradeTimeout::from(msg.clone()); + let msg_back = MsgChannelUpgradeTimeout::try_from(raw_back.clone()).unwrap(); + assert_eq!(raw, raw_back); + assert_eq!(msg, msg_back); + } +} diff --git a/crates/relayer-types/src/core/ics04_channel/timeout.rs b/crates/relayer-types/src/core/ics04_channel/timeout.rs index 2d53a7c2e3..cee5f40601 100644 --- a/crates/relayer-types/src/core/ics04_channel/timeout.rs +++ b/crates/relayer-types/src/core/ics04_channel/timeout.rs @@ -1,14 +1,16 @@ use core::fmt::{Display, Error as FmtError, Formatter}; +use std::str::FromStr; +use flex_error::{define_error, TraceError}; use serde::{Deserialize, Serialize}; -use ibc_proto::ibc::core::channel::v1::Timeout as RawUpgradeTimeout; +use ibc_proto::ibc::core::channel::v1::Timeout as RawTimeout; use ibc_proto::ibc::core::client::v1::Height as RawHeight; use ibc_proto::Protobuf; use crate::core::ics02_client::{error::Error as ICS2Error, height::Height}; use crate::core::ics04_channel::error::Error as ChannelError; -use crate::timestamp::Timestamp; +use crate::timestamp::{ParseTimestampError, Timestamp}; /// Indicates a consensus height on the destination chain after which the packet /// will no longer be processed, and will instead count as having timed-out. @@ -188,8 +190,8 @@ impl<'de> Deserialize<'de> for TimeoutHeight { /// A composite of timeout height and timeout timestamp types, useful for when /// performing a channel upgrade handshake, as there are cases when only timeout /// height is set, only timeout timestamp is set, or both are set. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum UpgradeTimeout { +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum Timeout { /// Timeout height indicates the height at which the counterparty /// must no longer proceed with the upgrade handshake. /// The chains will then preserve their original channel and the upgrade handshake is aborted @@ -204,31 +206,73 @@ pub enum UpgradeTimeout { Both(Height, Timestamp), } -impl UpgradeTimeout { +impl Display for Timeout { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { + match self { + Self::Height(height) => write!(f, "{height}"), + Self::Timestamp(timestamp) => write!(f, "{timestamp}"), + Self::Both(height, timestamp) => write!(f, "{height}, {timestamp}"), + } + } +} + +impl Timeout { pub fn new(height: Option, timestamp: Option) -> Result { match (height, timestamp) { - (Some(height), None) => Ok(UpgradeTimeout::Height(height)), - (None, Some(timestamp)) => Ok(UpgradeTimeout::Timestamp(timestamp)), - (Some(height), Some(timestamp)) => Ok(UpgradeTimeout::Both(height, timestamp)), + (Some(height), None) => Ok(Timeout::Height(height)), + (None, Some(timestamp)) => Ok(Timeout::Timestamp(timestamp)), + (Some(height), Some(timestamp)) => Ok(Timeout::Both(height, timestamp)), (None, None) => Err(ChannelError::missing_upgrade_timeout()), } } pub fn into_tuple(self) -> (Option, Option) { match self { - UpgradeTimeout::Height(height) => (Some(height), None), - UpgradeTimeout::Timestamp(timestamp) => (None, Some(timestamp)), - UpgradeTimeout::Both(height, timestamp) => (Some(height), Some(timestamp)), + Timeout::Height(height) => (Some(height), None), + Timeout::Timestamp(timestamp) => (None, Some(timestamp)), + Timeout::Both(height, timestamp) => (Some(height), Some(timestamp)), + } + } +} + +define_error! { + #[derive(Debug, PartialEq, Eq)] + TimeoutError { + InvalidTimestamp + { timestamp: String } + [ TraceError ] + |e| { format_args!("cannot convert into a `Timestamp` type from string {0}", e.timestamp) }, + + InvalidTimeout + { timeout: String } + |e| { format_args!("invalid timeout {0}", e.timeout) }, + } +} + +impl FromStr for Timeout { + type Err = TimeoutError; + + fn from_str(value: &str) -> Result { + let split: Vec<&str> = value.split(' ').collect(); + + if split.len() != 2 { + return Err(TimeoutError::invalid_timeout(value.to_owned())); } + + // only timeout timestamp are supported at the moment + split[1] + .parse::() + .map(Timeout::Timestamp) + .map_err(|e| TimeoutError::invalid_timestamp(value.to_owned(), e)) } } -impl Protobuf for UpgradeTimeout {} +impl Protobuf for Timeout {} -impl TryFrom for UpgradeTimeout { +impl TryFrom for Timeout { type Error = ChannelError; - fn try_from(value: RawUpgradeTimeout) -> Result { + fn try_from(value: RawTimeout) -> Result { let raw_timeout_height = value.height.map(Height::try_from).transpose(); let raw_timeout_timestamp = Timestamp::from_nanoseconds(value.timestamp) @@ -252,18 +296,18 @@ impl TryFrom for UpgradeTimeout { } } -impl From for RawUpgradeTimeout { - fn from(value: UpgradeTimeout) -> Self { +impl From for RawTimeout { + fn from(value: Timeout) -> Self { match value { - UpgradeTimeout::Height(height) => Self { + Timeout::Height(height) => Self { height: Some(RawHeight::from(height)), timestamp: 0, }, - UpgradeTimeout::Timestamp(timestamp) => Self { + Timeout::Timestamp(timestamp) => Self { height: None, timestamp: timestamp.nanoseconds(), }, - UpgradeTimeout::Both(height, timestamp) => Self { + Timeout::Both(height, timestamp) => Self { height: Some(RawHeight::from(height)), timestamp: timestamp.nanoseconds(), }, @@ -273,13 +317,13 @@ impl From for RawUpgradeTimeout { #[cfg(test)] pub mod test_util { - use ibc_proto::ibc::core::channel::v1::Timeout as RawUpgradeTimeout; + use ibc_proto::ibc::core::channel::v1::Timeout as RawTimeout; use ibc_proto::ibc::core::client::v1::Height as RawHeight; use crate::core::ics02_client::height::Height; - pub fn get_dummy_upgrade_timeout() -> RawUpgradeTimeout { - RawUpgradeTimeout { + pub fn get_dummy_upgrade_timeout() -> RawTimeout { + RawTimeout { height: Some(RawHeight::from(Height::new(1, 50).unwrap())), timestamp: 0, } diff --git a/crates/relayer-types/src/core/ics04_channel/upgrade.rs b/crates/relayer-types/src/core/ics04_channel/upgrade.rs index fff2f15dcd..a909859cf5 100644 --- a/crates/relayer-types/src/core/ics04_channel/upgrade.rs +++ b/crates/relayer-types/src/core/ics04_channel/upgrade.rs @@ -4,14 +4,14 @@ use ibc_proto::Protobuf; use crate::core::ics04_channel::error::Error as ChannelError; use crate::core::ics04_channel::packet::Sequence; -use crate::core::ics04_channel::timeout::UpgradeTimeout; +use crate::core::ics04_channel::timeout::Timeout; use crate::core::ics04_channel::upgrade_fields::UpgradeFields; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Upgrade { pub fields: UpgradeFields, // timeout can be zero, see `TryFrom` implementation - pub timeout: Option, + pub timeout: Option, pub next_sequence_send: Sequence, } @@ -27,8 +27,8 @@ impl TryFrom for Upgrade { .try_into()?; let timeout = value .timeout - .filter(|tm| UpgradeTimeout::try_from(tm.clone()).is_ok()) - .map(|tm| UpgradeTimeout::try_from(tm).unwrap()); + .filter(|tm| Timeout::try_from(tm.clone()).is_ok()) + .map(|tm| Timeout::try_from(tm).unwrap()); let next_sequence_send = value.next_sequence_send.into(); Ok(Self { diff --git a/crates/relayer-types/src/events.rs b/crates/relayer-types/src/events.rs index c3db57d264..67731938b4 100644 --- a/crates/relayer-types/src/events.rs +++ b/crates/relayer-types/src/events.rs @@ -135,6 +135,7 @@ const CHANNEL_UPGRADE_ACK_EVENT: &str = "channel_upgrade_ack"; const CHANNEL_UPGRADE_CONFIRM_EVENT: &str = "channel_upgrade_confirm"; const CHANNEL_UPGRADE_OPEN_EVENT: &str = "channel_upgrade_open"; const CHANNEL_UPGRADE_CANCEL_EVENT: &str = "channel_upgrade_cancelled"; +const CHANNEL_UPGRADE_TIMEOUT_EVENT: &str = "channel_upgrade_timeout"; /// Packet event types const SEND_PACKET_EVENT: &str = "send_packet"; const RECEIVE_PACKET_EVENT: &str = "receive_packet"; @@ -172,6 +173,7 @@ pub enum IbcEventType { UpgradeConfirmChannel, UpgradeOpenChannel, UpgradeCancelChannel, + UpgradeTimeoutChannel, SendPacket, ReceivePacket, WriteAck, @@ -210,6 +212,7 @@ impl IbcEventType { IbcEventType::UpgradeConfirmChannel => CHANNEL_UPGRADE_CONFIRM_EVENT, IbcEventType::UpgradeOpenChannel => CHANNEL_UPGRADE_OPEN_EVENT, IbcEventType::UpgradeCancelChannel => CHANNEL_UPGRADE_CANCEL_EVENT, + IbcEventType::UpgradeTimeoutChannel => CHANNEL_UPGRADE_TIMEOUT_EVENT, IbcEventType::SendPacket => SEND_PACKET_EVENT, IbcEventType::ReceivePacket => RECEIVE_PACKET_EVENT, IbcEventType::WriteAck => WRITE_ACK_EVENT, @@ -252,6 +255,7 @@ impl FromStr for IbcEventType { CHANNEL_UPGRADE_CONFIRM_EVENT => Ok(IbcEventType::UpgradeConfirmChannel), CHANNEL_UPGRADE_OPEN_EVENT => Ok(IbcEventType::UpgradeOpenChannel), CHANNEL_UPGRADE_CANCEL_EVENT => Ok(IbcEventType::UpgradeCancelChannel), + CHANNEL_UPGRADE_TIMEOUT_EVENT => Ok(IbcEventType::UpgradeTimeoutChannel), SEND_PACKET_EVENT => Ok(IbcEventType::SendPacket), RECEIVE_PACKET_EVENT => Ok(IbcEventType::ReceivePacket), WRITE_ACK_EVENT => Ok(IbcEventType::WriteAck), @@ -296,6 +300,7 @@ pub enum IbcEvent { UpgradeConfirmChannel(ChannelEvents::UpgradeConfirm), UpgradeOpenChannel(ChannelEvents::UpgradeOpen), UpgradeCancelChannel(ChannelEvents::UpgradeCancel), + UpgradeTimeoutChannel(ChannelEvents::UpgradeTimeout), SendPacket(ChannelEvents::SendPacket), ReceivePacket(ChannelEvents::ReceivePacket), @@ -341,6 +346,7 @@ impl Display for IbcEvent { IbcEvent::UpgradeConfirmChannel(ev) => write!(f, "UpgradeConfirmChannel({ev})"), IbcEvent::UpgradeOpenChannel(ev) => write!(f, "UpgradeOpenChannel({ev})"), IbcEvent::UpgradeCancelChannel(ev) => write!(f, "UpgradeCancelChannel({ev})"), + IbcEvent::UpgradeTimeoutChannel(ev) => write!(f, "UpgradeTimeoutChannel({ev})"), IbcEvent::SendPacket(ev) => write!(f, "SendPacket({ev})"), IbcEvent::ReceivePacket(ev) => write!(f, "ReceivePacket({ev})"), @@ -386,6 +392,7 @@ impl TryFrom for abci::Event { IbcEvent::UpgradeConfirmChannel(event) => event.into(), IbcEvent::UpgradeOpenChannel(event) => event.into(), IbcEvent::UpgradeCancelChannel(event) => event.into(), + IbcEvent::UpgradeTimeoutChannel(event) => event.into(), IbcEvent::SendPacket(event) => event.try_into().map_err(Error::channel)?, IbcEvent::ReceivePacket(event) => event.try_into().map_err(Error::channel)?, IbcEvent::WriteAcknowledgement(event) => event.try_into().map_err(Error::channel)?, @@ -434,6 +441,7 @@ impl IbcEvent { IbcEvent::UpgradeConfirmChannel(_) => IbcEventType::UpgradeConfirmChannel, IbcEvent::UpgradeOpenChannel(_) => IbcEventType::UpgradeOpenChannel, IbcEvent::UpgradeCancelChannel(_) => IbcEventType::UpgradeCancelChannel, + IbcEvent::UpgradeTimeoutChannel(_) => IbcEventType::UpgradeTimeoutChannel, IbcEvent::SendPacket(_) => IbcEventType::SendPacket, IbcEvent::ReceivePacket(_) => IbcEventType::ReceivePacket, IbcEvent::WriteAcknowledgement(_) => IbcEventType::WriteAck, @@ -466,6 +474,7 @@ impl IbcEvent { IbcEvent::UpgradeConfirmChannel(ev) => Some(ev.into()), IbcEvent::UpgradeOpenChannel(ev) => Some(ev.into()), IbcEvent::UpgradeCancelChannel(ev) => Some(ev.into()), + IbcEvent::UpgradeTimeoutChannel(ev) => Some(ev.into()), _ => None, } } diff --git a/crates/relayer/src/chain/cosmos/types/events/channel.rs b/crates/relayer/src/chain/cosmos/types/events/channel.rs index 969ec7be74..3151b269f5 100644 --- a/crates/relayer/src/chain/cosmos/types/events/channel.rs +++ b/crates/relayer/src/chain/cosmos/types/events/channel.rs @@ -75,6 +75,8 @@ fn extract_upgrade_attributes( upgrade_ordering: extract_attribute(object, &format!("{namespace}.upgrade_ordering"))? .parse() .map_err(|_| EventError::missing_action_string())?, + upgrade_timeout: maybe_extract_attribute(object, &format!("{namespace}.upgrade_timeout")) + .and_then(|v| v.parse().ok()), }) } diff --git a/crates/relayer/src/channel.rs b/crates/relayer/src/channel.rs index 9d78b19a7b..2611150566 100644 --- a/crates/relayer/src/channel.rs +++ b/crates/relayer/src/channel.rs @@ -4,6 +4,7 @@ use ibc_relayer_types::core::ics04_channel::msgs::chan_upgrade_ack::MsgChannelUp use ibc_relayer_types::core::ics04_channel::msgs::chan_upgrade_cancel::MsgChannelUpgradeCancel; use ibc_relayer_types::core::ics04_channel::msgs::chan_upgrade_confirm::MsgChannelUpgradeConfirm; use ibc_relayer_types::core::ics04_channel::msgs::chan_upgrade_open::MsgChannelUpgradeOpen; +use ibc_relayer_types::core::ics04_channel::msgs::chan_upgrade_timeout::MsgChannelUpgradeTimeout; use ibc_relayer_types::core::ics04_channel::packet::Sequence; use ibc_relayer_types::core::ics04_channel::upgrade_fields::UpgradeFields; @@ -300,14 +301,14 @@ impl Channel { chain: ChainA, counterparty_chain: ChainB, channel: WorkerChannelObject, - height: Height, + height: QueryHeight, ) -> Result<(Channel, State), ChannelError> { let (a_channel, _) = chain .query_channel( QueryChannelRequest { port_id: channel.src_port_id.clone(), channel_id: channel.src_channel_id.clone(), - height: QueryHeight::Specific(height), + height, }, // IncludeProof::Yes forces a new query when the CachingChainHandle // is used. @@ -393,7 +394,7 @@ impl Channel { QueryChannelRequest { port_id: a_channel.remote.port_id.clone(), channel_id: a_channel.remote.channel_id.clone().unwrap(), - height: QueryHeight::Specific(height), + height, }, // IncludeProof::Yes forces a new query when the CachingChainHandle // is used. @@ -824,11 +825,12 @@ impl Channel { None => Some(self.build_chan_upgrade_cancel_and_send()?), } } - (State::Flushing, State::Open(UpgradeState::Upgrading)) => { - match self.build_chan_upgrade_ack_and_send()? { - Some(event) => Some(event), - None => Some(self.flipped().build_chan_upgrade_cancel_and_send()?), - } + (State::Flushing, State::Flushing) => match self.build_chan_upgrade_ack_and_send()? { + Some(event) => Some(event), + None => Some(self.flipped().build_chan_upgrade_cancel_and_send()?), + }, + (State::Flushcomplete, State::Flushcomplete) => { + Some(self.build_chan_upgrade_open_and_send()?) } (State::Flushcomplete, State::Flushing) => { match self.build_chan_upgrade_confirm_and_send()? { @@ -836,12 +838,98 @@ impl Channel { None => Some(self.flipped().build_chan_upgrade_cancel_and_send()?), } } - (State::Flushcomplete, State::Open(UpgradeState::Upgrading)) => { - Some(self.flipped().build_chan_upgrade_open_and_send()?) + (State::Flushing, State::Open(UpgradeState::Upgrading)) => { + // Verify if an ErrorReceipt for the Upgrade exists on the Chain which + // has the channel end Open. If it is the case an UpgradeCancel needs to + // be relayed to the Chain with the channel end Flushing. + // Else relay the UpgradeAck. + let dst_latest_height = self + .dst_chain() + .query_latest_height() + .map_err(|e| ChannelError::chain_query(self.dst_chain().id(), e))?; + let src_latest_height = self + .src_chain() + .query_latest_height() + .map_err(|e| ChannelError::chain_query(self.src_chain().id(), e))?; + let (error_receipt, _) = self + .dst_chain() + .query_upgrade_error( + QueryUpgradeErrorRequest { + port_id: self.dst_port_id().to_string(), + channel_id: self.dst_channel_id().unwrap().to_string(), + }, + dst_latest_height, + ) + .map_err(|e| ChannelError::chain_query(self.src_chain().id(), e))?; + + let (channel_end, _) = self + .src_chain() + .query_channel( + QueryChannelRequest { + port_id: self.src_port_id().clone(), + channel_id: self.src_channel_id().unwrap().clone(), + height: QueryHeight::Specific(src_latest_height), + }, + IncludeProof::Yes, + ) + .map_err(|e| ChannelError::query(self.src_chain().id(), e))?; + + if error_receipt.sequence == channel_end.upgrade_sequence { + Some(self.flipped().build_chan_upgrade_cancel_and_send()?) + } else { + match self.build_chan_upgrade_ack_and_send()? { + Some(event) => Some(event), + None => Some(self.flipped().build_chan_upgrade_cancel_and_send()?), + } + } + } + (State::Open(UpgradeState::NotUpgrading), State::Flushing) => { + let dst_latest_height = self + .dst_chain() + .query_latest_height() + .map_err(|e| ChannelError::chain_query(self.dst_chain().id(), e))?; + let src_latest_height = self + .src_chain() + .query_latest_height() + .map_err(|e| ChannelError::chain_query(self.src_chain().id(), e))?; + let (error_receipt, _) = self + .src_chain() + .query_upgrade_error( + QueryUpgradeErrorRequest { + port_id: self.src_port_id().to_string(), + channel_id: self.src_channel_id().unwrap().to_string(), + }, + src_latest_height, + ) + .map_err(|e| ChannelError::chain_query(self.src_chain().id(), e))?; + + let (channel_end, _) = self + .dst_chain() + .query_channel( + QueryChannelRequest { + port_id: self.dst_port_id().clone(), + channel_id: self.dst_channel_id().unwrap().clone(), + height: QueryHeight::Specific(dst_latest_height), + }, + IncludeProof::Yes, + ) + .map_err(|e| ChannelError::query(self.dst_chain().id(), e))?; + + if error_receipt.sequence == channel_end.upgrade_sequence { + Some(self.build_chan_upgrade_cancel_and_send()?) + } else { + match self.flipped().build_chan_upgrade_ack_and_send()? { + Some(event) => Some(event), + None => Some(self.build_chan_upgrade_cancel_and_send()?), + } + } } (State::Open(UpgradeState::Upgrading), State::Flushcomplete) => { Some(self.build_chan_upgrade_open_and_send()?) } + (State::Open(UpgradeState::NotUpgrading), State::Flushcomplete) => { + Some(self.build_chan_upgrade_open_and_send()?) + } _ => None, }; @@ -2227,6 +2315,102 @@ impl Channel { } } + pub fn build_chan_upgrade_timeout(&self) -> Result, ChannelError> { + // Destination channel ID must exist + let src_channel_id = self + .src_channel_id() + .ok_or_else(ChannelError::missing_counterparty_channel_id)?; + + let dst_channel_id = self + .dst_channel_id() + .ok_or_else(ChannelError::missing_counterparty_channel_id)?; + + let src_port_id = self.src_port_id(); + + let dst_port_id = self.dst_port_id(); + + let src_latest_height = self + .src_chain() + .query_latest_height() + .map_err(|e| ChannelError::chain_query(self.src_chain().id(), e))?; + + // Retrieve counterparty channel + let (counterparty_channel, _) = self + .src_chain() + .query_channel( + QueryChannelRequest { + port_id: src_port_id.clone(), + channel_id: src_channel_id.clone(), + height: QueryHeight::Specific(src_latest_height), + }, + IncludeProof::Yes, + ) + .map_err(|e| ChannelError::query(self.src_chain().id(), e))?; + + // Building the channel proof at the queried height + let proofs = self + .src_chain() + .build_channel_proofs( + &src_port_id.clone(), + &src_channel_id.clone(), + src_latest_height, + ) + .map_err(ChannelError::channel_proof)?; + + // Build message(s) to update client on destination + let mut msgs = self.build_update_client_on_dst(proofs.height())?; + + 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 = MsgChannelUpgradeTimeout { + port_id: dst_port_id.clone(), + channel_id: dst_channel_id.clone(), + counterparty_channel, + proof_channel: proofs.object_proof().clone(), + proof_height: proofs.height(), + signer, + }; + + msgs.push(new_msg.to_any()); + Ok(msgs) + } + + pub fn build_chan_upgrade_timeout_and_send(&self) -> Result { + let dst_msgs = self.build_chan_upgrade_timeout()?; + + let tm = TrackedMsgs::new_static(dst_msgs, "ChannelUpgradeTimeout"); + + let events = self + .dst_chain() + .send_messages_and_wait_commit(tm) + .map_err(|e| ChannelError::submit(self.dst_chain().id(), e))?; + + let result = events + .into_iter() + .find(|event_with_height| { + matches!(event_with_height.event, IbcEvent::UpgradeTimeoutChannel(_)) + || matches!(event_with_height.event, IbcEvent::ChainError(_)) + }) + .ok_or_else(|| { + ChannelError::missing_event( + "no channel upgrade timeout event was in the response".to_string(), + ) + })?; + + match &result.event { + IbcEvent::UpgradeTimeoutChannel(_) => { + info!("👋 {} => {}", self.dst_chain().id(), result); + Ok(result.event) + } + IbcEvent::ChainError(e) => Err(ChannelError::tx_response(e.clone())), + _ => Err(ChannelError::invalid_event(result.event)), + } + } + pub fn map_chain( self, mapper_a: impl Fn(ChainA) -> ChainC, diff --git a/crates/relayer/src/event.rs b/crates/relayer/src/event.rs index 8697e25f78..cda6519e5e 100644 --- a/crates/relayer/src/event.rs +++ b/crates/relayer/src/event.rs @@ -136,6 +136,10 @@ pub fn ibc_event_try_from_abci_event(abci_event: &AbciEvent) -> Result Ok(IbcEvent::UpgradeTimeoutChannel( + channel_upgrade_timeout_try_from_abci_event(abci_event) + .map_err(IbcEventError::channel)?, + )), Ok(IbcEventType::SendPacket) => Ok(IbcEvent::SendPacket( send_packet_try_from_abci_event(abci_event).map_err(IbcEventError::channel)?, )), @@ -337,6 +341,16 @@ pub fn channel_upgrade_cancelled_try_from_abci_event( } } +pub fn channel_upgrade_timeout_try_from_abci_event( + abci_event: &AbciEvent, +) -> Result { + match channel_upgrade_extract_attributes_from_tx(abci_event) { + Ok(attrs) => channel_events::UpgradeTimeout::try_from(attrs) + .map_err(|_| ChannelError::implementation_specific()), + Err(e) => Err(e), + } +} + pub fn send_packet_try_from_abci_event( abci_event: &AbciEvent, ) -> Result { diff --git a/crates/relayer/src/supervisor/spawn.rs b/crates/relayer/src/supervisor/spawn.rs index 788a38f12f..8706d46503 100644 --- a/crates/relayer/src/supervisor/spawn.rs +++ b/crates/relayer/src/supervisor/spawn.rs @@ -252,7 +252,6 @@ impl<'a, Chain: ChainHandle> SpawnContext<'a, Chain> { if (mode.clients.enabled || mode.packets.enabled) && chan_state_src.is_open() && (chan_state_dst.is_open() || chan_state_dst.is_closed()) - && !is_channel_upgrading { if mode.clients.enabled { // Spawn the client worker diff --git a/crates/relayer/src/worker/channel.rs b/crates/relayer/src/worker/channel.rs index a3018360b9..024592ad37 100644 --- a/crates/relayer/src/worker/channel.rs +++ b/crates/relayer/src/worker/channel.rs @@ -3,6 +3,7 @@ use crossbeam_channel::Receiver; use ibc_relayer_types::events::IbcEventType; use tracing::{debug, error_span}; +use crate::chain::requests::QueryHeight; use crate::channel::{channel_handshake_retry, Channel as RelayChannel}; use crate::util::retry::RetryResult; use crate::util::task::{spawn_background_task, Next, TaskError, TaskHandle}; @@ -61,7 +62,7 @@ pub fn spawn_channel_worker( chains.a.clone(), chains.b.clone(), channel.clone(), - event_with_height.height, + QueryHeight::Latest, ) { Ok((mut handshake_channel, _)) => handshake_channel .step_event(&event_with_height.event, index), @@ -94,10 +95,6 @@ pub fn spawn_channel_worker( } if complete_handshake_on_new_block => { debug!("starts processing block event at {:#?}", current_height); - let height = current_height - .decrement() - .map_err(|e| TaskError::Fatal(RunError::ics02(e)))?; - complete_handshake_on_new_block = false; retry_with_index( channel_handshake_retry::default_strategy(max_block_times), @@ -105,7 +102,7 @@ pub fn spawn_channel_worker( chains.a.clone(), chains.b.clone(), channel.clone(), - height, + QueryHeight::Latest, ) { Ok((mut handshake_channel, state)) => { handshake_channel.step_state(state, index) diff --git a/flake.lock b/flake.lock index e8ffbf3cd4..4fb2b8aa56 100644 --- a/flake.lock +++ b/flake.lock @@ -1041,11 +1041,11 @@ }, "nixpkgs_5": { "locked": { - "lastModified": 1705242415, - "narHash": "sha256-a8DRYrNrzTudvO7XHUPNJD89Wbf1ZZT0VbwCsPnHWaE=", + "lastModified": 1705303754, + "narHash": "sha256-loWkd7lUzSvGBU9xnva37iPB2rr5ulq1qBLT44KjzGA=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ea780f3de2d169f982564128804841500e85e373", + "rev": "e0629618b4b419a47e2c8a3cab223e2a7f3a8f97", "type": "github" }, "original": { diff --git a/guide/src/templates/commands/hermes/tx/chan-upgrade-timeout_1.md b/guide/src/templates/commands/hermes/tx/chan-upgrade-timeout_1.md new file mode 100644 index 0000000000..2ccf8b5d99 --- /dev/null +++ b/guide/src/templates/commands/hermes/tx/chan-upgrade-timeout_1.md @@ -0,0 +1 @@ +[[#BINARY hermes]][[#GLOBALOPTIONS]] tx chan-upgrade-timeout --dst-chain [[#DST_CHAIN_ID]] --src-chain [[#SRC_CHAIN_ID]] --dst-connection [[#DST_CONNECTION_ID]] --dst-port [[#DST_PORT_ID]] --src-port [[#SRC_PORT_ID]] --src-channel [[#SRC_CHANNEL_ID]] --dst-channel [[#DST_CHANNEL_ID]] diff --git a/guide/src/templates/help_templates/tx.md b/guide/src/templates/help_templates/tx.md index 8fc53c2405..9d83c50de5 100644 --- a/guide/src/templates/help_templates/tx.md +++ b/guide/src/templates/help_templates/tx.md @@ -18,6 +18,7 @@ SUBCOMMANDS: chan-upgrade-cancel Relay the channel upgrade cancellation (ChannelUpgradeCancel) chan-upgrade-confirm Relay the channel upgrade attempt (ChannelUpgradeConfirm) chan-upgrade-open Relay the channel upgrade attempt (ChannelUpgradeOpen) + chan-upgrade-timeout Relay the channel upgrade timeout (ChannelUpgradeTimeout) chan-upgrade-try Relay the channel upgrade attempt (ChannelUpgradeTry) conn-ack Relay acknowledgment of a connection attempt (ConnectionOpenAck) conn-confirm Confirm opening of a connection (ConnectionOpenConfirm) diff --git a/guide/src/templates/help_templates/tx/chan-upgrade-timeout.md b/guide/src/templates/help_templates/tx/chan-upgrade-timeout.md new file mode 100644 index 0000000000..3d783f1a3a --- /dev/null +++ b/guide/src/templates/help_templates/tx/chan-upgrade-timeout.md @@ -0,0 +1,30 @@ +DESCRIPTION: +Relay the channel upgrade timeout (ChannelUpgradeTimeout) + +USAGE: + hermes tx chan-upgrade-timeout --dst-chain --src-chain --dst-connection --dst-port --src-port --src-channel --dst-channel + +OPTIONS: + -h, --help Print help information + +REQUIRED: + --dst-chain + Identifier of the destination chain + + --dst-channel + Identifier of the destination channel (optional) [aliases: dst-chan] + + --dst-connection + Identifier of the destination connection [aliases: dst-conn] + + --dst-port + Identifier of the destination port + + --src-chain + Identifier of the source chain + + --src-channel + Identifier of the source channel (required) [aliases: src-chan] + + --src-port + Identifier of the source port diff --git a/tools/integration-test/src/tests/channel_upgrade/upgrade_handshake.rs b/tools/integration-test/src/tests/channel_upgrade/upgrade_handshake.rs index c2871b528f..0b297ac533 100644 --- a/tools/integration-test/src/tests/channel_upgrade/upgrade_handshake.rs +++ b/tools/integration-test/src/tests/channel_upgrade/upgrade_handshake.rs @@ -8,12 +8,14 @@ use std::thread::sleep; use ibc_relayer::chain::requests::{IncludeProof, QueryChannelRequest, QueryHeight}; use ibc_relayer_types::core::ics04_channel::packet::Sequence; use ibc_relayer_types::core::ics04_channel::version::Version; +use ibc_relayer_types::events::IbcEventType; use ibc_test_framework::chain::config::{set_max_deposit_period, set_voting_period}; use ibc_test_framework::prelude::*; use ibc_test_framework::relayer::channel::{ assert_eventually_channel_established, assert_eventually_channel_upgrade_ack, - assert_eventually_channel_upgrade_cancel, assert_eventually_channel_upgrade_open, - assert_eventually_channel_upgrade_try, ChannelUpgradableAttributes, + assert_eventually_channel_upgrade_cancel, assert_eventually_channel_upgrade_flushing, + assert_eventually_channel_upgrade_open, assert_eventually_channel_upgrade_try, + ChannelUpgradableAttributes, }; #[test] @@ -36,6 +38,11 @@ fn test_channel_upgrade_timeout_confirm_handshake() -> Result<(), Error> { run_binary_channel_test(&ChannelUpgradeTimeoutConfirmHandshake) } +#[test] +fn test_channel_upgrade_timeout_when_flushing_handshake() -> Result<(), Error> { + run_binary_channel_test(&ChannelUpgradeHandshakeTimeoutWhenFlushingHandshake) +} + const MAX_DEPOSIT_PERIOD: &str = "10s"; const VOTING_PERIOD: u64 = 10; @@ -530,3 +537,187 @@ impl BinaryChannelTest for ChannelUpgradeTimeoutConfirmHandshake { }) } } + +struct ChannelUpgradeHandshakeTimeoutWhenFlushingHandshake; + +impl TestOverrides for ChannelUpgradeHandshakeTimeoutWhenFlushingHandshake { + fn modify_relayer_config(&self, config: &mut Config) { + config.mode.channels.enabled = true; + + config.mode.clients.misbehaviour = false; + } + + fn modify_genesis_file(&self, genesis: &mut serde_json::Value) -> Result<(), Error> { + set_max_deposit_period(genesis, MAX_DEPOSIT_PERIOD)?; + set_voting_period(genesis, VOTING_PERIOD)?; + Ok(()) + } + + fn should_spawn_supervisor(&self) -> bool { + false + } +} + +impl BinaryChannelTest for ChannelUpgradeHandshakeTimeoutWhenFlushingHandshake { + fn run( + &self, + _config: &TestConfig, + relayer: RelayerDriver, + chains: ConnectedChains, + channels: ConnectedChannel, + ) -> Result<(), Error> { + info!("Check that channels are both in OPEN State"); + + assert_eventually_channel_established( + &chains.handle_b, + &chains.handle_a, + &channels.channel_id_b.as_ref(), + &channels.port_b.as_ref(), + )?; + + let channel_end_a = chains + .handle_a + .query_channel( + QueryChannelRequest { + port_id: channels.port_a.0.clone(), + channel_id: channels.channel_id_a.0.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map(|(channel_end, _)| channel_end) + .map_err(|e| eyre!("Error querying ChannelEnd A: {e}"))?; + + let channel_end_b = chains + .handle_b + .query_channel( + QueryChannelRequest { + port_id: channels.port_b.0.clone(), + channel_id: channels.channel_id_b.0.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map(|(channel_end, _)| channel_end) + .map_err(|e| eyre!("Error querying ChannelEnd B: {e}"))?; + + let old_version = channel_end_a.version; + let old_ordering = channel_end_a.ordering; + let old_connection_hops_a = channel_end_a.connection_hops; + let old_connection_hops_b = channel_end_b.connection_hops; + + let channel = channels.channel; + let new_version = Version::ics20_with_fee(); + + let old_attrs = ChannelUpgradableAttributes::new( + old_version.clone(), + old_version.clone(), + old_ordering, + old_connection_hops_a.clone(), + old_connection_hops_b.clone(), + Sequence::from(1), + ); + + info!("Will update channel params to set a shorter upgrade timeout..."); + + // the upgrade timeout should be long enough for chain a + // to complete Ack successfully so that it goes into `FLUSHING` + chains.node_b.chain_driver().update_channel_params( + 25000000000, + chains.handle_b().get_signer().unwrap().as_ref(), + "1", + )?; + + info!("Will initialise upgrade handshake with governance proposal..."); + + chains.node_a.chain_driver().initialise_channel_upgrade( + channel.src_port_id().as_str(), + channel.src_channel_id().unwrap().as_str(), + old_ordering.as_str(), + old_connection_hops_a.first().unwrap().as_str(), + &serde_json::to_string(&new_version.0).unwrap(), + chains.handle_a().get_signer().unwrap().as_ref(), + "1", + )?; + + info!("Will run ChanUpgradeTry step..."); + + channel.build_chan_upgrade_try_and_send()?; + + info!("Check that the step ChanUpgradeTry was correctly executed..."); + + assert_eventually_channel_upgrade_try( + &chains.handle_b, + &chains.handle_a, + &channels.channel_id_b.as_ref(), + &channels.port_b.as_ref(), + &old_attrs.flipped(), + )?; + + // send a IBC transfer message from chain a to chain b + // so that we have an in-flight packet and chain a + // will move to `FLUSHING` during Ack + let denom_a = chains.node_a.denom(); + let wallet_a = chains.node_a.wallets().user1().cloned(); + let wallet_b = chains.node_b.wallets().user1().cloned(); + let a_to_b_amount = 12345u64; + + info!( + "Sending IBC transfer from chain {} to chain {} with amount of {} {}", + chains.chain_id_a(), + chains.chain_id_b(), + a_to_b_amount, + denom_a + ); + + chains.node_a.chain_driver().ibc_transfer_token( + &channels.port_a.as_ref(), + &channels.channel_id_a.as_ref(), + &wallet_a.as_ref(), + &wallet_b.address(), + &denom_a.with_amount(a_to_b_amount).as_ref(), + )?; + + info!("Will run ChanUpgradeAck step..."); + + channel.flipped().build_chan_upgrade_ack_and_send()?; + + info!("Check that the step ChanUpgradeAck was correctly executed..."); + + assert_eventually_channel_upgrade_flushing( + &chains.handle_a, + &chains.handle_b, + &channels.channel_id_a.as_ref(), + &channels.port_a.as_ref(), + &old_attrs, + )?; + + // wait enough time so that timeout expires while chain a is in FLUSHING + sleep(Duration::from_nanos(35000000000)); + + info!("Will run ChanUpgradeTimeout step..."); + + // Since the chain a has not moved to `FLUSH_COMPLETE` before the upgrade timeout + // expired, then we can submit `MsgChannelUpgradeTimeout` on chain b + // to cancel the upgrade and move the channel back to `OPEN` + let timeout_event = channel.build_chan_upgrade_timeout_and_send()?; + assert_eq!( + timeout_event.event_type(), + IbcEventType::UpgradeTimeoutChannel + ); + + relayer.with_supervisor(|| { + info!("Check that the step ChanUpgradeTimeout was correctly executed..."); + + assert_eventually_channel_upgrade_cancel( + &chains.handle_b, + &chains.handle_a, + &channels.channel_id_b.as_ref(), + &channels.port_b.as_ref(), + &old_attrs.flipped(), + )?; + + Ok(()) + }) + } +} diff --git a/tools/integration-test/src/tests/channel_upgrade/upgrade_handshake_steps.rs b/tools/integration-test/src/tests/channel_upgrade/upgrade_handshake_steps.rs index 7c1c59865d..ad31dba222 100644 --- a/tools/integration-test/src/tests/channel_upgrade/upgrade_handshake_steps.rs +++ b/tools/integration-test/src/tests/channel_upgrade/upgrade_handshake_steps.rs @@ -12,8 +12,11 @@ //! - `ChannelUpgradeHandshakeFromConfirm` tests that the channel worker will finish the //! upgrade handshake if the channel is being upgraded and is at the Confirm step. //! -//! - `ChannelUpgradeHandshakeTimeoutOnAck` tests that the channel worker will finish the -//! cancel the upgrade handshake if the Ack step fails due to an upgrade timeout. +//! - `ChannelUpgradeHandshakeTimeoutOnAck` tests that the channel worker will cancel the +//! upgrade handshake if the Ack step fails due to an upgrade timeout. +//! +//! - `ChannelUpgradeHandshakeTimeoutWhenFlushing` tests that the channel worker will timeout the +//! upgrade handshake if the counterparty does not finish flushing the packets before the upgrade timeout. use ibc_relayer::chain::requests::{IncludeProof, QueryChannelRequest, QueryHeight}; use ibc_relayer_types::core::ics04_channel::packet::Sequence; @@ -24,9 +27,11 @@ use ibc_test_framework::prelude::*; use ibc_test_framework::relayer::channel::{ assert_eventually_channel_established, assert_eventually_channel_upgrade_ack, assert_eventually_channel_upgrade_cancel, assert_eventually_channel_upgrade_confirm, - assert_eventually_channel_upgrade_init, assert_eventually_channel_upgrade_open, - assert_eventually_channel_upgrade_try, ChannelUpgradableAttributes, + assert_eventually_channel_upgrade_flushing, assert_eventually_channel_upgrade_init, + assert_eventually_channel_upgrade_open, assert_eventually_channel_upgrade_try, + ChannelUpgradableAttributes, }; +use ibc_test_framework::util::random::random_u128_range; #[test] fn test_channel_upgrade_manual_handshake() -> Result<(), Error> { @@ -53,6 +58,11 @@ fn test_channel_upgrade_handshake_timeout_on_ack() -> Result<(), Error> { run_binary_channel_test(&ChannelUpgradeHandshakeTimeoutOnAck) } +#[test] +fn test_channel_upgrade_handshake_timeout_when_flushing() -> Result<(), Error> { + run_binary_channel_test(&ChannelUpgradeHandshakeTimeoutWhenFlushing) +} + const MAX_DEPOSIT_PERIOD: &str = "10s"; const VOTING_PERIOD: u64 = 10; @@ -783,6 +793,186 @@ impl BinaryChannelTest for ChannelUpgradeHandshakeTimeoutOnAck { } } +struct ChannelUpgradeHandshakeTimeoutWhenFlushing; + +impl BinaryChannelTest for ChannelUpgradeHandshakeTimeoutWhenFlushing { + fn run( + &self, + _config: &TestConfig, + _relayer: RelayerDriver, + chains: ConnectedChains, + channels: ConnectedChannel, + ) -> Result<(), Error> { + info!("Check that channels are both in OPEN State"); + + assert_eventually_channel_established( + &chains.handle_b, + &chains.handle_a, + &channels.channel_id_b.as_ref(), + &channels.port_b.as_ref(), + )?; + + let channel_end_a = chains + .handle_a + .query_channel( + QueryChannelRequest { + port_id: channels.port_a.0.clone(), + channel_id: channels.channel_id_a.0.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map(|(channel_end, _)| channel_end) + .map_err(|e| eyre!("Error querying ChannelEnd A: {e}"))?; + + let channel_end_b = chains + .handle_b + .query_channel( + QueryChannelRequest { + port_id: channels.port_b.0.clone(), + channel_id: channels.channel_id_b.0.clone(), + height: QueryHeight::Latest, + }, + IncludeProof::No, + ) + .map(|(channel_end, _)| channel_end) + .map_err(|e| eyre!("Error querying ChannelEnd B: {e}"))?; + + let old_version = channel_end_a.version; + let old_ordering = channel_end_a.ordering; + let old_connection_hops_a = channel_end_a.connection_hops; + let old_connection_hops_b = channel_end_b.connection_hops; + + let channel = channels.channel; + let new_version = Version::ics20_with_fee(); + + let old_attrs = ChannelUpgradableAttributes::new( + old_version.clone(), + old_version.clone(), + old_ordering, + old_connection_hops_a.clone(), + old_connection_hops_b.clone(), + Sequence::from(1), + ); + + info!("Will update channel params to set a shorter upgrade timeout..."); + + // the upgrade timeout should be long enough for chain a + // to complete Ack successfully so that it goes into `FLUSHING` + chains.node_b.chain_driver().update_channel_params( + 25000000000, + chains.handle_b().get_signer().unwrap().as_ref(), + "1", + )?; + + info!("Will initialise upgrade handshake with governance proposal..."); + + chains.node_a.chain_driver().initialise_channel_upgrade( + channel.src_port_id().as_str(), + channel.src_channel_id().unwrap().as_str(), + old_ordering.as_str(), + old_connection_hops_a.first().unwrap().as_str(), + &serde_json::to_string(&new_version.0).unwrap(), + chains.handle_a().get_signer().unwrap().as_ref(), + "1", + )?; + + info!("Check that the step ChanUpgradeInit was correctly executed..."); + + assert_eventually_channel_upgrade_init( + &chains.handle_a, + &chains.handle_b, + &channels.channel_id_a.as_ref(), + &channels.port_a.as_ref(), + &old_attrs, + )?; + + info!("Will run ChanUpgradeTry step..."); + + channel.build_chan_upgrade_try_and_send()?; + + info!("Check that the step ChanUpgradeTry was correctly executed..."); + + assert_eventually_channel_upgrade_try( + &chains.handle_b, + &chains.handle_a, + &channels.channel_id_b.as_ref(), + &channels.port_b.as_ref(), + &old_attrs.flipped(), + )?; + + // send a IBC transfer message from chain a to chain b + // so that we have an in-flight packet and chain a + // will move to `FLUSHING` during Ack + let denom_a = chains.node_a.denom(); + let wallet_a = chains.node_a.wallets().user1().cloned(); + let wallet_b = chains.node_b.wallets().user1().cloned(); + let a_to_b_amount = random_u128_range(1000, 5000); + + info!( + "Sending IBC transfer from chain {} to chain {} with amount of {} {}", + chains.chain_id_a(), + chains.chain_id_b(), + a_to_b_amount, + denom_a + ); + + chains.node_a.chain_driver().ibc_transfer_token( + &channels.port_a.as_ref(), + &channels.channel_id_a.as_ref(), + &wallet_a.as_ref(), + &wallet_b.address(), + &denom_a.with_amount(a_to_b_amount).as_ref(), + )?; + + info!("Will run ChanUpgradeAck step..."); + + channel.flipped().build_chan_upgrade_ack_and_send()?; + + info!("Check that the step ChanUpgradeAck was correctly executed..."); + + assert_eventually_channel_upgrade_flushing( + &chains.handle_a, + &chains.handle_b, + &channels.channel_id_a.as_ref(), + &channels.port_a.as_ref(), + &old_attrs, + )?; + + // wait enough time so that timeout expires while chain a is in FLUSHING + sleep(Duration::from_nanos(35000000000)); + + info!("Will run ChanUpgradeTimeout step..."); + + // Since the chain a has not moved to `FLUSH_COMPLETE` before the upgrade timeout + // expired, then we can submit `MsgChannelUpgradeTimeout` on chain b + // to cancel the upgrade and move the channel back to `OPEN` + let timeout_event = channel.build_chan_upgrade_timeout_and_send()?; + assert_eq!( + timeout_event.event_type(), + IbcEventType::UpgradeTimeoutChannel + ); + + let cancel_event = channel.flipped().build_chan_upgrade_cancel_and_send()?; + assert_eq!( + cancel_event.event_type(), + IbcEventType::UpgradeCancelChannel + ); + + info!("Check that the step ChanUpgradeTimeout was correctly executed..."); + + assert_eventually_channel_upgrade_cancel( + &chains.handle_b, + &chains.handle_a, + &channels.channel_id_b.as_ref(), + &channels.port_b.as_ref(), + &old_attrs.flipped(), + )?; + + Ok(()) + } +} + impl HasOverrides for ChannelUpgradeManualHandshake { type Overrides = ChannelUpgradeTestOverrides; @@ -822,3 +1012,11 @@ impl HasOverrides for ChannelUpgradeHandshakeTimeoutOnAck { &ChannelUpgradeTestOverrides } } + +impl HasOverrides for ChannelUpgradeHandshakeTimeoutWhenFlushing { + type Overrides = ChannelUpgradeTestOverrides; + + fn get_overrides(&self) -> &ChannelUpgradeTestOverrides { + &ChannelUpgradeTestOverrides + } +} diff --git a/tools/test-framework/src/chain/ext/bootstrap.rs b/tools/test-framework/src/chain/ext/bootstrap.rs index 37f7818d75..3fb6e00ecd 100644 --- a/tools/test-framework/src/chain/ext/bootstrap.rs +++ b/tools/test-framework/src/chain/ext/bootstrap.rs @@ -334,7 +334,7 @@ impl ChainBootstrapMethodsExt for ChainDriver { assert_eventually_succeed( &format!("proposal `{}` status: {}", proposal_id, status.as_str()), 10, - Duration::from_secs(2), + Duration::from_secs(3), || match query_gov_proposal( chain_id, command_path, diff --git a/tools/test-framework/src/relayer/channel.rs b/tools/test-framework/src/relayer/channel.rs index 94875f7139..0897ba94e1 100644 --- a/tools/test-framework/src/relayer/channel.rs +++ b/tools/test-framework/src/relayer/channel.rs @@ -453,6 +453,33 @@ pub fn assert_eventually_channel_upgrade_ack( + handle_a: &ChainA, + handle_b: &ChainB, + channel_id_a: &TaggedChannelIdRef, + port_id_a: &TaggedPortIdRef, + upgrade_attrs: &ChannelUpgradableAttributes, +) -> Result, Error> { + assert_eventually_succeed( + "channel upgrade ack step should be done", + 20, + Duration::from_secs(1), + || { + assert_channel_upgrade_state( + ChannelState::Flushing, + ChannelState::Flushing, + handle_a, + handle_b, + channel_id_a, + port_id_a, + upgrade_attrs, + &Sequence::from(1), + &Sequence::from(1), + ) + }, + ) +} + pub fn assert_eventually_channel_upgrade_confirm( handle_a: &ChainA, handle_b: &ChainB,