From 3118ba0e35e8a2525a3083cae73ab581584019f4 Mon Sep 17 00:00:00 2001 From: Luca Joss Date: Mon, 15 Jan 2024 09:38:46 +0100 Subject: [PATCH 1/3] Add test for ICA channel timeout --- tools/integration-test/src/tests/ica.rs | 201 ++++++++++++++++++++---- 1 file changed, 171 insertions(+), 30 deletions(-) diff --git a/tools/integration-test/src/tests/ica.rs b/tools/integration-test/src/tests/ica.rs index 8a7169ab0c..03d367b675 100644 --- a/tools/integration-test/src/tests/ica.rs +++ b/tools/integration-test/src/tests/ica.rs @@ -24,7 +24,7 @@ use ibc_test_framework::chain::ext::ica::register_interchain_account; use ibc_test_framework::ibc::denom::Denom; use ibc_test_framework::prelude::*; use ibc_test_framework::relayer::channel::{ - assert_eventually_channel_established, query_channel_end, + assert_eventually_channel_closed, assert_eventually_channel_established, query_channel_end, }; #[test] @@ -43,6 +43,16 @@ fn test_ica_filter_allow() -> Result<(), Error> { ))) } +#[test] +fn test_ica_filter_deny() -> Result<(), Error> { + run_binary_connection_test(&IcaFilterTestDeny) +} + +#[test] +fn test_ica_close_channel() -> Result<(), Error> { + run_binary_connection_test(&ICACloseChannelTest) +} + pub struct IcaFilterTestAllow { packet_filter: PacketFilter, } @@ -177,35 +187,6 @@ impl BinaryConnectionTest for IcaFilterTestAllow { Ok(()) } } - -fn interchain_send_tx( - chain: &ChainA, - from: &Signer, - connection: &ConnectionId, - msg: InterchainAccountPacketData, - relative_timeout: Timestamp, -) -> Result, Error> { - let msg = MsgSendTx { - owner: from.clone(), - connection_id: connection.clone(), - packet_data: msg, - relative_timeout, - }; - - let msg_any = msg.to_any(); - - let tm = TrackedMsgs::new_static(vec![msg_any], "SendTx"); - - chain - .send_messages_and_wait_commit(tm) - .map_err(Error::relayer) -} - -#[test] -fn test_ica_filter_deny() -> Result<(), Error> { - run_binary_connection_test(&IcaFilterTestDeny) -} - pub struct IcaFilterTestDeny; impl TestOverrides for IcaFilterTestDeny { @@ -255,3 +236,163 @@ impl BinaryConnectionTest for IcaFilterTestDeny { ) } } + +pub struct ICACloseChannelTest; + +impl TestOverrides for ICACloseChannelTest { + fn modify_relayer_config(&self, config: &mut Config) { + config.mode.channels.enabled = true; + + config.mode.clients.misbehaviour = false; + } + + fn should_spawn_supervisor(&self) -> bool { + false + } +} + +impl BinaryConnectionTest for ICACloseChannelTest { + fn run( + &self, + _config: &TestConfig, + relayer: RelayerDriver, + chains: ConnectedChains, + connection: ConnectedConnection, + ) -> Result<(), Error> { + let stake_denom: MonoTagged = MonoTagged::new(Denom::base("stake")); + let (wallet, ica_address, controller_channel_id, controller_port_id) = relayer + .with_supervisor(|| { + // Register an interchain account on behalf of + // controller wallet `user1` where the counterparty chain is the interchain accounts host. + let (wallet, controller_channel_id, controller_port_id) = + register_interchain_account(&chains.node_a, chains.handle_a(), &connection)?; + + // Check that the corresponding ICA channel is eventually established. + let _counterparty_channel_id = assert_eventually_channel_established( + chains.handle_a(), + chains.handle_b(), + &controller_channel_id.as_ref(), + &controller_port_id.as_ref(), + )?; + + // Query the controller chain for the address of the ICA wallet on the host chain. + let ica_address = chains.node_a.chain_driver().query_interchain_account( + &wallet.address(), + &connection.connection_id_a.as_ref(), + )?; + + chains.node_b.chain_driver().assert_eventual_wallet_amount( + &ica_address.as_ref(), + &stake_denom.with_amount(0u64).as_ref(), + )?; + + Ok(( + wallet, + ica_address, + controller_channel_id, + controller_port_id, + )) + })?; + + // Send funds to the interchain account. + let ica_fund = 42000u64; + + chains.node_b.chain_driver().local_transfer_token( + &chains.node_b.wallets().user1(), + &ica_address.as_ref(), + &stake_denom.with_amount(ica_fund).as_ref(), + )?; + + chains.node_b.chain_driver().assert_eventual_wallet_amount( + &ica_address.as_ref(), + &stake_denom.with_amount(ica_fund).as_ref(), + )?; + + let amount = 12345u64; + + let msg = MsgSend { + from_address: ica_address.to_string(), + to_address: chains.node_b.wallets().user2().address().to_string(), + amount: vec![Coin { + denom: stake_denom.to_string(), + amount: Amount(U256::from(amount)), + }], + }; + + let raw_msg = msg.to_any(); + + let cosmos_tx = CosmosTx { + messages: vec![raw_msg], + }; + + let raw_cosmos_tx = cosmos_tx.to_any(); + + let interchain_account_packet_data = InterchainAccountPacketData::new(raw_cosmos_tx.value); + + let signer = Signer::from_str(&wallet.address().to_string()).unwrap(); + + let balance_user2 = chains.node_b.chain_driver().query_balance( + &chains.node_b.wallets().user2().address(), + &stake_denom.as_ref(), + )?; + + interchain_send_tx( + chains.handle_a(), + &signer, + &connection.connection_id_a.0, + interchain_account_packet_data, + Timestamp::from_nanoseconds(1000000000).unwrap(), + )?; + + sleep(Duration::from_nanos(3000000000)); + + relayer.with_supervisor(|| { + // Check that user2 has not received the sent amount. + chains.node_b.chain_driver().assert_eventual_wallet_amount( + &chains.node_b.wallets().user2().address(), + &(balance_user2).as_ref(), + )?; + sleep(Duration::from_secs(5)); + + // Check that the ICA account's balance has not been debited the sent amount. + chains.node_b.chain_driver().assert_eventual_wallet_amount( + &ica_address.as_ref(), + &stake_denom.with_amount(ica_fund).as_ref(), + )?; + + info!("Check that the channel closed after packet timeout..."); + + assert_eventually_channel_closed( + &chains.handle_a, + &chains.handle_b, + &controller_channel_id.as_ref(), + &controller_port_id.as_ref(), + )?; + + Ok(()) + }) + } +} + +fn interchain_send_tx( + chain: &ChainA, + from: &Signer, + connection: &ConnectionId, + msg: InterchainAccountPacketData, + relative_timeout: Timestamp, +) -> Result, Error> { + let msg = MsgSendTx { + owner: from.clone(), + connection_id: connection.clone(), + packet_data: msg, + relative_timeout, + }; + + let msg_any = msg.to_any(); + + let tm = TrackedMsgs::new_static(vec![msg_any], "SendTx"); + + chain + .send_messages_and_wait_commit(tm) + .map_err(Error::relayer) +} From 387d6f4140a45ae777ede04824c9f0fda3988cc9 Mon Sep 17 00:00:00 2001 From: Luca Joss Date: Mon, 15 Jan 2024 09:45:58 +0100 Subject: [PATCH 2/3] Add changelog entry --- .../ibc-integration-test/3778-ordered-channel-timeout.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/ibc-integration-test/3778-ordered-channel-timeout.md diff --git a/.changelog/unreleased/features/ibc-integration-test/3778-ordered-channel-timeout.md b/.changelog/unreleased/features/ibc-integration-test/3778-ordered-channel-timeout.md new file mode 100644 index 0000000000..7a46b56ba9 --- /dev/null +++ b/.changelog/unreleased/features/ibc-integration-test/3778-ordered-channel-timeout.md @@ -0,0 +1,2 @@ +- Add an ICA test to assert a channel correctly closes after a packet time-outs + ([\#3778](https://github.com/informalsystems/hermes/issues/3778)) \ No newline at end of file From 1dafcd71725ada0955dddac2dea6e126f79cd8db Mon Sep 17 00:00:00 2001 From: Luca Joss Date: Mon, 15 Jan 2024 10:59:57 +0100 Subject: [PATCH 3/3] Add missing assertion method --- tools/test-framework/src/relayer/channel.rs | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tools/test-framework/src/relayer/channel.rs b/tools/test-framework/src/relayer/channel.rs index 391a3b10f7..2affc07f87 100644 --- a/tools/test-framework/src/relayer/channel.rs +++ b/tools/test-framework/src/relayer/channel.rs @@ -247,3 +247,45 @@ pub fn assert_eventually_channel_established( + handle_a: &ChainA, + handle_b: &ChainB, + channel_id_a: &TaggedChannelIdRef, + port_id_a: &TaggedPortIdRef, +) -> Result, Error> { + assert_eventually_succeed( + "channel should eventually closed", + 20, + Duration::from_secs(2), + || { + let channel_end_a = query_channel_end(handle_a, channel_id_a, port_id_a)?; + + if !channel_end_a.value().state_matches(&ChannelState::Closed) { + return Err(Error::generic(eyre!( + "expected channel end A to be in closed state, but it is instead `{}", + channel_end_a.value().state() + ))); + } + + let channel_id_b = channel_end_a + .tagged_counterparty_channel_id() + .ok_or_else(|| { + eyre!("expected counterparty channel id to present on closed channel") + })?; + + let port_id_b = channel_end_a.tagged_counterparty_port_id(); + + let channel_end_b = + query_channel_end(handle_b, &channel_id_b.as_ref(), &port_id_b.as_ref())?; + + if !channel_end_b.value().state_matches(&ChannelState::Closed) { + return Err(Error::generic(eyre!( + "expected channel end B to be in closed state" + ))); + } + + Ok(channel_id_b) + }, + ) +}