diff --git a/docs/docs/02-apps/02-interchain-accounts/01-overview.md b/docs/docs/02-apps/02-interchain-accounts/01-overview.md new file mode 100644 index 00000000000..d09654230b9 --- /dev/null +++ b/docs/docs/02-apps/02-interchain-accounts/01-overview.md @@ -0,0 +1,48 @@ +--- +title: Overview +sidebar_label: Overview +sidebar_position: 1 +slug: /apps/interchain-accounts/overview +--- + + +# Overview + +:::note Synopsis +Learn about what the Interchain Accounts module is +::: + +## What is the Interchain Accounts module? + +Interchain Accounts is the Cosmos SDK implementation of the ICS-27 protocol, which enables cross-chain account management built upon IBC. + +- How does an interchain account differ from a regular account? + +Regular accounts use a private key to sign transactions. Interchain Accounts are instead controlled programmatically by counterparty chains via IBC packets. + +## Concepts + +`Host Chain`: The chain where the interchain account is registered. The host chain listens for IBC packets from a controller chain which should contain instructions (e.g. Cosmos SDK messages) for which the interchain account will execute. + +`Controller Chain`: The chain registering and controlling an account on a host chain. The controller chain sends IBC packets to the host chain to control the account. + +`Interchain Account`: An account on a host chain created using the ICS-27 protocol. An interchain account has all the capabilities of a normal account. However, rather than signing transactions with a private key, a controller chain will send IBC packets to the host chain which signals what transactions the interchain account should execute. + +`Authentication Module`: A custom application module on the controller chain that uses the Interchain Accounts module to build custom logic for the creation & management of interchain accounts. It can be either an IBC application module using the [legacy API](10-legacy/03-keeper-api.md), or a regular Cosmos SDK application module sending messages to the controller submodule's `MsgServer` (this is the recommended approach from ibc-go v6 if access to packet callbacks is not needed). Please note that the legacy API will eventually be removed and IBC applications will not be able to use them in later releases. + +## SDK security model + +SDK modules on a chain are assumed to be trustworthy. For example, there are no checks to prevent an untrustworthy module from accessing the bank keeper. + +The implementation of ICS-27 in ibc-go uses this assumption in its security considerations. + +The implementation assumes other IBC application modules will not bind to ports within the ICS-27 namespace. + +## Channel Closure + +The provided interchain account host and controller implementations do not support `ChanCloseInit`. However, they do support `ChanCloseConfirm`. +This means that the host and controller modules cannot close channels, but they will confirm channel closures initiated by other implementations of ICS-27. + +In the event of a channel closing (due to a packet timeout in an ordered channel, for example), the interchain account associated with that channel can become accessible again if a new channel is created with a (JSON-formatted) version string that encodes the exact same `Metadata` information of the previous channel. The channel can be reopened using either [`MsgRegisterInterchainAccount`](./05-messages.md#msgregisterinterchainaccount) or `MsgChannelOpenInit`. If `MsgRegisterInterchainAccount` is used, then it is possible to leave the `version` field of the message empty, since it will be filled in by the controller submodule. If `MsgChannelOpenInit` is used, then the `version` field must be provided with the correct JSON-encoded `Metadata` string. See section [Understanding Active Channels](./09-active-channels.md#understanding-active-channels) for more information. + +When reopening a channel with the default controller submodule, the ordering of the channel cannot be changed. In order to change the ordering of the channel, the channel has to go through a [channel upgrade handshake](../../01-ibc/06-channel-upgrades.md) or reopen the channel with a custom controller implementation. diff --git a/docs/docs/02-apps/02-interchain-accounts/05-messages.md b/docs/docs/02-apps/02-interchain-accounts/05-messages.md new file mode 100644 index 00000000000..1e40a4c3c9c --- /dev/null +++ b/docs/docs/02-apps/02-interchain-accounts/05-messages.md @@ -0,0 +1,79 @@ +--- +title: Messages +sidebar_label: Messages +sidebar_position: 5 +slug: /apps/interchain-accounts/messages +--- + + +# Messages + +## `MsgRegisterInterchainAccount` + +An Interchain Accounts channel handshake can be initiated using `MsgRegisterInterchainAccount`: + +```go +type MsgRegisterInterchainAccount struct { + Owner string + ConnectionID string + Version string + Order channeltypes.Order +} +``` + +This message is expected to fail if: + +- `Owner` is an empty string. +- `ConnectionID` is invalid (see [24-host naming requirements](https://github.com/cosmos/ibc/blob/master/spec/core/ics-024-host-requirements/README.md#paths-identifiers-separators)). + +This message will construct a new `MsgChannelOpenInit` on chain and route it to the core IBC message server to initiate the opening step of the channel handshake. + +The controller submodule will generate a new port identifier and claim the associated port capability. The caller is expected to provide an appropriate application version string. For example, this may be an ICS-27 JSON encoded [`Metadata`](https://github.com/cosmos/ibc-go/blob/v6.0.0/proto/ibc/applications/interchain_accounts/v1/metadata.proto#L11) type or an ICS-29 JSON encoded [`Metadata`](https://github.com/cosmos/ibc-go/blob/v6.0.0/proto/ibc/applications/fee/v1/metadata.proto#L11) type with a nested application version. +If the `Version` string is omitted, the controller submodule will construct a default version string in the `OnChanOpenInit` handshake callback. + +```go +type MsgRegisterInterchainAccountResponse struct { + ChannelID string + PortId string +} +``` + +The `ChannelID` and `PortID` are returned in the message response. + +## `MsgSendTx` + +An Interchain Accounts transaction can be executed on a remote host chain by sending a `MsgSendTx` from the corresponding controller chain: + +```go +type MsgSendTx struct { + Owner string + ConnectionID string + PacketData InterchainAccountPacketData + RelativeTimeout uint64 +} +``` + +This message is expected to fail if: + +- `Owner` is an empty string. +- `ConnectionID` is invalid (see [24-host naming requirements](https://github.com/cosmos/ibc/blob/master/spec/core/ics-024-host-requirements/README.md#paths-identifiers-separators)). +- `PacketData` contains an `UNSPECIFIED` type enum, the length of `Data` bytes is zero or the `Memo` field exceeds 256 characters in length. +- `RelativeTimeout` is zero. + +This message will create a new IBC packet with the provided `PacketData` and send it via the channel associated with the `Owner` and `ConnectionID`. +The `PacketData` is expected to contain a list of serialized `[]sdk.Msg` in the form of `CosmosTx`. Please note the signer field of each `sdk.Msg` must be the interchain account address. +When the packet is relayed to the host chain, the `PacketData` is unmarshalled and the messages are authenticated and executed. + +```go +type MsgSendTxResponse struct { + Sequence uint64 +} +``` + +The packet `Sequence` is returned in the message response. + +## Atomicity + +As the Interchain Accounts module supports the execution of multiple transactions using the Cosmos SDK `Msg` interface, it provides the same atomicity guarantees as Cosmos SDK-based applications, leveraging the [`CacheMultiStore`](https://docs.cosmos.network/main/learn/advanced/store#cachemultistore) architecture provided by the [`Context`](https://docs.cosmos.network/main/learn/advanced/context.html) type. + +This provides atomic execution of transactions when using Interchain Accounts, where state changes are only committed if all `Msg`s succeed. diff --git a/docs/docs/02-apps/02-interchain-accounts/08-client.md b/docs/docs/02-apps/02-interchain-accounts/08-client.md new file mode 100644 index 00000000000..2fd5f44d513 --- /dev/null +++ b/docs/docs/02-apps/02-interchain-accounts/08-client.md @@ -0,0 +1,202 @@ +--- +title: Client +sidebar_label: Client +sidebar_position: 8 +slug: /apps/interchain-accounts/client +--- + +# Client + +## CLI + +A user can query and interact with the Interchain Accounts module using the CLI. Use the `--help` flag to discover the available commands: + +```shell +simd query interchain-accounts --help +``` + +> Please not that this section does not document all the available commands, but only the ones that deserved extra documentation that was not possible to fit in the command line documentation. + +### Controller + +A user can query and interact with the controller submodule. + +#### Query + +The `query` commands allow users to query the controller submodule. + +```shell +simd query interchain-accounts controller --help +``` + +#### Transactions + +The `tx` commands allow users to interact with the controller submodule. + +```shell +simd tx interchain-accounts controller --help +``` + +#### `register` + +The `register` command allows users to register an interchain account on a host chain on the provided connection. + +```shell +simd tx interchain-accounts controller register [connection-id] [flags] +``` + +During registration a new channel is set up between controller and host. There are two flags available that influence the channel that is created: + +- `--version` to specify the (JSON-formatted) version string of the channel. For example: `{\"version\":\"ics27-1\",\"encoding\":\"proto3\",\"tx_type\":\"sdk_multi_msg\",\"controller_connection_id\":\"connection-0\",\"host_connection_id\":\"connection-0\"}`. Passing a custom version string is useful if you want to specify, for example, the encoding format of the interchain accounts packet data (either `proto3` or `proto3json`). If not specified the controller submodule will generate a default version string. +- `--ordering` to specify the ordering of the channel. Available options are `order_ordered` (default if not specified) and `order_unordered`. + +Example: + +```shell +simd tx interchain-accounts controller register connection-0 --ordering order_unordered --from cosmos1.. +``` + +#### `send-tx` + +The `send-tx` command allows users to send a transaction on the provided connection to be executed using an interchain account on the host chain. + +```shell +simd tx interchain-accounts controller send-tx [connection-id] [path/to/packet_msg.json] +``` + +Example: + +```shell +simd tx interchain-accounts controller send-tx connection-0 packet-data.json --from cosmos1.. +``` + +See below for example contents of `packet-data.json`. The CLI handler will unmarshal the following into `InterchainAccountPacketData` appropriately. + +```json +{ + "type":"TYPE_EXECUTE_TX", + "data":"CqIBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEoEBCkFjb3Ntb3MxNWNjc2hobXAwZ3N4MjlxcHFxNmc0em1sdG5udmdteXU5dWV1YWRoOXkybmM1emowc3psczVndGRkehItY29zbW9zMTBoOXN0YzV2Nm50Z2V5Z2Y1eGY5NDVuanFxNWgzMnI1M3VxdXZ3Gg0KBXN0YWtlEgQxMDAw", + "memo":"" +} +``` + +Note the `data` field is a base64 encoded byte string as per the tx encoding agreed upon during the channel handshake. + +A helper CLI is provided in the host submodule which can be used to generate the packet data JSON using the counterparty chain's binary. See the [`generate-packet-data` command](#generate-packet-data) for an example. + +### Host + +A user can query and interact with the host submodule. + +#### Query + +The `query` commands allow users to query the host submodule. + +```shell +simd query interchain-accounts host --help +``` + +#### Transactions + +The `tx` commands allow users to interact with the controller submodule. + +```shell +simd tx interchain-accounts host --help +``` + +##### `generate-packet-data` + +The `generate-packet-data` command allows users to generate protobuf or proto3 JSON encoded interchain accounts packet data for input message(s). The packet data can then be used with the controller submodule's [`send-tx` command](#send-tx). The `--encoding` flag can be used to specify the encoding format (value must be either `proto3` or `proto3json`); if not specified, the default will be `proto3`. The `--memo` flag can be used to include a memo string in the interchain accounts packet data. + +```shell +simd tx interchain-accounts host generate-packet-data [message] +``` + +Example: + +```shell +simd tx interchain-accounts host generate-packet-data '[{ + "@type":"/cosmos.bank.v1beta1.MsgSend", + "from_address":"cosmos15ccshhmp0gsx29qpqq6g4zmltnnvgmyu9ueuadh9y2nc5zj0szls5gtddz", + "to_address":"cosmos10h9stc5v6ntgeygf5xf945njqq5h32r53uquvw", + "amount": [ + { + "denom": "stake", + "amount": "1000" + } + ] +}]' --memo memo +``` + +The command accepts a single `sdk.Msg` or a list of `sdk.Msg`s that will be encoded into the outputs `data` field. + +Example output: + +```json +{ + "type":"TYPE_EXECUTE_TX", + "data":"CqIBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEoEBCkFjb3Ntb3MxNWNjc2hobXAwZ3N4MjlxcHFxNmc0em1sdG5udmdteXU5dWV1YWRoOXkybmM1emowc3psczVndGRkehItY29zbW9zMTBoOXN0YzV2Nm50Z2V5Z2Y1eGY5NDVuanFxNWgzMnI1M3VxdXZ3Gg0KBXN0YWtlEgQxMDAw", + "memo":"memo" +} +``` + +## gRPC + +A user can query the interchain account module using gRPC endpoints. + +### Controller + +A user can query the controller submodule using gRPC endpoints. + +#### `InterchainAccount` + +The `InterchainAccount` endpoint allows users to query the controller submodule for the interchain account address for a given owner on a particular connection. + +```shell +ibc.applications.interchain_accounts.controller.v1.Query/InterchainAccount +``` + +Example: + +```shell +grpcurl -plaintext \ + -d '{"owner":"cosmos1..","connection_id":"connection-0"}' \ + localhost:9090 \ + ibc.applications.interchain_accounts.controller.v1.Query/InterchainAccount +``` + +#### `Params` + +The `Params` endpoint users to query the current controller submodule parameters. + +```shell +ibc.applications.interchain_accounts.controller.v1.Query/Params +``` + +Example: + +```shell +grpcurl -plaintext \ + localhost:9090 \ + ibc.applications.interchain_accounts.controller.v1.Query/Params +``` + +### Host + +A user can query the host submodule using gRPC endpoints. + +#### `Params` + +The `Params` endpoint users to query the current host submodule parameters. + +```shell +ibc.applications.interchain_accounts.host.v1.Query/Params +``` + +Example: + +```shell +grpcurl -plaintext \ + localhost:9090 \ + ibc.applications.interchain_accounts.host.v1.Query/Params +``` diff --git a/docs/docs/02-apps/02-interchain-accounts/09-active-channels.md b/docs/docs/02-apps/02-interchain-accounts/09-active-channels.md new file mode 100644 index 00000000000..09ae7063a68 --- /dev/null +++ b/docs/docs/02-apps/02-interchain-accounts/09-active-channels.md @@ -0,0 +1,45 @@ +--- +title: Active Channels +sidebar_label: Active Channels +sidebar_position: 9 +slug: /apps/interchain-accounts/active-channels +--- + +# Understanding Active Channels + +The Interchain Accounts module uses either [ORDERED or UNORDERED](https://github.com/cosmos/ibc/tree/master/spec/core/ics-004-channel-and-packet-semantics#ordering) channels. + +When using `ORDERED` channels, the order of transactions when sending packets from a controller to a host chain is maintained. + +When using `UNORDERED` channels, there is no guarantee that the order of transactions when sending packets from the controller to the host chain is maintained. + +> A limitation when using ORDERED channels is that when a packet times out the channel will be closed. + +In the case of a channel closing, a controller chain needs to be able to regain access to the interchain account registered on this channel. `Active Channels` enable this functionality. + +When an Interchain Account is registered using `MsgRegisterInterchainAccount`, a new channel is created on a particular port. During the `OnChanOpenAck` and `OnChanOpenConfirm` steps (on controller & host chain respectively) the `Active Channel` for this interchain account is stored in state. + +It is possible to create a new channel using the same controller chain portID if the previously set `Active Channel` is now in a `CLOSED` state. This channel creation can be initialized programmatically by sending a new `MsgChannelOpenInit` message like so: + +```go +msg := channeltypes.NewMsgChannelOpenInit(portID, string(versionBytes), channeltypes.ORDERED, []string{connectionID}, icatypes.HostPortID, authtypes.NewModuleAddress(icatypes.ModuleName).String()) +handler := keeper.msgRouter.Handler(msg) +res, err := handler(ctx, msg) +if err != nil { + return err +} +``` + +Alternatively, any relayer operator may initiate a new channel handshake for this interchain account once the previously set `Active Channel` is in a `CLOSED` state. This is done by initiating the channel handshake on the controller chain using the same portID associated with the interchain account in question. + +It is important to note that once a channel has been opened for a given interchain account, new channels can not be opened for this account until the currently set `Active Channel` is set to `CLOSED`. + +## Future improvements + +Future versions of the ICS-27 protocol and the Interchain Accounts module will likely use a new channel type that provides ordering of packets without the channel closing in the event of a packet timing out, thus removing the need for `Active Channels` entirely. +The following is a list of issues which will provide the infrastructure to make this possible: + +- [IBC Channel Upgrades](https://github.com/cosmos/ibc-go/issues/1599) +- [Implement ORDERED_ALLOW_TIMEOUT logic in 04-channel](https://github.com/cosmos/ibc-go/issues/1661) +- [Add ORDERED_ALLOW_TIMEOUT as supported ordering in 03-connection](https://github.com/cosmos/ibc-go/issues/1662) +- [Allow ICA channels to be opened as ORDERED_ALLOW_TIMEOUT](https://github.com/cosmos/ibc-go/issues/1663) diff --git a/e2e/tests/core/04-channel/upgrades_test.go b/e2e/tests/core/04-channel/upgrades_test.go new file mode 100644 index 00000000000..b3cd671911a --- /dev/null +++ b/e2e/tests/core/04-channel/upgrades_test.go @@ -0,0 +1,328 @@ +//go:build !test_e2e + +package channel + +import ( + "context" + "testing" + + "github.com/strangelove-ventures/interchaintest/v8/ibc" + test "github.com/strangelove-ventures/interchaintest/v8/testutil" + testifysuite "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" +) + +func TestChannelTestSuite(t *testing.T) { + testifysuite.Run(t, new(ChannelTestSuite)) +} + +type ChannelTestSuite struct { + testsuite.E2ETestSuite +} + +// TestChannelUpgrade_WithFeeMiddleware_Succeeds tests upgrading a transfer channel to wire up fee middleware +func (s *ChannelTestSuite) TestChannelUpgrade_WithFeeMiddleware_Succeeds() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, s.TransferChannelOptions()) + channelB := channelA.Counterparty + chainA, chainB := s.GetChains() + + chainADenom := chainA.Config().Denom + chainBDenom := chainB.Config().Denom + chainAIBCToken := testsuite.GetIBCToken(chainBDenom, channelA.PortID, channelA.ChannelID) + chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelB.PortID, channelB.ChannelID) + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainAAddress := chainAWallet.FormattedAddress() + + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + chainBAddress := chainBWallet.FormattedAddress() + + var ( + chainARelayerWallet, chainBRelayerWallet ibc.Wallet + relayerAStartingBalance int64 + testFee = testvalues.DefaultFee(chainADenom) + ) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + + // trying to create some inflight packets, although they might get relayed before the upgrade starts + t.Run("create inflight transfer packets between chain A and chain B", func(t *testing.T) { + chainBWalletAmount := ibc.WalletAmount{ + Address: chainBWallet.FormattedAddress(), // destination address + Denom: chainADenom, + Amount: sdkmath.NewInt(testvalues.IBCTransferAmount), + } + + transferTxResp, err := chainA.SendIBCTransfer(ctx, channelA.ChannelID, chainAWallet.KeyName(), chainBWalletAmount, ibc.TransferOptions{}) + s.Require().NoError(err) + s.Require().NoError(transferTxResp.Validate(), "chain-a ibc transfer tx is invalid") + + chainAwalletAmount := ibc.WalletAmount{ + Address: chainAWallet.FormattedAddress(), // destination address + Denom: chainBDenom, + Amount: sdkmath.NewInt(testvalues.IBCTransferAmount), + } + + transferTxResp, err = chainB.SendIBCTransfer(ctx, channelB.ChannelID, chainBWallet.KeyName(), chainAwalletAmount, ibc.TransferOptions{}) + s.Require().NoError(err) + s.Require().NoError(transferTxResp.Validate(), "chain-b ibc transfer tx is invalid") + }) + + t.Run("execute gov proposal to initiate channel upgrade", func(t *testing.T) { + chA, err := s.QueryChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + + s.initiateChannelUpgrade(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, s.createUpgradeFields(chA)) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB), "failed to wait for blocks") + + t.Run("packets are relayed between chain A and chain B", func(t *testing.T) { + // packet from chain A to chain B + s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) + actualBalance, err := s.QueryBalance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + + // packet from chain B to chain A + s.AssertPacketRelayed(ctx, chainB, channelB.PortID, channelB.ChannelID, 1) + actualBalance, err = s.QueryBalance(ctx, chainA, chainAAddress, chainAIBCToken.IBCDenom()) + s.Require().NoError(err) + expected = testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) + + t.Run("verify channel A upgraded and is fee enabled", func(t *testing.T) { + channel, err := s.QueryChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + + // check the channel version include the fee version + version, err := feetypes.MetadataFromVersion(channel.Version) + s.Require().NoError(err) + s.Require().Equal(feetypes.Version, version.FeeVersion, "the channel version did not include ics29") + + // extra check + feeEnabled, err := s.QueryFeeEnabledChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Equal(true, feeEnabled) + }) + + t.Run("verify channel B upgraded and is fee enabled", func(t *testing.T) { + channel, err := s.QueryChannel(ctx, chainB, channelB.PortID, channelB.ChannelID) + s.Require().NoError(err) + + // check the channel version include the fee version + version, err := feetypes.MetadataFromVersion(channel.Version) + s.Require().NoError(err) + s.Require().Equal(feetypes.Version, version.FeeVersion, "the channel version did not include ics29") + + // extra check + feeEnabled, err := s.QueryFeeEnabledChannel(ctx, chainB, channelB.PortID, channelB.ChannelID) + s.Require().NoError(err) + s.Require().Equal(true, feeEnabled) + }) + + t.Run("prune packet acknowledgements", func(t *testing.T) { + // there should be one ack for the packet that we sent before the upgrade + acks, err := s.QueryPacketAcknowledgements(ctx, chainA, channelA.PortID, channelA.ChannelID, []uint64{}) + s.Require().NoError(err) + s.Require().Len(acks, 1) + s.Require().Equal(uint64(1), acks[0].Sequence) + + pruneAcksTxResponse := s.PruneAcknowledgements(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, uint64(1)) + s.AssertTxSuccess(pruneAcksTxResponse) + + // after pruning there should not be any acks + acks, err = s.QueryPacketAcknowledgements(ctx, chainA, channelA.PortID, channelA.ChannelID, []uint64{}) + s.Require().NoError(err) + s.Require().Empty(acks) + }) + + t.Run("stop relayer", func(t *testing.T) { + s.StopRelayer(ctx, relayer) + }) + + t.Run("recover relayer wallets", func(t *testing.T) { + err := s.RecoverRelayerWallets(ctx, relayer) + s.Require().NoError(err) + + chainARelayerWallet, chainBRelayerWallet, err = s.GetRelayerWallets(relayer) + s.Require().NoError(err) + + relayerAStartingBalance, err = s.GetChainANativeBalance(ctx, chainARelayerWallet) + s.Require().NoError(err) + t.Logf("relayer A user starting with balance: %d", relayerAStartingBalance) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + + t.Run("register and verify counterparty payee", func(t *testing.T) { + _, chainBRelayerUser := s.GetRelayerUsers(ctx) + resp := s.RegisterCounterPartyPayee(ctx, chainB, chainBRelayerUser, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, chainBRelayerWallet.FormattedAddress(), chainARelayerWallet.FormattedAddress()) + s.AssertTxSuccess(resp) + + address, err := s.QueryCounterPartyPayee(ctx, chainB, chainBRelayerWallet.FormattedAddress(), channelA.Counterparty.ChannelID) + s.Require().NoError(err) + s.Require().Equal(chainARelayerWallet.FormattedAddress(), address) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("send incentivized transfer packet", func(t *testing.T) { + // before adding fees for the packet, there should not be incentivized packets + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + + transferAmount := testvalues.DefaultTransferAmount(chainA.Config().Denom) + + msgPayPacketFee := feetypes.NewMsgPayPacketFee(testFee, channelA.PortID, channelA.ChannelID, chainAWallet.FormattedAddress(), nil) + msgTransfer := transfertypes.NewMsgTransfer(channelA.PortID, channelA.ChannelID, transferAmount, chainAWallet.FormattedAddress(), chainBWallet.FormattedAddress(), s.GetTimeoutHeight(ctx, chainB), 0, "") + resp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgPayPacketFee, msgTransfer) + s.AssertTxSuccess(resp) + }) + + t.Run("packets are relayed", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + t.Run("tokens are received by walletB", func(t *testing.T) { + actualBalance, err := s.QueryBalance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + + // walletB has received two IBC transfers of value testvalues.IBCTransferAmount since the start of the test. + expected := 2 * testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) + + t.Run("timeout fee is refunded", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + // once the relayer has relayed the packets, the timeout fee should be refunded. + // walletA has done two IBC transfers of value testvalues.IBCTransferAmount since the start of the test. + expected := testvalues.StartingTokenAmount - (2 * testvalues.IBCTransferAmount) - testFee.AckFee.AmountOf(chainADenom).Int64() - testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("relayerA is paid ack and recv fee", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainARelayerWallet) + s.Require().NoError(err) + + expected := relayerAStartingBalance + testFee.AckFee.AmountOf(chainADenom).Int64() + testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) +} + +// TestChannelUpgrade_WithFeeMiddleware_FailsWithTimeoutOnAck tests upgrading a transfer channel to wire up fee middleware but fails on ACK because of timeout +func (s *ChannelTestSuite) TestChannelUpgrade_WithFeeMiddleware_FailsWithTimeoutOnAck() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, s.TransferChannelOptions()) + channelB := channelA.Counterparty + chainA, chainB := s.GetChains() + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + + t.Run("execute gov proposal to set upgrade timeout", func(t *testing.T) { + s.setUpgradeTimeoutParam(ctx, chainB, chainBWallet) + }) + + t.Run("execute gov proposal to initiate channel upgrade", func(t *testing.T) { + chA, err := s.QueryChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + + s.initiateChannelUpgrade(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, s.createUpgradeFields(chA)) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB), "failed to wait for blocks") + + t.Run("verify channel A did not upgrade", func(t *testing.T) { + channel, err := s.QueryChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + + s.Require().Equal(channeltypes.OPEN, channel.State, "the channel state is not OPEN") + s.Require().Equal(transfertypes.Version, channel.Version, "the channel version is not ics20-1") + + errorReceipt, err := s.QueryUpgradeError(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Equal(uint64(1), errorReceipt.Sequence) + s.Require().Contains(errorReceipt.Message, "restored channel to pre-upgrade state") + }) + + t.Run("verify channel B did not upgrade", func(t *testing.T) { + channel, err := s.QueryChannel(ctx, chainB, channelB.PortID, channelB.ChannelID) + s.Require().NoError(err) + + s.Require().Equal(channeltypes.OPEN, channel.State, "the channel state is not OPEN") + s.Require().Equal(transfertypes.Version, channel.Version, "the channel version is not ics20-1") + + errorReceipt, err := s.QueryUpgradeError(ctx, chainB, channelB.PortID, channelB.ChannelID) + s.Require().NoError(err) + s.Require().Equal(uint64(1), errorReceipt.Sequence) + s.Require().Contains(errorReceipt.Message, "restored channel to pre-upgrade state") + }) +} + +// createUpgradeFields created the upgrade fields for channel +func (s *ChannelTestSuite) createUpgradeFields(channel channeltypes.Channel) channeltypes.UpgradeFields { + versionMetadata := feetypes.Metadata{ + FeeVersion: feetypes.Version, + AppVersion: transfertypes.Version, + } + versionBytes, err := feetypes.ModuleCdc.MarshalJSON(&versionMetadata) + s.Require().NoError(err) + + return channeltypes.NewUpgradeFields(channel.Ordering, channel.ConnectionHops, string(versionBytes)) +} + +// setUpgradeTimeoutParam creates and submits a governance proposal to execute the message to update 04-channel params with a timeout of 1s +func (s *ChannelTestSuite) setUpgradeTimeoutParam(ctx context.Context, chain ibc.Chain, wallet ibc.Wallet) { + const timeoutDelta = 1000000000 // use 1 second as relative timeout to force upgrade timeout on the counterparty + govModuleAddress, err := s.QueryModuleAccountAddress(ctx, govtypes.ModuleName, chain) + s.Require().NoError(err) + s.Require().NotNil(govModuleAddress) + + upgradeTimeout := channeltypes.NewTimeout(channeltypes.DefaultTimeout.Height, timeoutDelta) + msg := channeltypes.NewMsgUpdateChannelParams(govModuleAddress.String(), channeltypes.NewParams(upgradeTimeout)) + s.ExecuteAndPassGovV1Proposal(ctx, msg, chain, wallet) +} + +// initiateChannelUpgrade creates and submits a governance proposal to execute the message to initiate a channel upgrade +func (s *ChannelTestSuite) initiateChannelUpgrade(ctx context.Context, chain ibc.Chain, wallet ibc.Wallet, portID, channelID string, upgradeFields channeltypes.UpgradeFields) { + govModuleAddress, err := s.QueryModuleAccountAddress(ctx, govtypes.ModuleName, chain) + s.Require().NoError(err) + s.Require().NotNil(govModuleAddress) + + msg := channeltypes.NewMsgChannelUpgradeInit(portID, channelID, upgradeFields, govModuleAddress.String()) + s.ExecuteAndPassGovV1Proposal(ctx, msg, chain, wallet) +} diff --git a/e2e/tests/interchain_accounts/base_test.go b/e2e/tests/interchain_accounts/base_test.go new file mode 100644 index 00000000000..1199e0f35b8 --- /dev/null +++ b/e2e/tests/interchain_accounts/base_test.go @@ -0,0 +1,554 @@ +//go:build !test_e2e + +package interchainaccounts + +import ( + "context" + "testing" + "time" + + "github.com/cosmos/gogoproto/proto" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + test "github.com/strangelove-ventures/interchaintest/v8/testutil" + testifysuite "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + controllertypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/controller/types" + icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +// orderMapping is a mapping from channel ordering to the string representation of the ordering. +// the representation can be different depending on the relayer implementation. +var orderMapping = map[channeltypes.Order][]string{ + channeltypes.ORDERED: {channeltypes.ORDERED.String(), "Ordered"}, + channeltypes.UNORDERED: {channeltypes.UNORDERED.String(), "Unordered"}, +} + +func TestInterchainAccountsTestSuite(t *testing.T) { + testifysuite.Run(t, new(InterchainAccountsTestSuite)) +} + +type InterchainAccountsTestSuite struct { + testsuite.E2ETestSuite +} + +// RegisterInterchainAccount will attempt to register an interchain account on the counterparty chain. +func (s *InterchainAccountsTestSuite) RegisterInterchainAccount(ctx context.Context, chain ibc.Chain, user ibc.Wallet, msgRegisterAccount *controllertypes.MsgRegisterInterchainAccount) { + txResp := s.BroadcastMessages(ctx, chain, user, msgRegisterAccount) + s.AssertTxSuccess(txResp) +} + +func (s *InterchainAccountsTestSuite) TestMsgSendTx_SuccessfulTransfer() { + s.testMsgSendTxSuccessfulTransfer(channeltypes.ORDERED) +} + +func (s *InterchainAccountsTestSuite) TestMsgSendTx_SuccessfulTransfer_UnorderedChannel() { + s.testMsgSendTxSuccessfulTransfer(channeltypes.UNORDERED) +} + +func (s *InterchainAccountsTestSuite) testMsgSendTxSuccessfulTransfer(order channeltypes.Order) { + t := s.T() + ctx := context.TODO() + + // setup relayers and connection-0 between two chains + // channel-0 is a transfer channel but it will not be used in this test case + relayer, _ := s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, chainB := s.GetChains() + + // setup 2 accounts: controller account on chain A, a second chain B account. + // host account will be created when the ICA is registered + controllerAccount := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + controllerAddress := controllerAccount.FormattedAddress() + chainBAccount := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + var hostAccount string + + t.Run("broadcast MsgRegisterInterchainAccount", func(t *testing.T) { + // explicitly set the version string because we don't want to use incentivized channels. + version := icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID) + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, controllerAddress, version, order) + + txResp := s.BroadcastMessages(ctx, chainA, controllerAccount, msgRegisterAccount) + s.AssertTxSuccess(txResp) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("verify interchain account", func(t *testing.T) { + var err error + hostAccount, err = s.QueryInterchainAccount(ctx, chainA, controllerAddress, ibctesting.FirstConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(hostAccount)) + + channels, err := relayer.GetChannels(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID) + s.Require().NoError(err) + s.Require().Equal(len(channels), 2) + icaChannel := channels[0] + s.Require().Contains(orderMapping[order], icaChannel.Ordering) + }) + + t.Run("interchain account executes a bank transfer on behalf of the corresponding owner account", func(t *testing.T) { + t.Run("fund interchain account wallet", func(t *testing.T) { + // fund the host account so it has some $$ to send + err := chainB.SendFunds(ctx, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Address: hostAccount, + Amount: sdkmath.NewInt(testvalues.StartingTokenAmount), + Denom: chainB.Config().Denom, + }) + s.Require().NoError(err) + }) + + t.Run("broadcast MsgSendTx", func(t *testing.T) { + // assemble bank transfer message from host account to user account on host chain + msgSend := &banktypes.MsgSend{ + FromAddress: hostAccount, + ToAddress: chainBAccount.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainB.Config().Denom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(controllerAccount.FormattedAddress(), ibctesting.FirstConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + + resp := s.BroadcastMessages( + ctx, + chainA, + controllerAccount, + msgSendTx, + ) + + s.AssertTxSuccess(resp) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB)) + }) + + t.Run("verify tokens transferred", func(t *testing.T) { + balance, err := s.QueryBalance(ctx, chainB, chainBAccount.FormattedAddress(), chainB.Config().Denom) + s.Require().NoError(err) + + _, err = s.QueryBalance(ctx, chainB, hostAccount, chainB.Config().Denom) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64()) + }) + }) +} + +func (s *InterchainAccountsTestSuite) TestMsgSendTx_FailedTransfer_InsufficientFunds() { + t := s.T() + ctx := context.TODO() + + // setup relayers and connection-0 between two chains + // channel-0 is a transfer channel but it will not be used in this test case + relayer, _ := s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, chainB := s.GetChains() + + // setup 2 accounts: controller account on chain A, a second chain B account. + // host account will be created when the ICA is registered + controllerAccount := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + controllerAddress := controllerAccount.FormattedAddress() + chainBAccount := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + var hostAccount string + + t.Run("broadcast MsgRegisterInterchainAccount", func(t *testing.T) { + // explicitly set the version string because we don't want to use incentivized channels. + version := icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID) + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, controllerAddress, version, channeltypes.ORDERED) + + txResp := s.BroadcastMessages(ctx, chainA, controllerAccount, msgRegisterAccount) + s.AssertTxSuccess(txResp) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("verify interchain account", func(t *testing.T) { + var err error + hostAccount, err = s.QueryInterchainAccount(ctx, chainA, controllerAddress, ibctesting.FirstConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(hostAccount)) + + channels, err := relayer.GetChannels(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID) + s.Require().NoError(err) + s.Require().Equal(len(channels), 2) + }) + + t.Run("fail to execute bank transfer over ICA", func(t *testing.T) { + t.Run("verify empty host wallet", func(t *testing.T) { + hostAccountBalance, err := s.QueryBalance(ctx, chainB, hostAccount, chainB.Config().Denom) + + s.Require().NoError(err) + s.Require().Zero(hostAccountBalance.Int64()) + }) + + t.Run("broadcast MsgSendTx", func(t *testing.T) { + // assemble bank transfer message from host account to user account on host chain + msgSend := &banktypes.MsgSend{ + FromAddress: hostAccount, + ToAddress: chainBAccount.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainB.Config().Denom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(controllerAddress, ibctesting.FirstConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + + txResp := s.BroadcastMessages( + ctx, + chainA, + controllerAccount, + msgSendTx, + ) + + s.AssertTxSuccess(txResp) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB)) + }) + + t.Run("verify balance is the same", func(t *testing.T) { + balance, err := s.QueryBalance(ctx, chainB, chainBAccount.FormattedAddress(), chainB.Config().Denom) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64()) + }) + }) +} + +func (s *InterchainAccountsTestSuite) TestMsgSendTx_SuccessfulTransfer_AfterReopeningICA() { + t := s.T() + ctx := context.TODO() + + // setup relayers and connection-0 between two chains + // channel-0 is a transfer channel but it will not be used in this test case + relayer, _ := s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, chainB := s.GetChains() + + // setup 2 accounts: controller account on chain A, a second chain B account. + // host account will be created when the ICA is registered + controllerAccount := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + controllerAddress := controllerAccount.FormattedAddress() + chainBAccount := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + var ( + portID string + hostAccount string + + initialChannelID = "channel-1" + channelIDAfterReopening = "channel-2" + ) + + t.Run("register interchain account", func(t *testing.T) { + var err error + // explicitly set the version string because we don't want to use incentivized channels. + version := icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID) + msgRegisterInterchainAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, controllerAddress, version, channeltypes.ORDERED) + s.RegisterInterchainAccount(ctx, chainA, controllerAccount, msgRegisterInterchainAccount) + portID, err = icatypes.NewControllerPortID(controllerAddress) + s.Require().NoError(err) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("verify interchain account", func(t *testing.T) { + var err error + hostAccount, err = s.QueryInterchainAccount(ctx, chainA, controllerAddress, ibctesting.FirstConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(hostAccount)) + + _, err = s.QueryChannel(ctx, chainA, portID, initialChannelID) + s.Require().NoError(err) + }) + + // stop the relayer to let the submit tx message time out + t.Run("stop relayer", func(t *testing.T) { + s.StopRelayer(ctx, relayer) + }) + + t.Run("submit tx message with bank transfer message times out", func(t *testing.T) { + t.Run("fund interchain account wallet", func(t *testing.T) { + // fund the host account account so it has some $$ to send + err := chainB.SendFunds(ctx, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Address: hostAccount, + Amount: sdkmath.NewInt(testvalues.StartingTokenAmount), + Denom: chainB.Config().Denom, + }) + s.Require().NoError(err) + }) + + t.Run("broadcast MsgSendTx", func(t *testing.T) { + // assemble bank transfer message from host account to user account on host chain + msgSend := &banktypes.MsgSend{ + FromAddress: hostAccount, + ToAddress: chainBAccount.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainB.Config().Denom)), + } + + cdc := testsuite.Codec() + + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(controllerAddress, ibctesting.FirstConnectionID, uint64(1), packetData) + + resp := s.BroadcastMessages( + ctx, + chainA, + controllerAccount, + msgSendTx, + ) + + s.AssertTxSuccess(resp) + + // this sleep is to allow the packet to timeout + time.Sleep(1 * time.Second) + }) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("verify channel is closed due to timeout on ordered channel", func(t *testing.T) { + channel, err := s.QueryChannel(ctx, chainA, portID, initialChannelID) + s.Require().NoError(err) + + s.Require().Equal(channeltypes.CLOSED, channel.State, "the channel was not in an expected state") + }) + + t.Run("verify tokens not transferred", func(t *testing.T) { + balance, err := s.QueryBalance(ctx, chainB, chainBAccount.FormattedAddress(), chainB.Config().Denom) + s.Require().NoError(err) + + _, err = s.QueryBalance(ctx, chainB, hostAccount, chainB.Config().Denom) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64()) + }) + + // re-register interchain account to reopen the channel now that it has been closed due to timeout + // on an ordered channel + t.Run("register interchain account", func(t *testing.T) { + // explicitly set the version string because we don't want to use incentivized channels. + version := icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID) + msgRegisterInterchainAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, controllerAddress, version, channeltypes.ORDERED) + s.RegisterInterchainAccount(ctx, chainA, controllerAccount, msgRegisterInterchainAccount) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB)) + }) + + t.Run("verify new channel is now open and interchain account has been reregistered with the same portID", func(t *testing.T) { + channel, err := s.QueryChannel(ctx, chainA, portID, channelIDAfterReopening) + s.Require().NoError(err) + + s.Require().Equal(channeltypes.OPEN, channel.State, "the channel was not in an expected state") + }) + + t.Run("broadcast MsgSendTx", func(t *testing.T) { + // assemble bank transfer message from host account to user account on host chain + msgSend := &banktypes.MsgSend{ + FromAddress: hostAccount, + ToAddress: chainBAccount.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainB.Config().Denom)), + } + + cdc := testsuite.Codec() + + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(controllerAddress, ibctesting.FirstConnectionID, uint64(5*time.Minute), packetData) + + resp := s.BroadcastMessages( + ctx, + chainA, + controllerAccount, + msgSendTx, + ) + + s.AssertTxSuccess(resp) + + // time for the packet to be relayed + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB)) + }) + + t.Run("verify tokens transferred", func(t *testing.T) { + balance, err := s.QueryBalance(ctx, chainB, chainBAccount.FormattedAddress(), chainB.Config().Denom) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64()) + }) +} + +/* +TODO: uncomment when hermes works with upgrades, https://github.com/cosmos/ibc-go/issues/5644 +func (s *InterchainAccountsTestSuite) TestMsgSendTx_SuccessfulTransfer_AfterUpgradingOrdertoUnordered() { + t := s.T() + ctx := context.TODO() + + // setup relayers and connection-0 between two chains + // channel-0 is a transfer channel but it will not be used in this test case + relayer, _ := s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, _ := s.GetChains() + + // setup 2 accounts: controller account on chain A, a second chain B account. + // host account will be created when the ICA is registered + controllerAccount := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + controllerAddress := controllerAccount.FormattedAddress() + // chainBAccount := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + var ( + portID string + hostAccount string + + initialChannelID = "channel-1" + ) + + t.Run("register interchain account", func(t *testing.T) { + var err error + // explicitly set the version string because we don't want to use incentivized channels. + version := icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID) + msgRegisterInterchainAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, controllerAddress, version, channeltypes.ORDERED) + s.RegisterInterchainAccount(ctx, chainA, controllerAccount, msgRegisterInterchainAccount) + portID, err = icatypes.NewControllerPortID(controllerAddress) + s.Require().NoError(err) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("verify interchain account", func(t *testing.T) { + var err error + hostAccount, err = s.QueryInterchainAccount(ctx, chainA, controllerAddress, ibctesting.FirstConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(hostAccount)) + + _, err = s.QueryChannel(ctx, chainA, portID, initialChannelID) + s.Require().NoError(err) + }) + + // t.Run("fund interchain account wallet", func(t *testing.T) { + // // fund the host account account so it has some $$ to send + // err := chainB.SendFunds(ctx, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + // Address: hostAccount, + // Amount: sdkmath.NewInt(testvalues.StartingTokenAmount), + // Denom: chainB.Config().Denom, + // }) + // s.Require().NoError(err) + // }) + + // t.Run("broadcast MsgSendTx", func(t *testing.T) { + // // assemble bank transfer message from host account to user account on host chain + // msgSend := &banktypes.MsgSend{ + // FromAddress: hostAccount, + // ToAddress: chainBAccount.FormattedAddress(), + // Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainB.Config().Denom)), + // } + + // cdc := testsuite.Codec() + + // bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + // s.Require().NoError(err) + + // packetData := icatypes.InterchainAccountPacketData{ + // Type: icatypes.EXECUTE_TX, + // Data: bz, + // Memo: "e2e", + // } + + // msgSendTx := controllertypes.NewMsgSendTx(controllerAddress, ibctesting.FirstConnectionID, uint64(5*time.Minute), packetData) + + // resp := s.BroadcastMessages( + // ctx, + // chainA, + // controllerAccount, + // msgSendTx, + // ) + + // s.AssertTxSuccess(resp) + + // // time for the packet to be relayed + // s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB)) + // }) + + // t.Run("verify tokens transferred", func(t *testing.T) { + // balance, err := s.QueryBalance(ctx, chainB, chainBAccount.FormattedAddress(), chainB.Config().Denom) + // s.Require().NoError(err) + + // expected := testvalues.IBCTransferAmount + testvalues.StartingTokenAmount + // s.Require().Equal(expected, balance.Int64()) + // }) + + channel, err := s.QueryChannel(ctx, chainA, portID, initialChannelID) + s.Require().NoError(err) + + // upgrade the channel ordering to UNORDERED + upgradeFields := channeltypes.NewUpgradeFields(channeltypes.UNORDERED, channel.ConnectionHops, channel.Version) + + t.Run("execute gov proposal to initiate channel upgrade", func(t *testing.T) { + govModuleAddress, err := s.QueryModuleAccountAddress(ctx, govtypes.ModuleName, chainA) + s.Require().NoError(err) + s.Require().NotNil(govModuleAddress) + + msg := channeltypes.NewMsgChannelUpgradeInit(portID, initialChannelID, upgradeFields, govModuleAddress.String()) + s.ExecuteAndPassGovV1Proposal(ctx, msg, chainA, controllerAccount) + }) + + t.Run("verify channel A upgraded and is now unordered", func(t *testing.T) { + var channel channeltypes.Channel + waitErr := test.WaitForCondition(time.Minute*2, time.Second*5, func() (bool, error) { + channel, err = s.QueryChannel(ctx, chainA, portID, initialChannelID) + if err != nil { + return false, err + } + return channel.Ordering == channeltypes.UNORDERED, nil + }) + s.Require().NoErrorf(waitErr, "channel was not upgraded: expected %s got %s", channeltypes.UNORDERED, channel.Ordering) + }) +} +*/ diff --git a/e2e/tests/interchain_accounts/gov_test.go b/e2e/tests/interchain_accounts/gov_test.go new file mode 100644 index 00000000000..212b5d173f4 --- /dev/null +++ b/e2e/tests/interchain_accounts/gov_test.go @@ -0,0 +1,122 @@ +//go:build !test_e2e + +package interchainaccounts + +import ( + "context" + "testing" + "time" + + "github.com/cosmos/gogoproto/proto" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + test "github.com/strangelove-ventures/interchaintest/v8/testutil" + testifysuite "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + controllertypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/controller/types" + icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +func TestInterchainAccountsGovTestSuite(t *testing.T) { + testifysuite.Run(t, new(InterchainAccountsGovTestSuite)) +} + +type InterchainAccountsGovTestSuite struct { + testsuite.E2ETestSuite +} + +func (s *InterchainAccountsGovTestSuite) TestInterchainAccountsGovIntegration() { + t := s.T() + ctx := context.TODO() + + // setup relayers and connection-0 between two chains + // channel-0 is a transfer channel but it will not be used in this test case + relayer, _ := s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, chainB := s.GetChains() + controllerAccount := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + chainBAccount := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + chainBAddress := chainBAccount.FormattedAddress() + + govModuleAddress, err := s.QueryModuleAccountAddress(ctx, govtypes.ModuleName, chainA) + s.Require().NoError(err) + s.Require().NotNil(govModuleAddress) + + t.Run("execute proposal for MsgRegisterInterchainAccount", func(t *testing.T) { + version := icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID) + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, govModuleAddress.String(), version, channeltypes.ORDERED) + s.ExecuteAndPassGovV1Proposal(ctx, msgRegisterAccount, chainA, controllerAccount) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB)) + + var interchainAccAddr string + t.Run("verify interchain account registration success", func(t *testing.T) { + var err error + interchainAccAddr, err = s.QueryInterchainAccount(ctx, chainA, govModuleAddress.String(), ibctesting.FirstConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddr)) + + channels, err := relayer.GetChannels(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID) + s.Require().NoError(err) + s.Require().Equal(len(channels), 2) + }) + + t.Run("interchain account executes a bank transfer on behalf of the corresponding owner account", func(t *testing.T) { + t.Run("fund interchain account wallet", func(t *testing.T) { + // fund the host account, so it has some $$ to send + err := chainB.SendFunds(ctx, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Address: interchainAccAddr, + Amount: sdkmath.NewInt(testvalues.StartingTokenAmount), + Denom: chainB.Config().Denom, + }) + s.Require().NoError(err) + }) + + t.Run("execute proposal for MsgSendTx", func(t *testing.T) { + msgBankSend := &banktypes.MsgSend{ + FromAddress: interchainAccAddr, + ToAddress: chainBAddress, + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainB.Config().Denom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgBankSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(govModuleAddress.String(), ibctesting.FirstConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + s.ExecuteAndPassGovV1Proposal(ctx, msgSendTx, chainA, controllerAccount) + }) + + t.Run("verify tokens transferred", func(t *testing.T) { + balance, err := s.QueryBalance(ctx, chainB, chainBAccount.FormattedAddress(), chainB.Config().Denom) + s.Require().NoError(err) + + _, err = s.QueryBalance(ctx, chainB, interchainAccAddr, chainB.Config().Denom) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64()) + }) + }) +} diff --git a/e2e/tests/interchain_accounts/groups_test.go b/e2e/tests/interchain_accounts/groups_test.go new file mode 100644 index 00000000000..43faca17a3f --- /dev/null +++ b/e2e/tests/interchain_accounts/groups_test.go @@ -0,0 +1,215 @@ +//go:build !test_e2e + +package interchainaccounts + +import ( + "context" + "testing" + "time" + + "github.com/cosmos/gogoproto/proto" + interchaintest "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + test "github.com/strangelove-ventures/interchaintest/v8/testutil" + testifysuite "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + grouptypes "github.com/cosmos/cosmos-sdk/x/group" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + controllertypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/controller/types" + icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +const ( + // DefaultGroupMemberWeight is the members voting weight. + // A group members weight is used in the sum of `YES` votes required to meet a decision policy threshold. + DefaultGroupMemberWeight = "1" + + // DefaultGroupThreshold is the minimum weighted sum of `YES` votes that must be met or + // exceeded for a proposal to succeed. + DefaultGroupThreshold = "1" + + // DefaultMetadata defines a reusable metadata string for testing purposes + DefaultMetadata = "custom metadata" + + // DefaultMinExecutionPeriod is the minimum duration after the proposal submission + // where members can start sending MsgExec. This means that the window for + // sending a MsgExec transaction is: + // `[ submission + min_execution_period ; submission + voting_period + max_execution_period]` + // where max_execution_period is a app-specific config, defined in the keeper. + // If not set, min_execution_period will default to 0. + DefaultMinExecutionPeriod = time.Duration(0) + + // DefaultVotingPeriod is the duration from submission of a proposal to the end of voting period + // Within this times votes can be submitted with MsgVote. + DefaultVotingPeriod = time.Minute + + // InitialGroupID is the first group ID generated by x/group + InitialGroupID = 1 + + // InitialProposalID is the first group proposal ID generated by x/group + InitialProposalID = 1 +) + +func TestInterchainAccountsGroupsTestSuite(t *testing.T) { + testifysuite.Run(t, new(InterchainAccountsGroupsTestSuite)) +} + +type InterchainAccountsGroupsTestSuite struct { + testsuite.E2ETestSuite +} + +func (s *InterchainAccountsGroupsTestSuite) QueryGroupPolicyAddress(ctx context.Context, chain ibc.Chain) string { + queryClient := s.GetChainGRCPClients(chain).GroupsQueryClient + res, err := queryClient.GroupPoliciesByGroup(ctx, &grouptypes.QueryGroupPoliciesByGroupRequest{ + GroupId: InitialGroupID, // always use the initial group id + }) + s.Require().NoError(err) + + return res.GroupPolicies[0].Address +} + +func (s *InterchainAccountsGroupsTestSuite) TestInterchainAccountsGroupsIntegration() { + t := s.T() + ctx := context.TODO() + + var ( + groupPolicyAddr string + interchainAccAddr string + err error + ) + + // setup relayers and connection-0 between two chains + // channel-0 is a transfer channel but it will not be used in this test case + relayer, _ := s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, chainB := s.GetChains() + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainAAddress := chainAWallet.FormattedAddress() + + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + chainBAddress := chainBWallet.FormattedAddress() + + t.Run("create group with new threshold decision policy", func(t *testing.T) { + members := []grouptypes.MemberRequest{ + { + Address: chainAAddress, + Weight: DefaultGroupMemberWeight, + }, + } + + decisionPolicy := grouptypes.NewThresholdDecisionPolicy(DefaultGroupThreshold, DefaultVotingPeriod, DefaultMinExecutionPeriod) + msgCreateGroupWithPolicy, err := grouptypes.NewMsgCreateGroupWithPolicy(chainAAddress, members, DefaultMetadata, DefaultMetadata, true, decisionPolicy) + s.Require().NoError(err) + + txResp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgCreateGroupWithPolicy) + s.AssertTxSuccess(txResp) + }) + + t.Run("submit proposal for MsgRegisterInterchainAccount", func(t *testing.T) { + groupPolicyAddr = s.QueryGroupPolicyAddress(ctx, chainA) + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, groupPolicyAddr, icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID), channeltypes.ORDERED) + + msgSubmitProposal, err := grouptypes.NewMsgSubmitProposal(groupPolicyAddr, []string{chainAAddress}, []sdk.Msg{msgRegisterAccount}, DefaultMetadata, grouptypes.Exec_EXEC_UNSPECIFIED, "e2e groups proposal: for MsgRegisterInterchainAccount", "e2e groups proposal: for MsgRegisterInterchainAccount") + s.Require().NoError(err) + + txResp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgSubmitProposal) + s.AssertTxSuccess(txResp) + }) + + t.Run("vote and exec proposal", func(t *testing.T) { + msgVote := &grouptypes.MsgVote{ + ProposalId: InitialProposalID, + Voter: chainAAddress, + Option: grouptypes.VOTE_OPTION_YES, + Exec: grouptypes.Exec_EXEC_TRY, + } + + txResp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgVote) + s.AssertTxSuccess(txResp) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("verify interchain account registration success", func(t *testing.T) { + interchainAccAddr, err = s.QueryInterchainAccount(ctx, chainA, groupPolicyAddr, ibctesting.FirstConnectionID) + s.Require().NotEmpty(interchainAccAddr) + s.Require().NoError(err) + + channels, err := relayer.GetChannels(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID) + s.Require().NoError(err) + s.Require().Equal(len(channels), 2) // 1 transfer (created by default), 1 interchain-accounts + }) + + t.Run("fund interchain account wallet", func(t *testing.T) { + err := chainB.SendFunds(ctx, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Address: interchainAccAddr, + Amount: sdkmath.NewInt(testvalues.StartingTokenAmount), + Denom: chainB.Config().Denom, + }) + s.Require().NoError(err) + }) + + t.Run("submit proposal for MsgSendTx", func(t *testing.T) { + msgBankSend := &banktypes.MsgSend{ + FromAddress: interchainAccAddr, + ToAddress: chainBAddress, + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainB.Config().Denom)), + } + + cdc := testsuite.Codec() + + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgBankSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSubmitTx := controllertypes.NewMsgSendTx(groupPolicyAddr, ibctesting.FirstConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + msgSubmitProposal, err := grouptypes.NewMsgSubmitProposal(groupPolicyAddr, []string{chainAAddress}, []sdk.Msg{msgSubmitTx}, DefaultMetadata, grouptypes.Exec_EXEC_UNSPECIFIED, "e2e groups proposal: for MsgRegisterInterchainAccount", "e2e groups proposal: for MsgRegisterInterchainAccount") + s.Require().NoError(err) + + txResp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgSubmitProposal) + s.AssertTxSuccess(txResp) + }) + + t.Run("vote and exec proposal", func(t *testing.T) { + msgVote := &grouptypes.MsgVote{ + ProposalId: InitialProposalID + 1, + Voter: chainAAddress, + Option: grouptypes.VOTE_OPTION_YES, + Exec: grouptypes.Exec_EXEC_TRY, + } + + txResp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgVote) + s.AssertTxSuccess(txResp) + }) + + t.Run("verify tokens transferred", func(t *testing.T) { + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB), "failed to wait for blocks") + balance, err := s.QueryBalance(ctx, chainB, chainBAddress, chainB.Config().Denom) + + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64()) + + balance, err = s.QueryBalance(ctx, chainB, interchainAccAddr, chainB.Config().Denom) + s.Require().NoError(err) + + expected = testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, balance.Int64()) + }) +} diff --git a/e2e/tests/interchain_accounts/incentivized_test.go b/e2e/tests/interchain_accounts/incentivized_test.go new file mode 100644 index 00000000000..309a8e92b8f --- /dev/null +++ b/e2e/tests/interchain_accounts/incentivized_test.go @@ -0,0 +1,383 @@ +//go:build !test_e2e + +package interchainaccounts + +import ( + "context" + "testing" + "time" + + "github.com/cosmos/gogoproto/proto" + interchaintest "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + test "github.com/strangelove-ventures/interchaintest/v8/testutil" + testifysuite "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + controllertypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/controller/types" + icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +func TestIncentivizedInterchainAccountsTestSuite(t *testing.T) { + testifysuite.Run(t, new(IncentivizedInterchainAccountsTestSuite)) +} + +type IncentivizedInterchainAccountsTestSuite struct { + InterchainAccountsTestSuite +} + +func (s *IncentivizedInterchainAccountsTestSuite) TestMsgSendTx_SuccessfulBankSend_Incentivized() { + t := s.T() + ctx := context.TODO() + + // setup relayers and connection-0 between two chains + // channel-0 is a transfer channel but it will not be used in this test case + relayer, _ := s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, chainB := s.GetChains() + + var ( + chainADenom = chainA.Config().Denom + interchainAcc = "" + testFee = testvalues.DefaultFee(chainADenom) + ) + + t.Run("relayer wallets recovered", func(t *testing.T) { + err := s.RecoverRelayerWallets(ctx, relayer) + s.Require().NoError(err) + }) + + chainARelayerWallet, chainBRelayerWallet, err := s.GetRelayerWallets(relayer) + t.Run("relayer wallets fetched", func(t *testing.T) { + s.Require().NoError(err) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") + + chainARelayerUser, chainBRelayerUser := s.GetRelayerUsers(ctx) + relayerAStartingBalance, err := s.GetChainANativeBalance(ctx, chainARelayerUser) + s.Require().NoError(err) + t.Logf("relayer A user starting with balance: %d", relayerAStartingBalance) + + controllerAccount := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainBAccount := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + t.Run("broadcast MsgRegisterInterchainAccount", func(t *testing.T) { + version := "" // allow version to be specified by the controller chain since both chains should support incentivized channels + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, controllerAccount.FormattedAddress(), version, channeltypes.ORDERED) + + txResp := s.BroadcastMessages(ctx, chainA, controllerAccount, msgRegisterAccount) + s.AssertTxSuccess(txResp) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + var channelOutput ibc.ChannelOutput + t.Run("verify interchain account", func(t *testing.T) { + var err error + interchainAcc, err = s.QueryInterchainAccount(ctx, chainA, controllerAccount.FormattedAddress(), ibctesting.FirstConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAcc)) + + channels, err := relayer.GetChannels(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID) + s.Require().NoError(err) + s.Require().Equal(len(channels), 2) + + // interchain accounts channel at index: 0 + channelOutput = channels[0] + + s.Require().NoError(test.WaitForBlocks(ctx, 2, chainA, chainB)) + }) + + t.Run("execute interchain account bank send through controller", func(t *testing.T) { + t.Run("fund interchain account wallet on host chainB", func(t *testing.T) { + // fund the interchain account so it has some $$ to send + err := chainB.SendFunds(ctx, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Address: interchainAcc, + Amount: sdkmath.NewInt(testvalues.StartingTokenAmount), + Denom: chainB.Config().Denom, + }) + s.Require().NoError(err) + }) + + t.Run("register counterparty payee", func(t *testing.T) { + resp := s.RegisterCounterPartyPayee(ctx, chainB, chainBRelayerUser, channelOutput.Counterparty.PortID, channelOutput.Counterparty.ChannelID, chainBRelayerWallet.FormattedAddress(), chainARelayerWallet.FormattedAddress()) + s.AssertTxSuccess(resp) + }) + + t.Run("verify counterparty payee", func(t *testing.T) { + address, err := s.QueryCounterPartyPayee(ctx, chainB, chainBRelayerWallet.FormattedAddress(), channelOutput.Counterparty.ChannelID) + s.Require().NoError(err) + s.Require().Equal(chainARelayerWallet.FormattedAddress(), address) + }) + + t.Run("no incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelOutput.PortID, channelOutput.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + t.Run("stop relayer", func(t *testing.T) { + s.StopRelayer(ctx, relayer) + }) + + t.Run("broadcast incentivized MsgSendTx", func(t *testing.T) { + msgPayPacketFee := &feetypes.MsgPayPacketFee{ + Fee: testvalues.DefaultFee(chainADenom), + SourcePortId: channelOutput.PortID, + SourceChannelId: channelOutput.ChannelID, + Signer: controllerAccount.FormattedAddress(), + } + + msgSend := &banktypes.MsgSend{ + FromAddress: interchainAcc, + ToAddress: chainBAccount.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainB.Config().Denom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(controllerAccount.FormattedAddress(), ibctesting.FirstConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + + resp := s.BroadcastMessages(ctx, chainA, controllerAccount, msgPayPacketFee, msgSendTx) + s.AssertTxSuccess(resp) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB)) + }) + + t.Run("there should be incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelOutput.PortID, channelOutput.ChannelID) + s.Require().NoError(err) + s.Require().Len(packets, 1) + actualFee := packets[0].PacketFees[0].Fee + + s.Require().True(actualFee.RecvFee.Equal(testFee.RecvFee)) + s.Require().True(actualFee.AckFee.Equal(testFee.AckFee)) + s.Require().True(actualFee.TimeoutFee.Equal(testFee.TimeoutFee)) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("packets are relayed", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelOutput.PortID, channelOutput.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + t.Run("verify interchain account sent tokens", func(t *testing.T) { + balance, err := s.QueryBalance(ctx, chainB, chainBAccount.FormattedAddress(), chainB.Config().Denom) + s.Require().NoError(err) + + _, err = s.QueryBalance(ctx, chainB, interchainAcc, chainB.Config().Denom) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64()) + }) + + t.Run("timeout fee is refunded", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, controllerAccount) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testFee.AckFee.AmountOf(chainADenom).Int64() - testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("relayerA is paid ack and recv fee", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainARelayerUser) + s.Require().NoError(err) + + expected := relayerAStartingBalance + testFee.AckFee.AmountOf(chainADenom).Int64() + testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + }) +} + +func (s *IncentivizedInterchainAccountsTestSuite) TestMsgSendTx_FailedBankSend_Incentivized() { + t := s.T() + ctx := context.TODO() + + // setup relayers and connection-0 between two chains + // channel-0 is a transfer channel but it will not be used in this test case + relayer, _ := s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, chainB := s.GetChains() + + var ( + chainADenom = chainA.Config().Denom + interchainAcc = "" + testFee = testvalues.DefaultFee(chainADenom) + ) + + t.Run("relayer wallets recovered", func(t *testing.T) { + err := s.RecoverRelayerWallets(ctx, relayer) + s.Require().NoError(err) + }) + + chainARelayerWallet, chainBRelayerWallet, err := s.GetRelayerWallets(relayer) + t.Run("relayer wallets fetched", func(t *testing.T) { + s.Require().NoError(err) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") + + chainARelayerUser, chainBRelayerUser := s.GetRelayerUsers(ctx) + relayerAStartingBalance, err := s.GetChainANativeBalance(ctx, chainARelayerUser) + s.Require().NoError(err) + t.Logf("relayer A user starting with balance: %d", relayerAStartingBalance) + + controllerAccount := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainBAccount := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + t.Run("broadcast MsgRegisterInterchainAccount", func(t *testing.T) { + version := "" // allow version to be specified by the controller chain since both chains should support incentivized channels + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, controllerAccount.FormattedAddress(), version, channeltypes.ORDERED) + + txResp := s.BroadcastMessages(ctx, chainA, controllerAccount, msgRegisterAccount) + s.AssertTxSuccess(txResp) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + var channelOutput ibc.ChannelOutput + t.Run("verify interchain account", func(t *testing.T) { + var err error + interchainAcc, err = s.QueryInterchainAccount(ctx, chainA, controllerAccount.FormattedAddress(), ibctesting.FirstConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAcc)) + + channels, err := relayer.GetChannels(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID) + s.Require().NoError(err) + s.Require().Equal(len(channels), 2) + + // interchain accounts channel at index: 0 + channelOutput = channels[0] + + s.Require().NoError(test.WaitForBlocks(ctx, 2, chainA, chainB)) + }) + + t.Run("execute interchain account bank send through controller", func(t *testing.T) { + t.Run("register counterparty payee", func(t *testing.T) { + resp := s.RegisterCounterPartyPayee(ctx, chainB, chainBRelayerUser, channelOutput.Counterparty.PortID, channelOutput.Counterparty.ChannelID, chainBRelayerWallet.FormattedAddress(), chainARelayerWallet.FormattedAddress()) + s.AssertTxSuccess(resp) + }) + + t.Run("verify counterparty payee", func(t *testing.T) { + address, err := s.QueryCounterPartyPayee(ctx, chainB, chainBRelayerWallet.FormattedAddress(), channelOutput.Counterparty.ChannelID) + s.Require().NoError(err) + s.Require().Equal(chainARelayerWallet.FormattedAddress(), address) + }) + + t.Run("no incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelOutput.PortID, channelOutput.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + t.Run("stop relayer", func(t *testing.T) { + err := relayer.StopRelayer(ctx, s.GetRelayerExecReporter()) + s.Require().NoError(err) + }) + + t.Run("broadcast incentivized MsgSendTx", func(t *testing.T) { + msgPayPacketFee := &feetypes.MsgPayPacketFee{ + Fee: testvalues.DefaultFee(chainADenom), + SourcePortId: channelOutput.PortID, + SourceChannelId: channelOutput.ChannelID, + Signer: controllerAccount.FormattedAddress(), + } + + msgSend := &banktypes.MsgSend{ + FromAddress: interchainAcc, + ToAddress: chainBAccount.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainB.Config().Denom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(controllerAccount.FormattedAddress(), ibctesting.FirstConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + + resp := s.BroadcastMessages(ctx, chainA, controllerAccount, msgPayPacketFee, msgSendTx) + s.AssertTxSuccess(resp) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB)) + }) + + t.Run("there should be incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelOutput.PortID, channelOutput.ChannelID) + s.Require().NoError(err) + s.Require().Len(packets, 1) + actualFee := packets[0].PacketFees[0].Fee + + s.Require().True(actualFee.RecvFee.Equal(testFee.RecvFee)) + s.Require().True(actualFee.AckFee.Equal(testFee.AckFee)) + s.Require().True(actualFee.TimeoutFee.Equal(testFee.TimeoutFee)) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("packets are relayed", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelOutput.PortID, channelOutput.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + t.Run("verify interchain account did not send tokens", func(t *testing.T) { + balance, err := s.QueryBalance(ctx, chainB, chainBAccount.FormattedAddress(), chainB.Config().Denom) + s.Require().NoError(err) + + _, err = s.QueryBalance(ctx, chainB, interchainAcc, chainB.Config().Denom) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64(), "tokens should not have been sent as interchain account was not funded") + }) + + t.Run("timeout fee is refunded", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, controllerAccount) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testFee.AckFee.AmountOf(chainADenom).Int64() - testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("relayerA is paid ack and recv fee", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainARelayerUser) + s.Require().NoError(err) + + expected := relayerAStartingBalance + testFee.AckFee.AmountOf(chainADenom).Int64() + testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + }) +} diff --git a/e2e/tests/interchain_accounts/localhost_test.go b/e2e/tests/interchain_accounts/localhost_test.go new file mode 100644 index 00000000000..d486041de2e --- /dev/null +++ b/e2e/tests/interchain_accounts/localhost_test.go @@ -0,0 +1,477 @@ +//go:build !test_e2e + +package interchainaccounts + +import ( + "context" + "testing" + "time" + + "github.com/cosmos/gogoproto/proto" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + test "github.com/strangelove-ventures/interchaintest/v8/testutil" + testifysuite "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + controllertypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/controller/types" + icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + "github.com/cosmos/ibc-go/v8/modules/core/exported" + localhost "github.com/cosmos/ibc-go/v8/modules/light-clients/09-localhost" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +func TestInterchainAccountsLocalhostTestSuite(t *testing.T) { + testifysuite.Run(t, new(LocalhostInterchainAccountsTestSuite)) +} + +type LocalhostInterchainAccountsTestSuite struct { + testsuite.E2ETestSuite +} + +func (s *LocalhostInterchainAccountsTestSuite) TestInterchainAccounts_Localhost() { + t := s.T() + ctx := context.TODO() + + _, _ = s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, _ := s.GetChains() + + chainADenom := chainA.Config().Denom + + rlyWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userBWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + var ( + msgChanOpenInitRes channeltypes.MsgChannelOpenInitResponse + msgChanOpenTryRes channeltypes.MsgChannelOpenTryResponse + ack []byte + packet channeltypes.Packet + ) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA), "failed to wait for blocks") + + version := icatypes.NewDefaultMetadataString(exported.LocalhostConnectionID, exported.LocalhostConnectionID) + controllerPortID, err := icatypes.NewControllerPortID(userAWallet.FormattedAddress()) + s.Require().NoError(err) + + t.Run("channel open init localhost - broadcast MsgRegisterInterchainAccount", func(t *testing.T) { + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(exported.LocalhostConnectionID, userAWallet.FormattedAddress(), version, channeltypes.ORDERED) + + txResp := s.BroadcastMessages(ctx, chainA, userAWallet, msgRegisterAccount) + s.AssertTxSuccess(txResp) + + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenInitRes)) + }) + + t.Run("channel open try localhost", func(t *testing.T) { + msgChanOpenTry := channeltypes.NewMsgChannelOpenTry( + icatypes.HostPortID, icatypes.Version, + channeltypes.ORDERED, []string{exported.LocalhostConnectionID}, + controllerPortID, msgChanOpenInitRes.ChannelId, + version, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenTry) + s.AssertTxSuccess(txResp) + + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenTryRes)) + }) + + t.Run("channel open ack localhost", func(t *testing.T) { + msgChanOpenAck := channeltypes.NewMsgChannelOpenAck( + controllerPortID, msgChanOpenInitRes.ChannelId, + msgChanOpenTryRes.ChannelId, msgChanOpenTryRes.Version, + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenAck) + s.AssertTxSuccess(txResp) + }) + + t.Run("channel open confirm localhost", func(t *testing.T) { + msgChanOpenConfirm := channeltypes.NewMsgChannelOpenConfirm( + icatypes.HostPortID, msgChanOpenTryRes.ChannelId, + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenConfirm) + s.AssertTxSuccess(txResp) + }) + + t.Run("query localhost interchain accounts channel ends", func(t *testing.T) { + channelEndA, err := s.QueryChannel(ctx, chainA, controllerPortID, msgChanOpenInitRes.ChannelId) + s.Require().NoError(err) + s.Require().NotNil(channelEndA) + + channelEndB, err := s.QueryChannel(ctx, chainA, icatypes.HostPortID, msgChanOpenTryRes.ChannelId) + s.Require().NoError(err) + s.Require().NotNil(channelEndB) + + s.Require().Equal(channelEndA.GetConnectionHops(), channelEndB.GetConnectionHops()) + }) + + t.Run("verify interchain account registration and deposit funds", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + walletAmount := ibc.WalletAmount{ + Address: interchainAccAddress, + Amount: sdkmath.NewInt(testvalues.StartingTokenAmount), + Denom: chainADenom, + } + + s.Require().NoError(chainA.SendFunds(ctx, interchaintest.FaucetAccountKeyName, walletAmount)) + }) + + t.Run("send packet localhost interchain accounts", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + msgSend := &banktypes.MsgSend{ + FromAddress: interchainAccAddress, + ToAddress: userBWallet.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainADenom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(userAWallet.FormattedAddress(), exported.LocalhostConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + + txResp := s.BroadcastMessages(ctx, chainA, userAWallet, msgSendTx) + s.AssertTxSuccess(txResp) + + packet, err = ibctesting.ParsePacketFromEvents(txResp.Events) + s.Require().NoError(err) + s.Require().NotNil(packet) + }) + + t.Run("recv packet localhost interchain accounts", func(t *testing.T) { + msgRecvPacket := channeltypes.NewMsgRecvPacket(packet, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgRecvPacket) + s.AssertTxSuccess(txResp) + + ack, err = ibctesting.ParseAckFromEvents(txResp.Events) + s.Require().NoError(err) + s.Require().NotNil(ack) + }) + + t.Run("acknowledge packet localhost interchain accounts", func(t *testing.T) { + msgAcknowledgement := channeltypes.NewMsgAcknowledgement(packet, ack, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgAcknowledgement) + s.AssertTxSuccess(txResp) + }) + + t.Run("verify tokens transferred", func(t *testing.T) { + balance, err := s.QueryBalance(ctx, chainA, userBWallet.FormattedAddress(), chainADenom) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64()) + }) +} + +func (s *LocalhostInterchainAccountsTestSuite) TestInterchainAccounts_ReopenChannel_Localhost() { + t := s.T() + ctx := context.TODO() + + // relayer and channel output is discarded, only a single chain is required + _, _ = s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, _ := s.GetChains() + + chainADenom := chainA.Config().Denom + + rlyWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userBWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + var ( + msgChanOpenInitRes channeltypes.MsgChannelOpenInitResponse + msgChanOpenTryRes channeltypes.MsgChannelOpenTryResponse + ack []byte + packet channeltypes.Packet + ) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA), "failed to wait for blocks") + + version := icatypes.NewDefaultMetadataString(exported.LocalhostConnectionID, exported.LocalhostConnectionID) + controllerPortID, err := icatypes.NewControllerPortID(userAWallet.FormattedAddress()) + s.Require().NoError(err) + + t.Run("channel open init localhost - broadcast MsgRegisterInterchainAccount", func(t *testing.T) { + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(exported.LocalhostConnectionID, userAWallet.FormattedAddress(), version, channeltypes.ORDERED) + + txResp := s.BroadcastMessages(ctx, chainA, userAWallet, msgRegisterAccount) + s.AssertTxSuccess(txResp) + + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenInitRes)) + }) + + t.Run("channel open try localhost", func(t *testing.T) { + msgChanOpenTry := channeltypes.NewMsgChannelOpenTry( + icatypes.HostPortID, icatypes.Version, + channeltypes.ORDERED, []string{exported.LocalhostConnectionID}, + controllerPortID, msgChanOpenInitRes.ChannelId, + version, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenTry) + s.AssertTxSuccess(txResp) + + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenTryRes)) + }) + + t.Run("channel open ack localhost", func(t *testing.T) { + msgChanOpenAck := channeltypes.NewMsgChannelOpenAck( + controllerPortID, msgChanOpenInitRes.ChannelId, + msgChanOpenTryRes.ChannelId, msgChanOpenTryRes.Version, + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenAck) + s.AssertTxSuccess(txResp) + }) + + t.Run("channel open confirm localhost", func(t *testing.T) { + msgChanOpenConfirm := channeltypes.NewMsgChannelOpenConfirm( + icatypes.HostPortID, msgChanOpenTryRes.ChannelId, + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenConfirm) + s.AssertTxSuccess(txResp) + }) + + t.Run("query localhost interchain accounts channel ends", func(t *testing.T) { + channelEndA, err := s.QueryChannel(ctx, chainA, controllerPortID, msgChanOpenInitRes.ChannelId) + s.Require().NoError(err) + s.Require().NotNil(channelEndA) + + channelEndB, err := s.QueryChannel(ctx, chainA, icatypes.HostPortID, msgChanOpenTryRes.ChannelId) + s.Require().NoError(err) + s.Require().NotNil(channelEndB) + + s.Require().Equal(channelEndA.GetConnectionHops(), channelEndB.GetConnectionHops()) + }) + + t.Run("verify interchain account registration and deposit funds", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + walletAmount := ibc.WalletAmount{ + Address: interchainAccAddress, + Amount: sdkmath.NewInt(testvalues.StartingTokenAmount), + Denom: chainADenom, + } + + s.Require().NoError(chainA.SendFunds(ctx, interchaintest.FaucetAccountKeyName, walletAmount)) + }) + + t.Run("send localhost interchain accounts packet with timeout", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + msgSend := &banktypes.MsgSend{ + FromAddress: interchainAccAddress, + ToAddress: userBWallet.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainADenom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(userAWallet.FormattedAddress(), exported.LocalhostConnectionID, uint64(1), packetData) + + txResp := s.BroadcastMessages(ctx, chainA, userAWallet, msgSendTx) + s.AssertTxSuccess(txResp) + + packet, err = ibctesting.ParsePacketFromEvents(txResp.Events) + s.Require().NoError(err) + s.Require().NotNil(packet) + }) + + t.Run("timeout localhost interchain accounts packet", func(t *testing.T) { + msgTimeout := channeltypes.NewMsgTimeout(packet, 1, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgTimeout) + s.AssertTxSuccess(txResp) + }) + + t.Run("close interchain accounts host channel end", func(t *testing.T) { + // Pass in zero for counterpartyUpgradeSequence given that channel has not undergone any upgrades. + msgCloseConfirm := channeltypes.NewMsgChannelCloseConfirm(icatypes.HostPortID, msgChanOpenTryRes.ChannelId, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), 0) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgCloseConfirm) + s.AssertTxSuccess(txResp) + }) + + t.Run("verify localhost interchain accounts channel is closed", func(t *testing.T) { + channelEndA, err := s.QueryChannel(ctx, chainA, controllerPortID, msgChanOpenInitRes.ChannelId) + s.Require().NoError(err) + + s.Require().Equal(channeltypes.CLOSED, channelEndA.State, "the channel was not in an expected state") + + channelEndB, err := s.QueryChannel(ctx, chainA, icatypes.HostPortID, msgChanOpenTryRes.ChannelId) + s.Require().NoError(err) + + s.Require().Equal(channeltypes.CLOSED, channelEndB.State, "the channel was not in an expected state") + }) + + t.Run("channel open init localhost: create new channel for existing account", func(t *testing.T) { + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(exported.LocalhostConnectionID, userAWallet.FormattedAddress(), version, channeltypes.ORDERED) + + txResp := s.BroadcastMessages(ctx, chainA, userAWallet, msgRegisterAccount) + s.AssertTxSuccess(txResp) + + // note: response values are updated here in msgChanOpenInitRes + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenInitRes)) + }) + + t.Run("channel open try localhost", func(t *testing.T) { + msgChanOpenTry := channeltypes.NewMsgChannelOpenTry( + icatypes.HostPortID, icatypes.Version, + channeltypes.ORDERED, []string{exported.LocalhostConnectionID}, + controllerPortID, msgChanOpenInitRes.ChannelId, + version, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenTry) + s.AssertTxSuccess(txResp) + + // note: response values are updated here in msgChanOpenTryRes + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &msgChanOpenTryRes)) + }) + + t.Run("channel open ack localhost", func(t *testing.T) { + msgChanOpenAck := channeltypes.NewMsgChannelOpenAck( + controllerPortID, msgChanOpenInitRes.ChannelId, + msgChanOpenTryRes.ChannelId, msgChanOpenTryRes.Version, + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenAck) + s.AssertTxSuccess(txResp) + }) + + t.Run("channel open confirm localhost", func(t *testing.T) { + msgChanOpenConfirm := channeltypes.NewMsgChannelOpenConfirm( + icatypes.HostPortID, msgChanOpenTryRes.ChannelId, + localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgChanOpenConfirm) + s.AssertTxSuccess(txResp) + }) + + t.Run("query localhost interchain accounts channel ends", func(t *testing.T) { + channelEndA, err := s.QueryChannel(ctx, chainA, controllerPortID, msgChanOpenInitRes.ChannelId) + s.Require().NoError(err) + s.Require().NotNil(channelEndA) + + channelEndB, err := s.QueryChannel(ctx, chainA, icatypes.HostPortID, msgChanOpenTryRes.ChannelId) + s.Require().NoError(err) + s.Require().NotNil(channelEndB) + + s.Require().Equal(channelEndA.GetConnectionHops(), channelEndB.GetConnectionHops()) + }) + + t.Run("verify interchain account and existing balance", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + balance, err := s.QueryBalance(ctx, chainA, interchainAccAddress, chainADenom) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64()) + }) + + t.Run("send packet localhost interchain accounts", func(t *testing.T) { + interchainAccAddress, err := s.QueryInterchainAccount(ctx, chainA, userAWallet.FormattedAddress(), exported.LocalhostConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(interchainAccAddress)) + + msgSend := &banktypes.MsgSend{ + FromAddress: interchainAccAddress, + ToAddress: userBWallet.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainADenom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(userAWallet.FormattedAddress(), exported.LocalhostConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + + txResp := s.BroadcastMessages(ctx, chainA, userAWallet, msgSendTx) + s.AssertTxSuccess(txResp) + + packet, err = ibctesting.ParsePacketFromEvents(txResp.Events) + s.Require().NoError(err) + s.Require().NotNil(packet) + }) + + t.Run("recv packet localhost interchain accounts", func(t *testing.T) { + msgRecvPacket := channeltypes.NewMsgRecvPacket(packet, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgRecvPacket) + s.AssertTxSuccess(txResp) + + ack, err = ibctesting.ParseAckFromEvents(txResp.Events) + s.Require().NoError(err) + s.Require().NotNil(ack) + }) + + t.Run("acknowledge packet localhost interchain accounts", func(t *testing.T) { + msgAcknowledgement := channeltypes.NewMsgAcknowledgement(packet, ack, localhost.SentinelProof, clienttypes.ZeroHeight(), rlyWallet.FormattedAddress()) + + txResp := s.BroadcastMessages(ctx, chainA, rlyWallet, msgAcknowledgement) + s.AssertTxSuccess(txResp) + }) + + t.Run("verify tokens transferred", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainA, controllerPortID, msgChanOpenInitRes.ChannelId, 1) + + balance, err := s.QueryBalance(ctx, chainA, userBWallet.FormattedAddress(), chainADenom) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + testvalues.StartingTokenAmount + s.Require().Equal(expected, balance.Int64()) + }) +} diff --git a/e2e/tests/interchain_accounts/params_test.go b/e2e/tests/interchain_accounts/params_test.go new file mode 100644 index 00000000000..4cdc64fa371 --- /dev/null +++ b/e2e/tests/interchain_accounts/params_test.go @@ -0,0 +1,152 @@ +//go:build !test_e2e + +package interchainaccounts + +import ( + "context" + "testing" + + "github.com/strangelove-ventures/interchaintest/v8/ibc" + testifysuite "github.com/stretchr/testify/suite" + + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + paramsproposaltypes "github.com/cosmos/cosmos-sdk/x/params/types/proposal" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + controllertypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/controller/types" + hosttypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/host/types" + icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +func TestInterchainAccountsParamsTestSuite(t *testing.T) { + testifysuite.Run(t, new(InterchainAccountsParamsTestSuite)) +} + +type InterchainAccountsParamsTestSuite struct { + testsuite.E2ETestSuite +} + +// QueryControllerParams queries the params for the controller +func (s *InterchainAccountsParamsTestSuite) QueryControllerParams(ctx context.Context, chain ibc.Chain) controllertypes.Params { + queryClient := s.GetChainGRCPClients(chain).ICAControllerQueryClient + res, err := queryClient.Params(ctx, &controllertypes.QueryParamsRequest{}) + s.Require().NoError(err) + + return *res.Params +} + +// QueryHostParams queries the host chain for the params +func (s *InterchainAccountsParamsTestSuite) QueryHostParams(ctx context.Context, chain ibc.Chain) hosttypes.Params { + queryClient := s.GetChainGRCPClients(chain).ICAHostQueryClient + res, err := queryClient.Params(ctx, &hosttypes.QueryParamsRequest{}) + s.Require().NoError(err) + + return *res.Params +} + +// TestControllerEnabledParam tests that changing the ControllerEnabled param works as expected +func (s *InterchainAccountsParamsTestSuite) TestControllerEnabledParam() { + t := s.T() + ctx := context.TODO() + + // setup relayers and connection-0 between two chains + // channel-0 is a transfer channel but it will not be used in this test case + _, _ = s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, _ := s.GetChains() + chainAVersion := chainA.Config().Images[0].Version + + // setup controller account on chainA + controllerAccount := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + controllerAddress := controllerAccount.FormattedAddress() + + t.Run("ensure the controller is enabled", func(t *testing.T) { + params := s.QueryControllerParams(ctx, chainA) + s.Require().True(params.ControllerEnabled) + }) + + t.Run("disable the controller", func(t *testing.T) { + if testvalues.SelfParamsFeatureReleases.IsSupported(chainAVersion) { + authority, err := s.QueryModuleAccountAddress(ctx, govtypes.ModuleName, chainA) + s.Require().NoError(err) + s.Require().NotNil(authority) + + msg := controllertypes.MsgUpdateParams{ + Signer: authority.String(), + Params: controllertypes.NewParams(false), + } + s.ExecuteAndPassGovV1Proposal(ctx, &msg, chainA, controllerAccount) + } else { + changes := []paramsproposaltypes.ParamChange{ + paramsproposaltypes.NewParamChange(controllertypes.StoreKey, string(controllertypes.KeyControllerEnabled), "false"), + } + + proposal := paramsproposaltypes.NewParameterChangeProposal(ibctesting.Title, ibctesting.Description, changes) + s.ExecuteAndPassGovV1Beta1Proposal(ctx, chainA, controllerAccount, proposal) + } + }) + + t.Run("ensure controller is disabled", func(t *testing.T) { + params := s.QueryControllerParams(ctx, chainA) + s.Require().False(params.ControllerEnabled) + }) + + t.Run("ensure that broadcasting a MsgRegisterInterchainAccount fails", func(t *testing.T) { + // explicitly set the version string because we don't want to use incentivized channels. + version := icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID) + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, controllerAddress, version, channeltypes.ORDERED) + + txResp := s.BroadcastMessages(ctx, chainA, controllerAccount, msgRegisterAccount) + s.AssertTxFailure(txResp, controllertypes.ErrControllerSubModuleDisabled) + }) +} + +func (s *InterchainAccountsParamsTestSuite) TestHostEnabledParam() { + t := s.T() + ctx := context.TODO() + + // setup relayers and connection-0 between two chains + // channel-0 is a transfer channel but it will not be used in this test case + _, _ = s.SetupChainsRelayerAndChannel(ctx, nil) + _, chainB := s.GetChains() + chainBVersion := chainB.Config().Images[0].Version + + // setup 2 accounts: controller account on chain A, a second chain B account. + // host account will be created when the ICA is registered + chainBUser := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + // Assert that default value for enabled is true. + t.Run("ensure the host is enabled", func(t *testing.T) { + params := s.QueryHostParams(ctx, chainB) + s.Require().True(params.HostEnabled) + s.Require().Equal([]string{hosttypes.AllowAllHostMsgs}, params.AllowMessages) + }) + + t.Run("disable the host", func(t *testing.T) { + if testvalues.SelfParamsFeatureReleases.IsSupported(chainBVersion) { + authority, err := s.QueryModuleAccountAddress(ctx, govtypes.ModuleName, chainB) + s.Require().NoError(err) + s.Require().NotNil(authority) + + msg := hosttypes.MsgUpdateParams{ + Signer: authority.String(), + Params: hosttypes.NewParams(false, []string{hosttypes.AllowAllHostMsgs}), + } + s.ExecuteAndPassGovV1Proposal(ctx, &msg, chainB, chainBUser) + } else { + changes := []paramsproposaltypes.ParamChange{ + paramsproposaltypes.NewParamChange(hosttypes.StoreKey, string(hosttypes.KeyHostEnabled), "false"), + } + + proposal := paramsproposaltypes.NewParameterChangeProposal(ibctesting.Title, ibctesting.Description, changes) + s.ExecuteAndPassGovV1Beta1Proposal(ctx, chainB, chainBUser, proposal) + } + }) + + t.Run("ensure the host is disabled", func(t *testing.T) { + params := s.QueryHostParams(ctx, chainB) + s.Require().False(params.HostEnabled) + }) +} diff --git a/e2e/tests/upgrades/genesis_test.go b/e2e/tests/upgrades/genesis_test.go new file mode 100644 index 00000000000..4b387bb128b --- /dev/null +++ b/e2e/tests/upgrades/genesis_test.go @@ -0,0 +1,246 @@ +//go:build !test_e2e + +package upgrades + +import ( + "context" + "testing" + "time" + + "github.com/cosmos/gogoproto/proto" + "github.com/strangelove-ventures/interchaintest/v8" + cosmos "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + test "github.com/strangelove-ventures/interchaintest/v8/testutil" + "github.com/stretchr/testify/suite" + "go.uber.org/zap" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + controllertypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/controller/types" + icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +func TestGenesisTestSuite(t *testing.T) { + suite.Run(t, new(GenesisTestSuite)) +} + +type GenesisTestSuite struct { + testsuite.E2ETestSuite +} + +func (s *GenesisTestSuite) TestIBCGenesis() { + t := s.T() + + configFileOverrides := make(map[string]any) + appTomlOverrides := make(test.Toml) + + appTomlOverrides["halt-height"] = haltHeight + configFileOverrides["config/app.toml"] = appTomlOverrides + chainOpts := func(options *testsuite.ChainOptions) { + options.ChainASpec.ConfigFileOverrides = configFileOverrides + } + + // create chains with specified chain configuration options + chainA, chainB := s.GetChains(chainOpts) + + ctx := context.Background() + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, nil) + var ( + chainADenom = chainA.Config().Denom + chainBIBCToken = testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) // IBC token sent to chainB + + ) + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainAAddress := chainAWallet.FormattedAddress() + + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + chainBAddress := chainBWallet.FormattedAddress() + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + + t.Run("ics20: native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { + transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, testvalues.DefaultTransferAmount(chainADenom), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0, "") + s.AssertTxSuccess(transferTxResp) + }) + + t.Run("ics20: tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + }) + + // setup 2 accounts: controller account on chain A, a second chain B account. + // host account will be created when the ICA is registered + controllerAccount := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + controllerAddress := controllerAccount.FormattedAddress() + chainBAccount := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + var hostAccount string + + t.Run("ics27: broadcast MsgRegisterInterchainAccount", func(t *testing.T) { + // explicitly set the version string because we don't want to use incentivized channels. + version := icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID) + msgRegisterAccount := controllertypes.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, controllerAddress, version, channeltypes.ORDERED) + + txResp := s.BroadcastMessages(ctx, chainA, controllerAccount, msgRegisterAccount) + s.AssertTxSuccess(txResp) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("ics20: packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) + + actualBalance, err := s.QueryBalance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) + + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) + + t.Run("ics27: verify interchain account", func(t *testing.T) { + var err error + hostAccount, err = s.QueryInterchainAccount(ctx, chainA, controllerAddress, ibctesting.FirstConnectionID) + s.Require().NoError(err) + s.Require().NotZero(len(hostAccount)) + + channels, err := relayer.GetChannels(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID) + s.Require().NoError(err) + s.Require().Equal(len(channels), 2) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB), "failed to wait for blocks") + + t.Run("Halt chain and export genesis", func(t *testing.T) { + s.HaltChainAndExportGenesis(ctx, chainA.(*cosmos.CosmosChain), relayer, int64(haltHeight)) + }) + + t.Run("ics20: native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { + transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, testvalues.DefaultTransferAmount(chainADenom), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0, "") + s.AssertTxSuccess(transferTxResp) + }) + + t.Run("ics20: tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - 2*testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + }) + + t.Run("ics27: interchain account executes a bank transfer on behalf of the corresponding owner account", func(t *testing.T) { + t.Run("fund interchain account wallet", func(t *testing.T) { + // fund the host account so it has some $$ to send + err := chainB.SendFunds(ctx, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Address: hostAccount, + Amount: sdkmath.NewInt(testvalues.StartingTokenAmount), + Denom: chainB.Config().Denom, + }) + s.Require().NoError(err) + }) + + t.Run("broadcast MsgSendTx", func(t *testing.T) { + // assemble bank transfer message from host account to user account on host chain + msgSend := &banktypes.MsgSend{ + FromAddress: hostAccount, + ToAddress: chainBAccount.FormattedAddress(), + Amount: sdk.NewCoins(testvalues.DefaultTransferAmount(chainB.Config().Denom)), + } + + cdc := testsuite.Codec() + bz, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{msgSend}, icatypes.EncodingProtobuf) + s.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: bz, + Memo: "e2e", + } + + msgSendTx := controllertypes.NewMsgSendTx(controllerAccount.FormattedAddress(), ibctesting.FirstConnectionID, uint64(time.Hour.Nanoseconds()), packetData) + + resp := s.BroadcastMessages( + ctx, + chainA, + controllerAccount, + msgSendTx, + ) + + s.AssertTxSuccess(resp) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB)) + }) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") +} + +func (s *GenesisTestSuite) HaltChainAndExportGenesis(ctx context.Context, chain *cosmos.CosmosChain, relayer ibc.Relayer, haltHeight int64) { + timeoutCtx, timeoutCtxCancel := context.WithTimeout(ctx, time.Minute*2) + defer timeoutCtxCancel() + + err := test.WaitForBlocks(timeoutCtx, int(haltHeight), chain) + s.Require().Error(err, "chain did not halt at halt height") + + err = chain.StopAllNodes(ctx) + s.Require().NoError(err, "error stopping node(s)") + + state, err := chain.ExportState(ctx, haltHeight) + s.Require().NoError(err) + + appTomlOverrides := make(test.Toml) + + appTomlOverrides["halt-height"] = 0 + + for _, node := range chain.Nodes() { + err := node.OverwriteGenesisFile(ctx, []byte(state)) + s.Require().NoError(err) + } + + for _, node := range chain.Nodes() { + err := test.ModifyTomlConfigFile( + ctx, + zap.NewExample(), + node.DockerClient, + node.TestName, + node.VolumeName, + "config/app.toml", + appTomlOverrides, + ) + s.Require().NoError(err) + + _, _, err = node.ExecBin(ctx, "comet", "unsafe-reset-all") + s.Require().NoError(err) + } + + err = chain.StartAllNodes(ctx) + s.Require().NoError(err) + + // we are reinitializing the clients because we need to update the hostGRPCAddress after + // the upgrade and subsequent restarting of nodes + s.InitGRPCClients(chain) + + timeoutCtx, timeoutCtxCancel = context.WithTimeout(ctx, time.Minute*2) + defer timeoutCtxCancel() + + err = test.WaitForBlocks(timeoutCtx, int(blocksAfterUpgrade), chain) + s.Require().NoError(err, "chain did not produce blocks after halt") + + height, err := chain.Height(ctx) + s.Require().NoError(err, "error fetching height after halt") + + s.Require().Greater(int64(height), haltHeight, "height did not increment after halt") +} diff --git a/e2e/testsuite/testconfig.go b/e2e/testsuite/testconfig.go new file mode 100644 index 00000000000..ea759ba1322 --- /dev/null +++ b/e2e/testsuite/testconfig.go @@ -0,0 +1,725 @@ +package testsuite + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path" + "strings" + "time" + + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + interchaintestutil "github.com/strangelove-ventures/interchaintest/v8/testutil" + "gopkg.in/yaml.v2" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module/testutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + + cmtjson "github.com/cometbft/cometbft/libs/json" + + "github.com/cosmos/ibc-go/e2e/relayer" + "github.com/cosmos/ibc-go/e2e/semverutil" + "github.com/cosmos/ibc-go/e2e/testvalues" + wasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" + ibctypes "github.com/cosmos/ibc-go/v8/modules/core/types" +) + +const ( + // ChainImageEnv specifies the image that the chains will use. If left unspecified, it will + // default to being determined based on the specified binary. E.g. ghcr.io/cosmos/ibc-go-simd + ChainImageEnv = "CHAIN_IMAGE" + // ChainATagEnv specifies the tag that Chain A will use. + ChainATagEnv = "CHAIN_A_TAG" + // ChainBTagEnv specifies the tag that Chain B will use. If unspecified + // the value will default to the same value as Chain A. + ChainBTagEnv = "CHAIN_B_TAG" + // RelayerIDEnv specifies the ID of the relayer to use. + RelayerIDEnv = "RELAYER_ID" + // ChainBinaryEnv binary is the binary that will be used for both chains. + ChainBinaryEnv = "CHAIN_BINARY" + // ChainUpgradeTagEnv specifies the upgrade version tag + ChainUpgradeTagEnv = "CHAIN_UPGRADE_TAG" + // ChainUpgradePlanEnv specifies the upgrade plan name + ChainUpgradePlanEnv = "CHAIN_UPGRADE_PLAN" + // E2EConfigFilePathEnv allows you to specify a custom path for the config file to be used. + E2EConfigFilePathEnv = "E2E_CONFIG_PATH" + + // defaultBinary is the default binary that will be used by the chains. + defaultBinary = "simd" + // defaultRlyTag is the tag that will be used if no relayer tag is specified. + // all images are here https://github.com/cosmos/relayer/pkgs/container/relayer/versions + defaultRlyTag = "latest" + + // TODO: https://github.com/cosmos/ibc-go/issues/4965 + defaultHyperspaceTag = "20231122v39" + // defaultHermesTag is the tag that will be used if no relayer tag is specified for hermes. + defaultHermesTag = "luca_joss-channel-upgrade-authority" + // defaultChainTag is the tag that will be used for the chains if none is specified. + defaultChainTag = "main" + // defaultConfigFileName is the default filename for the config file that can be used to configure + // e2e tests. See sample.config.yaml as an example for what this should look like. + defaultConfigFileName = ".ibc-go-e2e-config.yaml" +) + +func getChainImage(binary string) string { + if binary == "" { + binary = defaultBinary + } + return fmt.Sprintf("ghcr.io/cosmos/ibc-go-%s", binary) +} + +// TestConfig holds configuration used throughout the different e2e tests. +type TestConfig struct { + // ChainConfigs holds configuration values related to the chains used in the tests. + ChainConfigs []ChainConfig `yaml:"chains"` + // RelayerConfig holds all known relayer configurations that can be used in the tests. + RelayerConfigs []relayer.Config `yaml:"relayers"` + // ActiveRelayer specifies the relayer that will be used. It must match the ID of one of the entries in RelayerConfigs. + ActiveRelayer string `yaml:"activeRelayer"` + // UpgradeConfig holds values used only for the upgrade tests. + UpgradeConfig UpgradeConfig `yaml:"upgrade"` + // CometBFTConfig holds values for configuring CometBFT. + CometBFTConfig CometBFTConfig `yaml:"cometbft"` + // DebugConfig holds configuration for miscellaneous options. + DebugConfig DebugConfig `yaml:"debug"` +} + +// Validate validates the test configuration is valid for use within the tests. +// this should be called before using the configuration. +func (tc TestConfig) Validate() error { + if err := tc.validateChains(); err != nil { + return fmt.Errorf("invalid chain configuration: %w", err) + } + + if err := tc.validateRelayers(); err != nil { + return fmt.Errorf("invalid relayer configuration: %w", err) + } + return nil +} + +// validateChains validates the chain configurations. +func (tc TestConfig) validateChains() error { + for _, cfg := range tc.ChainConfigs { + if cfg.Binary == "" { + return fmt.Errorf("chain config missing binary: %+v", cfg) + } + if cfg.Image == "" { + return fmt.Errorf("chain config missing image: %+v", cfg) + } + if cfg.Tag == "" { + return fmt.Errorf("chain config missing tag: %+v", cfg) + } + + // TODO: validate chainID in https://github.com/cosmos/ibc-go/issues/4697 + // these are not passed in the CI at the moment. Defaults are used. + if !IsCI() { + if cfg.ChainID == "" { + return fmt.Errorf("chain config missing chainID: %+v", cfg) + } + } + + // TODO: validate number of nodes in https://github.com/cosmos/ibc-go/issues/4697 + // these are not passed in the CI at the moment. + if !IsCI() { + if cfg.NumValidators == 0 && cfg.NumFullNodes == 0 { + return fmt.Errorf("chain config missing number of validators or full nodes: %+v", cfg) + } + } + } + return nil +} + +// validateRelayers validates relayer configuration. +func (tc TestConfig) validateRelayers() error { + if len(tc.RelayerConfigs) < 1 { + return fmt.Errorf("no relayer configurations specified") + } + + for _, r := range tc.RelayerConfigs { + if r.ID == "" { + return fmt.Errorf("relayer config missing ID: %+v", r) + } + if r.Image == "" { + return fmt.Errorf("relayer config missing image: %+v", r) + } + if r.Tag == "" { + return fmt.Errorf("relayer config missing tag: %+v", r) + } + } + + if tc.GetActiveRelayerConfig() == nil { + return fmt.Errorf("active relayer %s not found in relayer configs: %+v", tc.ActiveRelayer, tc.RelayerConfigs) + } + + return nil +} + +// GetActiveRelayerConfig returns the currently specified relayer config. +func (tc TestConfig) GetActiveRelayerConfig() *relayer.Config { + for _, r := range tc.RelayerConfigs { + if r.ID == tc.ActiveRelayer { + return &r + } + } + return nil +} + +// GetChainNumValidators returns the number of validators for the specific chain index. +// default 1 +func (tc TestConfig) GetChainNumValidators(idx int) int { + if tc.ChainConfigs[idx].NumValidators > 0 { + return tc.ChainConfigs[idx].NumValidators + } + return 1 +} + +// GetChainNumFullNodes returns the number of full nodes for the specific chain index. +// default 0 +func (tc TestConfig) GetChainNumFullNodes(idx int) int { + if tc.ChainConfigs[idx].NumFullNodes > 0 { + return tc.ChainConfigs[idx].NumFullNodes + } + return 0 +} + +// GetChainAID returns the chain-id for chain A. +func (tc TestConfig) GetChainAID() string { + if tc.ChainConfigs[0].ChainID != "" { + return tc.ChainConfigs[0].ChainID + } + return "chainA-1" +} + +// GetChainBID returns the chain-id for chain B. +func (tc TestConfig) GetChainBID() string { + if tc.ChainConfigs[1].ChainID != "" { + return tc.ChainConfigs[1].ChainID + } + return "chainB-1" +} + +// UpgradeConfig holds values relevant to upgrade tests. +type UpgradeConfig struct { + PlanName string `yaml:"planName"` + Tag string `yaml:"tag"` +} + +// ChainConfig holds information about an individual chain used in the tests. +type ChainConfig struct { + ChainID string `yaml:"chainId"` + Image string `yaml:"image"` + Tag string `yaml:"tag"` + Binary string `yaml:"binary"` + NumValidators int `yaml:"numValidators"` + NumFullNodes int `yaml:"numFullNodes"` +} + +type CometBFTConfig struct { + LogLevel string `yaml:"logLevel"` +} + +type DebugConfig struct { + // DumpLogs forces the logs to be collected before removing test containers. + DumpLogs bool `yaml:"dumpLogs"` +} + +// LoadConfig attempts to load a atest configuration from the default file path. +// if any environment variables are specified, they will take precedence over the individual configuration +// options. +func LoadConfig() TestConfig { + tc := getConfig() + if err := tc.Validate(); err != nil { + panic(err) + } + return tc +} + +// getConfig returns the TestConfig with any environment variable overrides. +func getConfig() TestConfig { + fileTc, foundFile := fromFile() + if !foundFile { + return fromEnv() + } + + return applyEnvironmentVariableOverrides(fileTc) +} + +// fromFile returns a TestConfig from a json file and a boolean indicating if the file was found. +func fromFile() (TestConfig, bool) { + var tc TestConfig + bz, err := os.ReadFile(getConfigFilePath()) + if err != nil { + return TestConfig{}, false + } + + if err := yaml.Unmarshal(bz, &tc); err != nil { + panic(err) + } + + return tc, true +} + +// applyEnvironmentVariableOverrides applies all environment variable changes to the config +// loaded from a file. +func applyEnvironmentVariableOverrides(fromFile TestConfig) TestConfig { + envTc := fromEnv() + + if os.Getenv(ChainATagEnv) != "" { + fromFile.ChainConfigs[0].Tag = envTc.ChainConfigs[0].Tag + } + + if os.Getenv(ChainBTagEnv) != "" { + fromFile.ChainConfigs[1].Tag = envTc.ChainConfigs[1].Tag + } + + if os.Getenv(ChainBinaryEnv) != "" { + for i := range fromFile.ChainConfigs { + fromFile.ChainConfigs[i].Binary = envTc.ChainConfigs[i].Binary + } + } + + if os.Getenv(ChainImageEnv) != "" { + for i := range fromFile.ChainConfigs { + fromFile.ChainConfigs[i].Image = envTc.ChainConfigs[i].Image + } + } + + if os.Getenv(RelayerIDEnv) != "" { + fromFile.ActiveRelayer = envTc.ActiveRelayer + } + + if os.Getenv(ChainUpgradePlanEnv) != "" { + fromFile.UpgradeConfig.PlanName = envTc.UpgradeConfig.PlanName + } + + if os.Getenv(ChainUpgradeTagEnv) != "" { + fromFile.UpgradeConfig.Tag = envTc.UpgradeConfig.Tag + } + + return fromFile +} + +// fromEnv returns a TestConfig constructed from environment variables. +func fromEnv() TestConfig { + return TestConfig{ + ChainConfigs: getChainConfigsFromEnv(), + UpgradeConfig: getUpgradePlanConfigFromEnv(), + ActiveRelayer: os.Getenv(RelayerIDEnv), + + // TODO: we can remove this, and specify these values in a config file for the CI + // in https://github.com/cosmos/ibc-go/issues/4697 + RelayerConfigs: []relayer.Config{ + getDefaultRlyRelayerConfig(), + getDefaultHermesRelayerConfig(), + getDefaultHyperspaceRelayerConfig(), + }, + CometBFTConfig: CometBFTConfig{LogLevel: "info"}, + } +} + +// getChainConfigsFromEnv returns the chain configs from environment variables. +func getChainConfigsFromEnv() []ChainConfig { + chainBinary, ok := os.LookupEnv(ChainBinaryEnv) + if !ok { + chainBinary = defaultBinary + } + + chainATag, ok := os.LookupEnv(ChainATagEnv) + if !ok { + chainATag = defaultChainTag + } + + chainBTag, ok := os.LookupEnv(ChainBTagEnv) + if !ok { + chainBTag = chainATag + } + + chainAImage := getChainImage(chainBinary) + specifiedChainImage, ok := os.LookupEnv(ChainImageEnv) + if ok { + chainAImage = specifiedChainImage + } + + numValidators := 4 + numFullNodes := 1 + + chainBImage := chainAImage + return []ChainConfig{ + { + Image: chainAImage, + Tag: chainATag, + Binary: chainBinary, + NumValidators: numValidators, + NumFullNodes: numFullNodes, + }, + { + Image: chainBImage, + Tag: chainBTag, + Binary: chainBinary, + NumValidators: numValidators, + NumFullNodes: numFullNodes, + }, + } +} + +// getConfigFilePath returns the absolute path where the e2e config file should be. +func getConfigFilePath() string { + if absoluteConfigPath := os.Getenv(E2EConfigFilePathEnv); absoluteConfigPath != "" { + return absoluteConfigPath + } + + homeDir, err := os.UserHomeDir() + if err != nil { + panic(err) + } + return path.Join(homeDir, defaultConfigFileName) +} + +// TODO: remove in https://github.com/cosmos/ibc-go/issues/4697 +// getDefaultHermesRelayerConfig returns the default config for the hermes relayer. +func getDefaultHermesRelayerConfig() relayer.Config { + return relayer.Config{ + Tag: defaultHermesTag, + ID: relayer.Hermes, + Image: relayer.HermesRelayerRepository, + } +} + +// TODO: remove in https://github.com/cosmos/ibc-go/issues/4697 +// getDefaultRlyRelayerConfig returns the default config for the golang relayer. +func getDefaultRlyRelayerConfig() relayer.Config { + return relayer.Config{ + Tag: defaultRlyTag, + ID: relayer.Rly, + Image: relayer.RlyRelayerRepository, + } +} + +// TODO: remove in https://github.com/cosmos/ibc-go/issues/4697 +// getDefaultHyperspaceRelayerConfig returns the default config for the hyperspace relayer. +func getDefaultHyperspaceRelayerConfig() relayer.Config { + return relayer.Config{ + Tag: defaultHyperspaceTag, + ID: relayer.Hyperspace, + Image: relayer.HyperspaceRelayerRepository, + } +} + +// getUpgradePlanConfigFromEnv returns the upgrade config from environment variables. +func getUpgradePlanConfigFromEnv() UpgradeConfig { + upgradeTag, ok := os.LookupEnv(ChainUpgradeTagEnv) + if !ok { + upgradeTag = "" + } + + upgradePlan, ok := os.LookupEnv(ChainUpgradePlanEnv) + if !ok { + upgradePlan = "" + } + return UpgradeConfig{ + PlanName: upgradePlan, + Tag: upgradeTag, + } +} + +func GetChainATag() string { + return LoadConfig().ChainConfigs[0].Tag +} + +func GetChainBTag() string { + if chainBTag := LoadConfig().ChainConfigs[1].Tag; chainBTag != "" { + return chainBTag + } + return GetChainATag() +} + +// IsCI returns true if the tests are running in CI, false is returned +// if the tests are running locally. +// Note: github actions passes a CI env value of true by default to all runners. +func IsCI() bool { + return strings.ToLower(os.Getenv("CI")) == "true" +} + +// IsFork returns true if the tests are running in fork mode, false is returned otherwise. +func IsFork() bool { + return strings.ToLower(os.Getenv("FORK")) == "true" +} + +// ChainOptions stores chain configurations for the chains that will be +// created for the tests. They can be modified by passing ChainOptionConfiguration +// to E2ETestSuite.GetChains. +type ChainOptions struct { + ChainASpec *interchaintest.ChainSpec + ChainBSpec *interchaintest.ChainSpec + SkipPathCreation bool +} + +// ChainOptionConfiguration enables arbitrary configuration of ChainOptions. +type ChainOptionConfiguration func(options *ChainOptions) + +// DefaultChainOptions returns the default configuration for the chains. +// These options can be configured by passing configuration functions to E2ETestSuite.GetChains. +func DefaultChainOptions() ChainOptions { + tc := LoadConfig() + + chainACfg := newDefaultSimappConfig(tc.ChainConfigs[0], "simapp-a", tc.GetChainAID(), "atoma", tc.CometBFTConfig) + chainBCfg := newDefaultSimappConfig(tc.ChainConfigs[1], "simapp-b", tc.GetChainBID(), "atomb", tc.CometBFTConfig) + + chainAVal, chainAFn := getValidatorsAndFullNodes(0) + chainBVal, chainBFn := getValidatorsAndFullNodes(1) + + return ChainOptions{ + ChainASpec: &interchaintest.ChainSpec{ + ChainConfig: chainACfg, + NumFullNodes: &chainAFn, + NumValidators: &chainAVal, + }, + ChainBSpec: &interchaintest.ChainSpec{ + ChainConfig: chainBCfg, + NumFullNodes: &chainBFn, + NumValidators: &chainBVal, + }, + } +} + +// newDefaultSimappConfig creates an ibc configuration for simd. +func newDefaultSimappConfig(cc ChainConfig, name, chainID, denom string, cometCfg CometBFTConfig) ibc.ChainConfig { + configFileOverrides := make(map[string]any) + tmTomlOverrides := make(interchaintestutil.Toml) + + tmTomlOverrides["log_level"] = cometCfg.LogLevel // change to debug in ~/.ibc-go-e2e-config.json to increase cometbft logging. + configFileOverrides["config/config.toml"] = tmTomlOverrides + + return ibc.ChainConfig{ + Type: "cosmos", + Name: name, + ChainID: chainID, + Images: []ibc.DockerImage{ + { + Repository: cc.Image, + Version: cc.Tag, + UidGid: "1000:1000", + }, + }, + Bin: cc.Binary, + Bech32Prefix: "cosmos", + CoinType: fmt.Sprint(sdk.GetConfig().GetCoinType()), + Denom: denom, + EncodingConfig: SDKEncodingConfig(), + GasPrices: fmt.Sprintf("0.00%s", denom), + GasAdjustment: 1.3, + TrustingPeriod: "508h", + NoHostMount: false, + ModifyGenesis: getGenesisModificationFunction(cc), + ConfigFileOverrides: configFileOverrides, + } +} + +// getGenesisModificationFunction returns a genesis modification function that handles the GenesisState type +// correctly depending on if the govv1beta1 gov module is used or if govv1 is being used. +func getGenesisModificationFunction(cc ChainConfig) func(ibc.ChainConfig, []byte) ([]byte, error) { + binary := cc.Binary + version := cc.Tag + + simdSupportsGovV1Genesis := binary == defaultBinary && testvalues.GovGenesisFeatureReleases.IsSupported(version) + + if simdSupportsGovV1Genesis { + return defaultGovv1ModifyGenesis(version) + } + + return defaultGovv1Beta1ModifyGenesis(version) +} + +// defaultGovv1ModifyGenesis will only modify governance params to ensure the voting period and minimum deposit +// are functional for e2e testing purposes. +func defaultGovv1ModifyGenesis(version string) func(ibc.ChainConfig, []byte) ([]byte, error) { + stdlibJSONMarshalling := semverutil.FeatureReleases{MajorVersion: "v8"} + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + appGenesis, err := genutiltypes.AppGenesisFromReader(bytes.NewReader(genbz)) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into genesis doc: %w", err) + } + + var appState genutiltypes.AppMap + if err := json.Unmarshal(appGenesis.AppState, &appState); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into app state: %w", err) + } + + govGenBz, err := modifyGovV1AppState(chainConfig, appState[govtypes.ModuleName]) + if err != nil { + return nil, err + } + appState[govtypes.ModuleName] = govGenBz + + if !testvalues.AllowAllClientsWildcardFeatureReleases.IsSupported(version) { + ibcGenBz, err := modifyClientGenesisAppState(appState[ibcexported.ModuleName]) + if err != nil { + return nil, err + } + appState[ibcexported.ModuleName] = ibcGenBz + } + + appGenesis.AppState, err = json.Marshal(appState) + if err != nil { + return nil, err + } + + // in older version < v8, tmjson marshal must be used. + // regular json marshalling must be used for v8 and above as the + // sdk is de-coupled from comet. + marshalIndentFn := cmtjson.MarshalIndent + if stdlibJSONMarshalling.IsSupported(version) { + marshalIndentFn = json.MarshalIndent + } + + bz, err := marshalIndentFn(appGenesis, "", " ") + if err != nil { + return nil, err + } + + return bz, nil + } +} + +// defaultGovv1Beta1ModifyGenesis will only modify governance params to ensure the voting period and minimum deposit +// // are functional for e2e testing purposes. +func defaultGovv1Beta1ModifyGenesis(version string) func(ibc.ChainConfig, []byte) ([]byte, error) { + const appStateKey = "app_state" + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + genesisDocMap := map[string]interface{}{} + err := json.Unmarshal(genbz, &genesisDocMap) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into genesis doc: %w", err) + } + + appStateMap, ok := genesisDocMap[appStateKey].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("failed to extract to app_state") + } + + govModuleBytes, err := json.Marshal(appStateMap[govtypes.ModuleName]) + if err != nil { + return nil, fmt.Errorf("failed to extract gov genesis bytes: %s", err) + } + + govModuleGenesisBytes, err := modifyGovv1Beta1AppState(chainConfig, govModuleBytes) + if err != nil { + return nil, err + } + + govModuleGenesisMap := map[string]interface{}{} + err = json.Unmarshal(govModuleGenesisBytes, &govModuleGenesisMap) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal gov genesis bytes into map: %w", err) + } + + if !testvalues.AllowAllClientsWildcardFeatureReleases.IsSupported(version) { + ibcModuleBytes, err := json.Marshal(appStateMap[ibcexported.ModuleName]) + if err != nil { + return nil, fmt.Errorf("failed to extract ibc genesis bytes: %s", err) + } + + ibcGenesisBytes, err := modifyClientGenesisAppState(ibcModuleBytes) + if err != nil { + return nil, err + } + + ibcModuleGenesisMap := map[string]interface{}{} + err = json.Unmarshal(ibcGenesisBytes, &ibcModuleGenesisMap) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal gov genesis bytes into map: %w", err) + } + } + + appStateMap[govtypes.ModuleName] = govModuleGenesisMap + genesisDocMap[appStateKey] = appStateMap + + finalGenesisDocBytes, err := json.MarshalIndent(genesisDocMap, "", " ") + if err != nil { + return nil, err + } + + return finalGenesisDocBytes, nil + } +} + +// modifyGovV1AppState takes the existing gov app state and marshals it to a govv1 GenesisState. +func modifyGovV1AppState(chainConfig ibc.ChainConfig, govAppState []byte) ([]byte, error) { + cfg := testutil.MakeTestEncodingConfig() + + cdc := codec.NewProtoCodec(cfg.InterfaceRegistry) + govv1.RegisterInterfaces(cfg.InterfaceRegistry) + + govGenesisState := &govv1.GenesisState{} + + if err := cdc.UnmarshalJSON(govAppState, govGenesisState); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into gov genesis state: %w", err) + } + + if govGenesisState.Params == nil { + govGenesisState.Params = &govv1.Params{} + } + + govGenesisState.Params.MinDeposit = sdk.NewCoins(sdk.NewCoin(chainConfig.Denom, govv1beta1.DefaultMinDepositTokens)) + maxDep := time.Second * 10 + govGenesisState.Params.MaxDepositPeriod = &maxDep + vp := testvalues.VotingPeriod + govGenesisState.Params.VotingPeriod = &vp + + govGenBz := MustProtoMarshalJSON(govGenesisState) + + return govGenBz, nil +} + +// modifyGovv1Beta1AppState takes the existing gov app state and marshals it to a govv1beta1 GenesisState. +func modifyGovv1Beta1AppState(chainConfig ibc.ChainConfig, govAppState []byte) ([]byte, error) { + cfg := testutil.MakeTestEncodingConfig() + + cdc := codec.NewProtoCodec(cfg.InterfaceRegistry) + govv1beta1.RegisterInterfaces(cfg.InterfaceRegistry) + + govGenesisState := &govv1beta1.GenesisState{} + if err := cdc.UnmarshalJSON(govAppState, govGenesisState); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into govv1beta1 genesis state: %w", err) + } + + govGenesisState.DepositParams.MinDeposit = sdk.NewCoins(sdk.NewCoin(chainConfig.Denom, govv1beta1.DefaultMinDepositTokens)) + govGenesisState.VotingParams.VotingPeriod = testvalues.VotingPeriod + + govGenBz, err := cdc.MarshalJSON(govGenesisState) + if err != nil { + return nil, fmt.Errorf("failed to marshal gov genesis state: %w", err) + } + + return govGenBz, nil +} + +// modifyClientGenesisAppState takes the existing ibc app state and marshals it to an ibc GenesisState. +func modifyClientGenesisAppState(ibcAppState []byte) ([]byte, error) { + cfg := testutil.MakeTestEncodingConfig() + + cdc := codec.NewProtoCodec(cfg.InterfaceRegistry) + clienttypes.RegisterInterfaces(cfg.InterfaceRegistry) + + ibcGenesisState := &ibctypes.GenesisState{} + if err := cdc.UnmarshalJSON(ibcAppState, ibcGenesisState); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into client genesis state: %w", err) + } + + ibcGenesisState.ClientGenesis.Params.AllowedClients = append(ibcGenesisState.ClientGenesis.Params.AllowedClients, wasmtypes.Wasm) + ibcGenBz, err := cdc.MarshalJSON(ibcGenesisState) + if err != nil { + return nil, fmt.Errorf("failed to marshal gov genesis state: %w", err) + } + + return ibcGenBz, nil +} diff --git a/modules/apps/27-interchain-accounts/controller/client/cli/tx.go b/modules/apps/27-interchain-accounts/controller/client/cli/tx.go index 39f46d22533..356b7a8fc30 100644 --- a/modules/apps/27-interchain-accounts/controller/client/cli/tx.go +++ b/modules/apps/27-interchain-accounts/controller/client/cli/tx.go @@ -14,11 +14,15 @@ import ( "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/controller/types" icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + connectiontypes "github.com/cosmos/ibc-go/v8/modules/core/03-connection/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" ) const ( // The controller chain channel version - flagVersion = "version" + flagVersion = "version" + // The channel ordering + flagOrdering = "ordering" flagRelativePacketTimeout = "relative-packet-timeout" ) @@ -29,8 +33,8 @@ func newRegisterInterchainAccountCmd() *cobra.Command { Long: strings.TrimSpace(`Register an account on the counterparty chain via the connection id from the source chain. Connection identifier should be for the source chain and the interchain account will be created on the counterparty chain. Callers are expected to -provide the appropriate application version string via {version} flag. Generates a new -port identifier using the provided owner string, binds to the port identifier and claims +provide the appropriate application version string via {version} flag and the desired ordering +via the {ordering} flag. Generates a new port identifier using the provided owner string, binds to the port identifier and claims the associated capability.`), Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -46,13 +50,19 @@ the associated capability.`), return err } - msg := types.NewMsgRegisterInterchainAccount(connectionID, owner, version) + order, err := parseOrder(cmd) + if err != nil { + return err + } + + msg := types.NewMsgRegisterInterchainAccount(connectionID, owner, version, order) return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } cmd.Flags().String(flagVersion, "", "Controller chain channel version") + cmd.Flags().String(flagOrdering, channeltypes.ORDERED.String(), fmt.Sprintf("Channel ordering, can be one of: %s", strings.Join(connectiontypes.SupportedOrderings, ", "))) flags.AddTxFlagsToCmd(cmd) return cmd @@ -107,3 +117,18 @@ appropriate relative timeoutTimestamp must be provided with flag {relative-packe return cmd } + +// parseOrder gets the channel ordering from the flags. +func parseOrder(cmd *cobra.Command) (channeltypes.Order, error) { + orderString, err := cmd.Flags().GetString(flagOrdering) + if err != nil { + return channeltypes.NONE, err + } + + order, found := channeltypes.Order_value[strings.ToUpper(orderString)] + if !found { + return channeltypes.NONE, fmt.Errorf("invalid channel ordering: %s", orderString) + } + + return channeltypes.Order(order), nil +} diff --git a/modules/apps/27-interchain-accounts/controller/ibc_middleware_test.go b/modules/apps/27-interchain-accounts/controller/ibc_middleware_test.go index 5ec07fb8ea2..1c7925e1842 100644 --- a/modules/apps/27-interchain-accounts/controller/ibc_middleware_test.go +++ b/modules/apps/27-interchain-accounts/controller/ibc_middleware_test.go @@ -160,11 +160,6 @@ func (suite *InterchainAccountsTestSuite) TestOnChanOpenInit() { suite.chainA.GetSimApp().ICAControllerKeeper.SetParams(suite.chainA.GetContext(), types.NewParams(false)) }, false, }, - { - "ICA OnChanOpenInit fails - UNORDERED channel", func() { - channel.Ordering = channeltypes.UNORDERED - }, false, - }, { "ICA auth module callback fails", func() { suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenInit = func(ctx sdk.Context, order channeltypes.Order, connectionHops []string, @@ -767,9 +762,10 @@ func (suite *InterchainAccountsTestSuite) TestOnTimeoutPacket() { func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeInit() { var ( - path *ibctesting.Path - isNilApp bool - version string + path *ibctesting.Path + isNilApp bool + version string + channelOrder channeltypes.Order ) testCases := []struct { @@ -778,7 +774,12 @@ func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeInit() { expError error }{ { - "success", func() {}, nil, + "success w/ ORDERED channel", func() {}, nil, + }, + { + "success w/ UNORDERED channel", func() { + channelOrder = channeltypes.UNORDERED + }, nil, }, { "success: nil underlying app", @@ -844,11 +845,13 @@ func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeInit() { cbs = controller.NewIBCMiddleware(nil, suite.chainA.GetSimApp().ICAControllerKeeper) } + channelOrder = channeltypes.ORDERED + version, err = cbs.OnChanUpgradeInit( suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, - channeltypes.ORDERED, + channelOrder, []string{path.EndpointA.ConnectionID}, version, ) @@ -989,6 +992,7 @@ func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeOpen() { path *ibctesting.Path isNilApp bool counterpartyVersion string + channelOrder channeltypes.Order ) testCases := []struct { @@ -996,8 +1000,12 @@ func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeOpen() { malleate func() }{ { - "success", - func() {}, + "success w/ ORDERED channel", func() {}, + }, + { + "success w/ UNORDERED channel", func() { + channelOrder = channeltypes.UNORDERED + }, }, { "success: nil app", @@ -1044,11 +1052,13 @@ func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeOpen() { cbs = controller.NewIBCMiddleware(nil, suite.chainA.GetSimApp().ICAControllerKeeper) } + channelOrder = channeltypes.ORDERED + cbs.OnChanUpgradeOpen( suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, - channeltypes.ORDERED, + channelOrder, []string{path.EndpointA.ConnectionID}, counterpartyVersion, ) @@ -1215,7 +1225,7 @@ func (suite *InterchainAccountsTestSuite) TestInFlightHandshakeRespectsGoAPICall // attempt to start a second handshake via the controller msg server msgServer := controllerkeeper.NewMsgServerImpl(&suite.chainA.GetSimApp().ICAControllerKeeper) - msgRegisterInterchainAccount := types.NewMsgRegisterInterchainAccount(path.EndpointA.ConnectionID, suite.chainA.SenderAccount.GetAddress().String(), TestVersion) + msgRegisterInterchainAccount := types.NewMsgRegisterInterchainAccount(path.EndpointA.ConnectionID, suite.chainA.SenderAccount.GetAddress().String(), TestVersion, channeltypes.ORDERED) res, err := msgServer.RegisterInterchainAccount(suite.chainA.GetContext(), msgRegisterInterchainAccount) suite.Require().Error(err) @@ -1228,7 +1238,7 @@ func (suite *InterchainAccountsTestSuite) TestInFlightHandshakeRespectsMsgServer // initiate a channel handshake such that channel.State == INIT msgServer := controllerkeeper.NewMsgServerImpl(&suite.chainA.GetSimApp().ICAControllerKeeper) - msgRegisterInterchainAccount := types.NewMsgRegisterInterchainAccount(path.EndpointA.ConnectionID, suite.chainA.SenderAccount.GetAddress().String(), TestVersion) + msgRegisterInterchainAccount := types.NewMsgRegisterInterchainAccount(path.EndpointA.ConnectionID, suite.chainA.SenderAccount.GetAddress().String(), TestVersion, channeltypes.ORDERED) res, err := msgServer.RegisterInterchainAccount(suite.chainA.GetContext(), msgRegisterInterchainAccount) suite.Require().NotNil(res) @@ -1261,7 +1271,7 @@ func (suite *InterchainAccountsTestSuite) TestClosedChannelReopensWithMsgServer( // route a new MsgRegisterInterchainAccount in order to reopen the msgServer := controllerkeeper.NewMsgServerImpl(&suite.chainA.GetSimApp().ICAControllerKeeper) - msgRegisterInterchainAccount := types.NewMsgRegisterInterchainAccount(path.EndpointA.ConnectionID, suite.chainA.SenderAccount.GetAddress().String(), path.EndpointA.ChannelConfig.Version) + msgRegisterInterchainAccount := types.NewMsgRegisterInterchainAccount(path.EndpointA.ConnectionID, suite.chainA.SenderAccount.GetAddress().String(), path.EndpointA.ChannelConfig.Version, channeltypes.ORDERED) res, err := msgServer.RegisterInterchainAccount(suite.chainA.GetContext(), msgRegisterInterchainAccount) suite.Require().NoError(err) diff --git a/modules/apps/27-interchain-accounts/controller/keeper/account.go b/modules/apps/27-interchain-accounts/controller/keeper/account.go index bd0b04513c4..d25405fd198 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/account.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/account.go @@ -41,7 +41,7 @@ func (k Keeper) RegisterInterchainAccount(ctx sdk.Context, connectionID, owner, k.SetMiddlewareEnabled(ctx, portID, connectionID) - _, err = k.registerInterchainAccount(ctx, connectionID, portID, version) + _, err = k.registerInterchainAccount(ctx, connectionID, portID, version, channeltypes.ORDERED) if err != nil { return err } @@ -51,7 +51,7 @@ func (k Keeper) RegisterInterchainAccount(ctx sdk.Context, connectionID, owner, // registerInterchainAccount registers an interchain account, returning the channel id of the MsgChannelOpenInitResponse // and an error if one occurred. -func (k Keeper) registerInterchainAccount(ctx sdk.Context, connectionID, portID, version string) (string, error) { +func (k Keeper) registerInterchainAccount(ctx sdk.Context, connectionID, portID, version string, order channeltypes.Order) (string, error) { // if there is an active channel for this portID / connectionID return an error activeChannelID, found := k.GetOpenActiveChannel(ctx, connectionID, portID) if found { @@ -69,7 +69,7 @@ func (k Keeper) registerInterchainAccount(ctx sdk.Context, connectionID, portID, } } - msg := channeltypes.NewMsgChannelOpenInit(portID, version, channeltypes.ORDERED, []string{connectionID}, icatypes.HostPortID, authtypes.NewModuleAddress(icatypes.ModuleName).String()) + msg := channeltypes.NewMsgChannelOpenInit(portID, version, order, []string{connectionID}, icatypes.HostPortID, authtypes.NewModuleAddress(icatypes.ModuleName).String()) handler := k.msgRouter.Handler(msg) res, err := handler(ctx, msg) if err != nil { diff --git a/modules/apps/27-interchain-accounts/controller/keeper/handshake.go b/modules/apps/27-interchain-accounts/controller/keeper/handshake.go index 7c438b503bb..a45d09c6604 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/handshake.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/handshake.go @@ -15,8 +15,7 @@ import ( ) // OnChanOpenInit performs basic validation of channel initialization. -// The channel order must be ORDERED, the counterparty port identifier -// must be the host chain representation as defined in the types package, +// The counterparty port identifier must be the host chain representation as defined in the types package, // the channel version must be equal to the version in the types package, // there must not be an active channel for the specfied port identifier, // and the interchain accounts module must be able to claim the channel @@ -31,10 +30,6 @@ func (k Keeper) OnChanOpenInit( counterparty channeltypes.Counterparty, version string, ) (string, error) { - if order != channeltypes.ORDERED { - return "", errorsmod.Wrapf(channeltypes.ErrInvalidChannelOrdering, "expected %s channel, got %s", channeltypes.ORDERED, order) - } - if !strings.HasPrefix(portID, icatypes.ControllerPortPrefix) { return "", errorsmod.Wrapf(icatypes.ErrInvalidControllerPort, "expected %s{owner-account-address}, got %s", icatypes.ControllerPortPrefix, portID) } @@ -72,8 +67,12 @@ func (k Keeper) OnChanOpenInit( panic(fmt.Errorf("active channel mapping set for %s but channel does not exist in channel store", activeChannelID)) } - if channel.IsOpen() { - return "", errorsmod.Wrapf(icatypes.ErrActiveChannelAlreadySet, "existing active channel %s for portID %s is already OPEN", activeChannelID, portID) + if channel.State != channeltypes.CLOSED { + return "", errorsmod.Wrapf(icatypes.ErrActiveChannelAlreadySet, "existing active channel %s for portID %s must be %s", activeChannelID, portID, channeltypes.CLOSED) + } + + if channel.Ordering != order { + return "", errorsmod.Wrapf(channeltypes.ErrInvalidChannelOrdering, "order cannot change when reopening a channel expected %s, got %s", channel.Ordering, order) } appVersion, found := k.GetAppVersion(ctx, portID, activeChannelID) @@ -149,19 +148,13 @@ func (Keeper) OnChanCloseConfirm( // The following may be changed: // - tx type (must be supported) // - encoding (must be supported) +// - order // // The following may not be changed: -// - order // - connectionHops (and subsequently host/controller connectionIDs) // - interchain account address // - ICS27 protocol version func (k Keeper) OnChanUpgradeInit(ctx sdk.Context, portID, channelID string, proposedOrder channeltypes.Order, proposedConnectionHops []string, proposedversion string) (string, error) { - // verify order has not changed - // support for unordered ICA channels is not implemented yet - if proposedOrder != channeltypes.ORDERED { - return "", errorsmod.Wrapf(channeltypes.ErrInvalidChannelOrdering, "expected %s channel, got %s", channeltypes.ORDERED, proposedOrder) - } - // verify connection hops has not changed connectionID, err := k.GetConnectionID(ctx, portID, channelID) if err != nil { diff --git a/modules/apps/27-interchain-accounts/controller/keeper/handshake_test.go b/modules/apps/27-interchain-accounts/controller/keeper/handshake_test.go index b2fd351cc06..341df9b6295 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/handshake_test.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/handshake_test.go @@ -25,12 +25,12 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { testCases := []struct { name string malleate func() - expPass bool + expError error }{ { "success", func() {}, - true, + nil, }, { "success: previous active channel closed", @@ -48,14 +48,14 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { path.EndpointA.SetChannel(channel) }, - true, + nil, }, { "success: empty channel version returns default metadata JSON string", func() { channel.Version = "" }, - true, + nil, }, { "success: channel reopening", @@ -72,7 +72,25 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { path.EndpointA.ChannelID = "" path.EndpointB.ChannelID = "" }, - true, + nil, + }, + { + "failure: different ordering from previous channel", + func() { + suite.chainA.GetSimApp().ICAControllerKeeper.SetActiveChannelID(suite.chainA.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + + counterparty := channeltypes.NewCounterparty(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID) + channel := channeltypes.Channel{ + State: channeltypes.CLOSED, + Ordering: channeltypes.UNORDERED, + Counterparty: counterparty, + ConnectionHops: []string{path.EndpointA.ConnectionID}, + Version: TestVersion, + } + + path.EndpointA.SetChannel(channel) + }, + channeltypes.ErrInvalidChannelOrdering, }, { "invalid metadata - previous metadata is different", @@ -96,21 +114,14 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { } path.EndpointA.SetChannel(closedChannel) }, - false, - }, - { - "invalid order - UNORDERED", - func() { - channel.Ordering = channeltypes.UNORDERED - }, - false, + icatypes.ErrInvalidVersion, }, { "invalid port ID", func() { path.EndpointA.ChannelConfig.PortID = "invalid-port-id" //nolint:goconst }, - false, + icatypes.ErrInvalidControllerPort, }, { "invalid counterparty port ID", @@ -118,7 +129,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { path.EndpointA.SetChannel(*channel) channel.Counterparty.PortId = "invalid-port-id" }, - false, + icatypes.ErrInvalidHostPort, }, { "invalid metadata bytestring", @@ -126,7 +137,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { path.EndpointA.SetChannel(*channel) channel.Version = "invalid-metadata-bytestring" }, - false, + icatypes.ErrUnknownDataType, }, { "unsupported encoding format", @@ -139,7 +150,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { channel.Version = string(versionBytes) path.EndpointA.SetChannel(*channel) }, - false, + icatypes.ErrInvalidCodec, }, { "unsupported transaction type", @@ -152,7 +163,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { channel.Version = string(versionBytes) path.EndpointA.SetChannel(*channel) }, - false, + icatypes.ErrUnknownDataType, }, { "connection not found", @@ -160,7 +171,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { channel.ConnectionHops = []string{"invalid-connnection-id"} path.EndpointA.SetChannel(*channel) }, - false, + connectiontypes.ErrConnectionNotFound, }, { "connection not found with default empty channel version", @@ -168,7 +179,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { channel.ConnectionHops = []string{"connection-10"} channel.Version = "" }, - false, + connectiontypes.ErrConnectionNotFound, }, { "invalid controller connection ID", @@ -181,7 +192,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { channel.Version = string(versionBytes) path.EndpointA.SetChannel(*channel) }, - false, + connectiontypes.ErrInvalidConnection, }, { "invalid host connection ID", @@ -194,7 +205,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { channel.Version = string(versionBytes) path.EndpointA.SetChannel(*channel) }, - false, + connectiontypes.ErrInvalidConnection, }, { "invalid version", @@ -207,10 +218,10 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { channel.Version = string(versionBytes) path.EndpointA.SetChannel(*channel) }, - false, + icatypes.ErrInvalidVersion, }, { - "channel is already active", + "channel is already active (OPEN state)", func() { suite.chainA.GetSimApp().ICAControllerKeeper.SetActiveChannelID(suite.chainA.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) @@ -224,7 +235,24 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { } suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.SetChannel(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, channel) }, - false, + icatypes.ErrActiveChannelAlreadySet, + }, + { + "channel is already active (FLUSHING state)", + func() { + suite.chainA.GetSimApp().ICAControllerKeeper.SetActiveChannelID(suite.chainA.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + + counterparty := channeltypes.NewCounterparty(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID) + channel := channeltypes.Channel{ + State: channeltypes.FLUSHING, + Ordering: channeltypes.ORDERED, + Counterparty: counterparty, + ConnectionHops: []string{path.EndpointA.ConnectionID}, + Version: TestVersion, + } + suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.SetChannel(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, channel) + }, + icatypes.ErrActiveChannelAlreadySet, }, } @@ -268,11 +296,13 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, chanCap, channel.Counterparty, channel.Version, ) - if tc.expPass { + expPass := tc.expError == nil + if expPass { suite.Require().NoError(err) suite.Require().Equal(string(versionBytes), version) } else { suite.Require().Error(err) + suite.Require().ErrorIs(err, tc.expError) } }) } @@ -510,11 +540,11 @@ func (suite *KeeperTestSuite) TestOnChanUpgradeInit() { nil, }, { - name: "failure: invalid order", + name: "success: change order", malleate: func() { order = channeltypes.UNORDERED }, - expError: channeltypes.ErrInvalidChannelOrdering, + expError: nil, }, { name: "failure: connectionID not found", diff --git a/modules/apps/27-interchain-accounts/controller/keeper/msg_server.go b/modules/apps/27-interchain-accounts/controller/keeper/msg_server.go index 86355cac4f9..b76d2eb28f3 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/msg_server.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/msg_server.go @@ -39,7 +39,7 @@ func (s msgServer) RegisterInterchainAccount(goCtx context.Context, msg *types.M s.SetMiddlewareDisabled(ctx, portID, msg.ConnectionId) - channelID, err := s.registerInterchainAccount(ctx, msg.ConnectionId, portID, msg.Version) + channelID, err := s.registerInterchainAccount(ctx, msg.ConnectionId, portID, msg.Version, msg.Order) if err != nil { s.Logger(ctx).Error("error registering interchain account", "error", err.Error()) return nil, err diff --git a/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go b/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go index 4518a4c3825..f9040f231a0 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go @@ -75,7 +75,7 @@ func (suite *KeeperTestSuite) TestRegisterInterchainAccount_MsgServer() { path := NewICAPath(suite.chainA, suite.chainB) suite.coordinator.SetupConnections(path) - msg = types.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, ibctesting.TestAccAddress, "") + msg = types.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, ibctesting.TestAccAddress, "", channeltypes.ORDERED) tc.malleate() diff --git a/modules/apps/27-interchain-accounts/controller/types/msgs.go b/modules/apps/27-interchain-accounts/controller/types/msgs.go index 213fcbfbc04..c9dee9b8e53 100644 --- a/modules/apps/27-interchain-accounts/controller/types/msgs.go +++ b/modules/apps/27-interchain-accounts/controller/types/msgs.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" host "github.com/cosmos/ibc-go/v8/modules/core/24-host" ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" ) @@ -25,11 +26,12 @@ var ( ) // NewMsgRegisterInterchainAccount creates a new instance of MsgRegisterInterchainAccount -func NewMsgRegisterInterchainAccount(connectionID, owner, version string) *MsgRegisterInterchainAccount { +func NewMsgRegisterInterchainAccount(connectionID, owner, version string, order channeltypes.Order) *MsgRegisterInterchainAccount { return &MsgRegisterInterchainAccount{ ConnectionId: connectionID, Owner: owner, Version: version, + Order: order, } } diff --git a/modules/apps/27-interchain-accounts/controller/types/msgs_test.go b/modules/apps/27-interchain-accounts/controller/types/msgs_test.go index 541f3830fb6..e93a3407795 100644 --- a/modules/apps/27-interchain-accounts/controller/types/msgs_test.go +++ b/modules/apps/27-interchain-accounts/controller/types/msgs_test.go @@ -14,6 +14,7 @@ import ( "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/controller/types" icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" ibctesting "github.com/cosmos/ibc-go/v8/testing" ) @@ -80,6 +81,7 @@ func TestMsgRegisterInterchainAccountValidateBasic(t *testing.T) { ibctesting.FirstConnectionID, ibctesting.TestAccAddress, icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID), + channeltypes.ORDERED, ) tc.malleate() @@ -97,8 +99,16 @@ func TestMsgRegisterInterchainAccountGetSigners(t *testing.T) { expSigner, err := sdk.AccAddressFromBech32(ibctesting.TestAccAddress) require.NoError(t, err) +<<<<<<< HEAD msg := types.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, ibctesting.TestAccAddress, "") require.Equal(t, []sdk.AccAddress{expSigner}, msg.GetSigners()) +======= + msg := types.NewMsgRegisterInterchainAccount(ibctesting.FirstConnectionID, ibctesting.TestAccAddress, "", channeltypes.ORDERED) + encodingCfg := moduletestutil.MakeTestEncodingConfig(ica.AppModuleBasic{}) + signers, _, err := encodingCfg.Codec.GetMsgV1Signers(msg) + require.NoError(t, err) + require.Equal(t, expSigner.Bytes(), signers[0]) +>>>>>>> 61748221 (feat(ica): allow unordered ica channels (#5633)) } func TestMsgSendTxValidateBasic(t *testing.T) { diff --git a/modules/apps/27-interchain-accounts/controller/types/tx.pb.go b/modules/apps/27-interchain-accounts/controller/types/tx.pb.go index 4bb498300a7..f7e724b2e90 100644 --- a/modules/apps/27-interchain-accounts/controller/types/tx.pb.go +++ b/modules/apps/27-interchain-accounts/controller/types/tx.pb.go @@ -10,7 +10,8 @@ import ( _ "github.com/cosmos/gogoproto/gogoproto" grpc1 "github.com/cosmos/gogoproto/grpc" proto "github.com/cosmos/gogoproto/proto" - types "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + types1 "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + types "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" @@ -32,9 +33,10 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // MsgRegisterInterchainAccount defines the payload for Msg/RegisterAccount type MsgRegisterInterchainAccount struct { - Owner string `protobuf:"bytes,1,opt,name=owner,proto3" json:"owner,omitempty"` - ConnectionId string `protobuf:"bytes,2,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Owner string `protobuf:"bytes,1,opt,name=owner,proto3" json:"owner,omitempty"` + ConnectionId string `protobuf:"bytes,2,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Order types.Order `protobuf:"varint,4,opt,name=order,proto3,enum=ibc.core.channel.v1.Order" json:"order,omitempty"` } func (m *MsgRegisterInterchainAccount) Reset() { *m = MsgRegisterInterchainAccount{} } @@ -111,9 +113,9 @@ var xxx_messageInfo_MsgRegisterInterchainAccountResponse proto.InternalMessageIn // MsgSendTx defines the payload for Msg/SendTx type MsgSendTx struct { - Owner string `protobuf:"bytes,1,opt,name=owner,proto3" json:"owner,omitempty"` - ConnectionId string `protobuf:"bytes,2,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - PacketData types.InterchainAccountPacketData `protobuf:"bytes,3,opt,name=packet_data,json=packetData,proto3" json:"packet_data"` + Owner string `protobuf:"bytes,1,opt,name=owner,proto3" json:"owner,omitempty"` + ConnectionId string `protobuf:"bytes,2,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` + PacketData types1.InterchainAccountPacketData `protobuf:"bytes,3,opt,name=packet_data,json=packetData,proto3" json:"packet_data"` // Relative timeout timestamp provided will be added to the current block time during transaction execution. // The timeout timestamp must be non-zero. RelativeTimeout uint64 `protobuf:"varint,4,opt,name=relative_timeout,json=relativeTimeout,proto3" json:"relative_timeout,omitempty"` @@ -284,47 +286,49 @@ func init() { } var fileDescriptor_7def041328c84a30 = []byte{ - // 626 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x54, 0x4f, 0x4f, 0x13, 0x4f, - 0x18, 0xee, 0xfc, 0x58, 0xca, 0x8f, 0x01, 0x45, 0x37, 0x44, 0xca, 0x46, 0x17, 0x52, 0x3d, 0x20, - 0x09, 0x3b, 0x69, 0xd5, 0x68, 0x6a, 0x3c, 0x08, 0x78, 0x68, 0x4c, 0x93, 0x66, 0xc5, 0x84, 0x78, - 0x69, 0xa6, 0xb3, 0x93, 0x61, 0x64, 0x77, 0x66, 0xdd, 0x99, 0xae, 0x78, 0x33, 0x7a, 0x31, 0x1e, - 0x8c, 0x07, 0x3f, 0x00, 0x1f, 0x81, 0x6f, 0x21, 0x47, 0x8e, 0x9e, 0x8c, 0x81, 0x03, 0x37, 0x3f, - 0x83, 0xd9, 0x3f, 0xdd, 0xa2, 0x20, 0xc1, 0xc2, 0xad, 0xef, 0xfb, 0xf6, 0x79, 0xde, 0xe7, 0x79, - 0xe7, 0xdd, 0x17, 0x3e, 0xe4, 0x5d, 0x82, 0x70, 0x18, 0xfa, 0x9c, 0x60, 0xcd, 0xa5, 0x50, 0x88, - 0x0b, 0x4d, 0x23, 0xb2, 0x81, 0xb9, 0xe8, 0x60, 0x42, 0x64, 0x4f, 0x68, 0x85, 0x88, 0x14, 0x3a, - 0x92, 0xbe, 0x4f, 0x23, 0x14, 0xd7, 0x90, 0xde, 0x72, 0xc2, 0x48, 0x6a, 0x69, 0xd6, 0x79, 0x97, - 0x38, 0x47, 0xc1, 0xce, 0x09, 0x60, 0x67, 0x00, 0x76, 0xe2, 0x9a, 0x35, 0xcd, 0x24, 0x93, 0x29, - 0x1c, 0x25, 0xbf, 0x32, 0x26, 0xeb, 0xee, 0x99, 0x64, 0xc4, 0x35, 0x14, 0x62, 0xb2, 0x49, 0x75, - 0x8e, 0x5a, 0x19, 0x42, 0xfc, 0x11, 0x35, 0x19, 0xc9, 0x0c, 0x91, 0x2a, 0x90, 0x0a, 0x05, 0x8a, - 0x25, 0xf5, 0x40, 0xb1, 0xac, 0x50, 0x7d, 0x0f, 0xe0, 0xf5, 0x96, 0x62, 0x2e, 0x65, 0x5c, 0x69, - 0x1a, 0x35, 0x0b, 0xea, 0xc7, 0x19, 0xb3, 0x39, 0x0d, 0x47, 0xe5, 0x6b, 0x41, 0xa3, 0x0a, 0x98, - 0x07, 0x0b, 0xe3, 0x6e, 0x16, 0x98, 0x37, 0xe1, 0x25, 0x22, 0x85, 0xa0, 0x24, 0x51, 0xd4, 0xe1, - 0x5e, 0xe5, 0xbf, 0xb4, 0x3a, 0x39, 0x48, 0x36, 0x3d, 0xb3, 0x02, 0xc7, 0x62, 0x1a, 0x29, 0x2e, - 0x45, 0x65, 0x24, 0x2d, 0xf7, 0xc3, 0xc6, 0xe5, 0x0f, 0xdb, 0x73, 0xa5, 0x77, 0x87, 0x3b, 0x8b, - 0x19, 0x5d, 0xd5, 0x83, 0xb7, 0x4e, 0x13, 0xe1, 0x52, 0x15, 0x4a, 0xa1, 0xa8, 0x79, 0x03, 0x42, - 0xb2, 0x81, 0x85, 0xa0, 0x7e, 0xd2, 0x33, 0x53, 0x34, 0x9e, 0x67, 0x9a, 0x9e, 0x39, 0x03, 0xc7, - 0x42, 0x19, 0xe9, 0x81, 0x9e, 0x72, 0x12, 0x36, 0xbd, 0x86, 0x91, 0xf4, 0xab, 0xfe, 0x04, 0x70, - 0xbc, 0xa5, 0xd8, 0x33, 0x2a, 0xbc, 0xb5, 0xad, 0xf3, 0x18, 0xdb, 0x84, 0x13, 0xd9, 0x13, 0x75, - 0x3c, 0xac, 0x71, 0x6a, 0x6e, 0xa2, 0xbe, 0xea, 0x9c, 0x69, 0x51, 0xe2, 0x9a, 0x73, 0xcc, 0x5f, - 0x3b, 0x25, 0x5b, 0xc5, 0x1a, 0x2f, 0x1b, 0xbb, 0xdf, 0xe7, 0x4a, 0x2e, 0x0c, 0x8b, 0x8c, 0x79, - 0x1b, 0x5e, 0x89, 0xa8, 0x8f, 0x35, 0x8f, 0x69, 0x47, 0xf3, 0x80, 0xca, 0x9e, 0xae, 0x18, 0xf3, - 0x60, 0xc1, 0x70, 0xa7, 0xfa, 0xf9, 0xb5, 0x2c, 0x7d, 0x6c, 0xac, 0xf7, 0xe0, 0xd5, 0xc2, 0x6f, - 0x31, 0x43, 0x0b, 0xfe, 0xaf, 0xe8, 0xab, 0x1e, 0x15, 0x84, 0xa6, 0xd6, 0x0d, 0xb7, 0x88, 0xf3, - 0x39, 0x7d, 0x01, 0x70, 0xaa, 0xa5, 0xd8, 0xf3, 0xd0, 0xc3, 0x9a, 0xb6, 0x71, 0x84, 0x03, 0x65, - 0x5e, 0x83, 0x65, 0xc5, 0xd9, 0x60, 0x5c, 0x79, 0x64, 0xae, 0xc3, 0x72, 0x98, 0xfe, 0x23, 0x1d, - 0xd4, 0x44, 0xbd, 0xe1, 0xfc, 0xfb, 0xe7, 0xe2, 0x64, 0x3d, 0x72, 0xef, 0x39, 0x5f, 0x63, 0xaa, - 0x6f, 0x26, 0x6f, 0x55, 0x9d, 0x85, 0x33, 0x7f, 0xa8, 0xea, 0x7b, 0xaa, 0x7f, 0x34, 0xe0, 0x48, - 0x4b, 0x31, 0xf3, 0x2b, 0x80, 0xb3, 0x7f, 0x5f, 0xe5, 0xf6, 0x30, 0xda, 0x4e, 0xdb, 0x4b, 0x6b, - 0xfd, 0xa2, 0x19, 0x8b, 0x57, 0xfa, 0x04, 0x60, 0x39, 0x5f, 0xd4, 0x47, 0x43, 0x36, 0xc9, 0xe0, - 0xd6, 0x93, 0x73, 0xc1, 0x0b, 0x41, 0xdb, 0x00, 0x4e, 0xfe, 0xb6, 0x11, 0x2b, 0x43, 0xf2, 0x1e, - 0x25, 0xb1, 0x9e, 0x5e, 0x00, 0x49, 0x5f, 0xa2, 0x35, 0xfa, 0xf6, 0x70, 0x67, 0x11, 0x2c, 0xbf, - 0xdc, 0xdd, 0xb7, 0xc1, 0xde, 0xbe, 0x0d, 0x7e, 0xec, 0xdb, 0xe0, 0xf3, 0x81, 0x5d, 0xda, 0x3b, - 0xb0, 0x4b, 0xdf, 0x0e, 0xec, 0xd2, 0x8b, 0x36, 0xe3, 0x7a, 0xa3, 0xd7, 0x75, 0x88, 0x0c, 0x50, - 0x7e, 0x10, 0x79, 0x97, 0x2c, 0x31, 0x89, 0xe2, 0x07, 0x28, 0x90, 0x5e, 0xcf, 0xa7, 0x2a, 0x39, - 0xb5, 0x0a, 0xd5, 0xef, 0x2f, 0x0d, 0x74, 0x2c, 0x9d, 0x74, 0x65, 0xf5, 0x9b, 0x90, 0xaa, 0x6e, - 0x39, 0xbd, 0xa2, 0x77, 0x7e, 0x05, 0x00, 0x00, 0xff, 0xff, 0xa1, 0x90, 0xb0, 0xb9, 0x62, 0x06, - 0x00, 0x00, + // 668 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x54, 0x41, 0x4f, 0x13, 0x4f, + 0x14, 0xef, 0xfe, 0x29, 0xe5, 0xcf, 0x80, 0xa0, 0x1b, 0x22, 0x65, 0xa3, 0x05, 0xab, 0x07, 0x24, + 0x61, 0xc6, 0x56, 0x8d, 0xa6, 0xc6, 0x83, 0x80, 0x87, 0xc6, 0x34, 0x36, 0x2b, 0x26, 0xc4, 0x4b, + 0x33, 0x9d, 0x9d, 0x2c, 0x23, 0xdd, 0x99, 0x75, 0x66, 0xba, 0xe2, 0xcd, 0x78, 0x32, 0x1e, 0x8c, + 0x07, 0x3f, 0x00, 0x1f, 0x81, 0x8b, 0x9f, 0x41, 0x8e, 0x1c, 0x3d, 0x19, 0x03, 0x07, 0x6e, 0x7e, + 0x06, 0xb3, 0x3b, 0xdb, 0x2d, 0x0a, 0x12, 0x2c, 0xdc, 0xf6, 0xbd, 0x99, 0xf7, 0x7b, 0xbf, 0xdf, + 0x6f, 0xde, 0x3e, 0xf0, 0x80, 0xb5, 0x09, 0xc2, 0x61, 0xd8, 0x61, 0x04, 0x6b, 0x26, 0xb8, 0x42, + 0x8c, 0x6b, 0x2a, 0xc9, 0x3a, 0x66, 0xbc, 0x85, 0x09, 0x11, 0x5d, 0xae, 0x15, 0x22, 0x82, 0x6b, + 0x29, 0x3a, 0x1d, 0x2a, 0x51, 0x54, 0x41, 0x7a, 0x13, 0x86, 0x52, 0x68, 0x61, 0x57, 0x59, 0x9b, + 0xc0, 0xc3, 0xc5, 0xf0, 0x98, 0x62, 0xd8, 0x2f, 0x86, 0x51, 0xc5, 0x99, 0xf2, 0x85, 0x2f, 0x92, + 0x72, 0x14, 0x7f, 0x19, 0x24, 0xe7, 0xce, 0xa9, 0x68, 0x44, 0x15, 0x14, 0x62, 0xb2, 0x41, 0x75, + 0x5a, 0xb5, 0x3c, 0x00, 0xf9, 0x43, 0x6c, 0x0c, 0xc8, 0x34, 0x11, 0x2a, 0x10, 0x0a, 0x05, 0xca, + 0x8f, 0xcf, 0x03, 0xe5, 0xa7, 0x07, 0xd7, 0x62, 0x74, 0x22, 0x24, 0x45, 0x64, 0x1d, 0x73, 0x4e, + 0x3b, 0x49, 0xb9, 0xf9, 0x34, 0x57, 0xca, 0x5f, 0x2c, 0x70, 0xa5, 0xa1, 0x7c, 0x97, 0xfa, 0x4c, + 0x69, 0x2a, 0xeb, 0x59, 0xf7, 0x47, 0xa6, 0xb9, 0x3d, 0x05, 0x86, 0xc5, 0x6b, 0x4e, 0x65, 0xd1, + 0x9a, 0xb3, 0xe6, 0x47, 0x5d, 0x13, 0xd8, 0xd7, 0xc1, 0x05, 0x22, 0x38, 0xa7, 0x24, 0x26, 0xdd, + 0x62, 0x5e, 0xf1, 0xbf, 0xe4, 0x74, 0xbc, 0x9f, 0xac, 0x7b, 0x76, 0x11, 0x8c, 0x44, 0x54, 0x2a, + 0x26, 0x78, 0x71, 0x28, 0x39, 0xee, 0x85, 0xf6, 0x2d, 0x30, 0x2c, 0xa4, 0x47, 0x65, 0x31, 0x3f, + 0x67, 0xcd, 0x4f, 0x54, 0x1d, 0x18, 0x3f, 0x43, 0x4c, 0x14, 0xf6, 0xd8, 0x45, 0x15, 0xf8, 0x34, + 0xbe, 0xe1, 0x9a, 0x8b, 0xb5, 0x89, 0xf7, 0x5b, 0xb3, 0xb9, 0x77, 0x07, 0xdb, 0x0b, 0x86, 0x40, + 0xd9, 0x03, 0x37, 0x4e, 0xa2, 0xed, 0x52, 0x15, 0x0a, 0xae, 0xa8, 0x7d, 0x15, 0x80, 0x14, 0x32, + 0x66, 0x69, 0x34, 0x8c, 0xa6, 0x99, 0xba, 0x67, 0x4f, 0x83, 0x91, 0x50, 0x48, 0xdd, 0x57, 0x50, + 0x88, 0xc3, 0xba, 0x57, 0xcb, 0xc7, 0xfd, 0xca, 0x3f, 0x2d, 0x30, 0xda, 0x50, 0xfe, 0x33, 0xca, + 0xbd, 0xd5, 0xcd, 0xb3, 0x58, 0xb1, 0x01, 0xc6, 0xcc, 0xbb, 0xb7, 0x3c, 0xac, 0x71, 0x62, 0xc7, + 0x58, 0x75, 0x05, 0x9e, 0x6a, 0xfa, 0xa2, 0x0a, 0x3c, 0xa2, 0xaf, 0x99, 0x80, 0xad, 0x60, 0x8d, + 0x97, 0xf2, 0x3b, 0xdf, 0x67, 0x73, 0x2e, 0x08, 0xb3, 0x8c, 0x7d, 0x13, 0x5c, 0x94, 0xb4, 0x83, + 0x35, 0x8b, 0x68, 0x4b, 0xb3, 0x80, 0x8a, 0xae, 0x4e, 0x8c, 0xce, 0xbb, 0x93, 0xbd, 0xfc, 0xaa, + 0x49, 0x1f, 0xb1, 0xf5, 0x2e, 0xb8, 0x94, 0xe9, 0xcd, 0x3c, 0x74, 0xc0, 0xff, 0x8a, 0xbe, 0xea, + 0x52, 0x4e, 0x68, 0x22, 0x3d, 0xef, 0x66, 0x71, 0xea, 0xd3, 0x67, 0x0b, 0x4c, 0x36, 0x94, 0xff, + 0x3c, 0xf4, 0xb0, 0xa6, 0x4d, 0x2c, 0x71, 0xa0, 0xec, 0xcb, 0xa0, 0xa0, 0x98, 0xdf, 0xb7, 0x2b, + 0x8d, 0xec, 0x35, 0x50, 0x08, 0x93, 0x1b, 0x89, 0x51, 0x63, 0xd5, 0x1a, 0xfc, 0xf7, 0x7f, 0x10, + 0x9a, 0x1e, 0xa9, 0xf6, 0x14, 0xaf, 0x36, 0xd9, 0x13, 0x93, 0xb6, 0x2a, 0xcf, 0x80, 0xe9, 0x3f, + 0x58, 0xf5, 0x34, 0x55, 0x3f, 0xe4, 0xc1, 0x50, 0x43, 0xf9, 0xf6, 0x57, 0x0b, 0xcc, 0xfc, 0x7d, + 0xf8, 0x9b, 0x83, 0x70, 0x3b, 0x69, 0x2e, 0x9d, 0xb5, 0xf3, 0x46, 0xcc, 0x5e, 0xe9, 0xa3, 0x05, + 0x0a, 0xe9, 0xa0, 0x3e, 0x1c, 0xb0, 0x89, 0x29, 0x77, 0x1e, 0x9f, 0xa9, 0x3c, 0x23, 0xb4, 0x65, + 0x81, 0xf1, 0xdf, 0x26, 0x62, 0x79, 0x40, 0xdc, 0xc3, 0x20, 0xce, 0x93, 0x73, 0x00, 0xe9, 0x51, + 0x74, 0x86, 0xdf, 0x1e, 0x6c, 0x2f, 0x58, 0x4b, 0x2f, 0x77, 0xf6, 0x4a, 0xd6, 0xee, 0x5e, 0xc9, + 0xfa, 0xb1, 0x57, 0xb2, 0x3e, 0xed, 0x97, 0x72, 0xbb, 0xfb, 0xa5, 0xdc, 0xb7, 0xfd, 0x52, 0xee, + 0x45, 0xd3, 0x67, 0x7a, 0xbd, 0xdb, 0x86, 0x44, 0x04, 0x28, 0xdd, 0xb2, 0xac, 0x4d, 0x16, 0x7d, + 0x81, 0xa2, 0xfb, 0x28, 0x10, 0x5e, 0xb7, 0x43, 0x55, 0xbc, 0xbf, 0x15, 0xaa, 0xde, 0x5b, 0xec, + 0xf3, 0x58, 0x3c, 0x6e, 0x75, 0xeb, 0x37, 0x21, 0x55, 0xed, 0x42, 0xb2, 0x77, 0x6f, 0xff, 0x0a, + 0x00, 0x00, 0xff, 0xff, 0x5c, 0x9c, 0xca, 0xe1, 0xb7, 0x06, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -505,6 +509,11 @@ func (m *MsgRegisterInterchainAccount) MarshalToSizedBuffer(dAtA []byte) (int, e _ = i var l int _ = l + if m.Order != 0 { + i = encodeVarintTx(dAtA, i, uint64(m.Order)) + i-- + dAtA[i] = 0x20 + } if len(m.Version) > 0 { i -= len(m.Version) copy(dAtA[i:], m.Version) @@ -738,6 +747,9 @@ func (m *MsgRegisterInterchainAccount) Size() (n int) { if l > 0 { n += 1 + l + sovTx(uint64(l)) } + if m.Order != 0 { + n += 1 + sovTx(uint64(m.Order)) + } return n } @@ -947,6 +959,25 @@ func (m *MsgRegisterInterchainAccount) Unmarshal(dAtA []byte) error { } m.Version = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Order", wireType) + } + m.Order = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Order |= types.Order(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipTx(dAtA[iNdEx:]) diff --git a/modules/apps/27-interchain-accounts/host/ibc_module_test.go b/modules/apps/27-interchain-accounts/host/ibc_module_test.go index 827284ac310..5652805fe16 100644 --- a/modules/apps/27-interchain-accounts/host/ibc_module_test.go +++ b/modules/apps/27-interchain-accounts/host/ibc_module_test.go @@ -141,7 +141,12 @@ func (suite *InterchainAccountsTestSuite) TestOnChanOpenTry() { expPass bool }{ { - "success", func() {}, true, + "success w/ ORDERED channel", func() {}, true, + }, + { + "success w/ UNORDERED channel", func() { + channel.Ordering = channeltypes.UNORDERED + }, true, }, { "account address generation is block dependent", func() { @@ -170,11 +175,6 @@ func (suite *InterchainAccountsTestSuite) TestOnChanOpenTry() { } }, true, }, - { - "ICA callback fails - invalid channel order", func() { - channel.Ordering = channeltypes.UNORDERED - }, false, - }, } for _, tc := range testCases { diff --git a/modules/apps/27-interchain-accounts/host/keeper/handshake.go b/modules/apps/27-interchain-accounts/host/keeper/handshake.go index b38f779efbf..46e9e91fa8c 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/handshake.go +++ b/modules/apps/27-interchain-accounts/host/keeper/handshake.go @@ -30,10 +30,6 @@ func (k Keeper) OnChanOpenTry( counterparty channeltypes.Counterparty, counterpartyVersion string, ) (string, error) { - if order != channeltypes.ORDERED { - return "", errorsmod.Wrapf(channeltypes.ErrInvalidChannelOrdering, "expected %s channel, got %s", channeltypes.ORDERED, order) - } - if portID != icatypes.HostPortID { return "", errorsmod.Wrapf(icatypes.ErrInvalidHostPort, "expected %s, got %s", icatypes.HostPortID, portID) } @@ -54,18 +50,13 @@ func (k Keeper) OnChanOpenTry( panic(fmt.Errorf("active channel mapping set for %s but channel does not exist in channel store", activeChannelID)) } - if channel.IsOpen() { - return "", errorsmod.Wrapf(icatypes.ErrActiveChannelAlreadySet, "existing active channel %s for portID %s is already OPEN", activeChannelID, portID) - } - - appVersion, found := k.GetAppVersion(ctx, portID, activeChannelID) - if !found { - panic(fmt.Errorf("active channel mapping set for %s, but channel does not exist in channel store", activeChannelID)) + if channel.State != channeltypes.CLOSED { + return "", errorsmod.Wrapf(icatypes.ErrActiveChannelAlreadySet, "existing active channel %s for portID %s must be %s", activeChannelID, portID, channeltypes.CLOSED) } - if !icatypes.IsPreviousMetadataEqual(appVersion, metadata) { - return "", errorsmod.Wrap(icatypes.ErrInvalidVersion, "previous active channel metadata does not match provided version") - } + // if a channel is being reopened, we allow the controller to propose new fields + // which are not exactly the same as the previous. The provided address will + // be overwritten with the correct one before the metadata is returned. } // On the host chain the capability may only be claimed during the OnChanOpenTry @@ -139,19 +130,13 @@ func (Keeper) OnChanCloseConfirm( // The following may be changed: // - tx type (must be supported) // - encoding (must be supported) +// - order // // The following may not be changed: -// - order // - connectionHops (and subsequently host/controller connectionIDs) // - interchain account address // - ICS27 protocol version func (k Keeper) OnChanUpgradeTry(ctx sdk.Context, portID, channelID string, proposedOrder channeltypes.Order, proposedConnectionHops []string, counterpartyVersion string) (string, error) { - // verify order has not changed - // support for unordered ICA channels is not implemented yet - if proposedOrder != channeltypes.ORDERED { - return "", errorsmod.Wrapf(channeltypes.ErrInvalidChannelOrdering, "expected %s channel, got %s", channeltypes.ORDERED, proposedOrder) - } - if portID != icatypes.HostPortID { return "", errorsmod.Wrapf(porttypes.ErrInvalidPort, "expected %s, got %s", icatypes.HostPortID, portID) } diff --git a/modules/apps/27-interchain-accounts/host/keeper/handshake_test.go b/modules/apps/27-interchain-accounts/host/keeper/handshake_test.go index bb69322d40a..60b9e50a38d 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/handshake_test.go +++ b/modules/apps/27-interchain-accounts/host/keeper/handshake_test.go @@ -95,6 +95,25 @@ func (suite *KeeperTestSuite) TestOnChanOpenTry() { suite.Require().False(found) }, true, }, + { + "success - previous metadata is different", + func() { + // set the active channelID in state + suite.chainB.GetSimApp().ICAHostKeeper.SetActiveChannelID(suite.chainB.GetContext(), path.EndpointB.ConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointB.ChannelID) + + // set the previous encoding to be proto3json. + // the new encoding is set to be protobuf in the test below. + metadata.Encoding = icatypes.EncodingProto3JSON + + versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata) + suite.Require().NoError(err) + + channel.State = channeltypes.CLOSED + channel.Version = string(versionBytes) + + path.EndpointB.SetChannel(*channel) + }, true, + }, { "reopening account fails - no existing account", func() { @@ -148,35 +167,6 @@ func (suite *KeeperTestSuite) TestOnChanOpenTry() { }, false, }, - { - "invalid metadata - previous metadata is different", - func() { - // create a new channel and set it in state - ch := channeltypes.NewChannel(channeltypes.CLOSED, channeltypes.ORDERED, channeltypes.NewCounterparty(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID), []string{path.EndpointA.ConnectionID}, TestVersion) - suite.chainB.GetSimApp().GetIBCKeeper().ChannelKeeper.SetChannel(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, ch) - - // set the active channelID in state - suite.chainB.GetSimApp().ICAHostKeeper.SetActiveChannelID(suite.chainB.GetContext(), path.EndpointB.ConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointB.ChannelID) - - // attempt to downgrade version by reinitializing channel with version 1, but setting channel to version 2 - metadata.Version = "ics27-2" - - versionBytes, err := icatypes.ModuleCdc.MarshalJSON(&metadata) - suite.Require().NoError(err) - - channel.Version = string(versionBytes) - - path.EndpointB.SetChannel(*channel) - }, false, - }, - - { - "invalid order - UNORDERED", - func() { - channel.Ordering = channeltypes.UNORDERED - }, - false, - }, { "invalid port ID", func() { @@ -269,7 +259,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenTry() { false, }, { - "active channel already set", + "active channel already set (OPEN state)", func() { // create a new channel and set it in state ch := channeltypes.NewChannel(channeltypes.OPEN, channeltypes.ORDERED, channeltypes.NewCounterparty(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID), []string{path.EndpointA.ConnectionID}, ibctesting.DefaultChannelVersion) @@ -279,6 +269,23 @@ func (suite *KeeperTestSuite) TestOnChanOpenTry() { suite.chainB.GetSimApp().ICAHostKeeper.SetActiveChannelID(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointB.ChannelID) }, false, }, + { + "channel is already active (FLUSHING state)", + func() { + suite.chainB.GetSimApp().ICAHostKeeper.SetActiveChannelID(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID, path.EndpointB.ChannelID) + + counterparty := channeltypes.NewCounterparty(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + channel := channeltypes.Channel{ + State: channeltypes.FLUSHING, + Ordering: channeltypes.ORDERED, + Counterparty: counterparty, + ConnectionHops: []string{path.EndpointB.ConnectionID}, + Version: TestVersion, + } + suite.chainB.GetSimApp().IBCKeeper.ChannelKeeper.SetChannel(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, channel) + }, + false, + }, } for _, tc := range testCases { @@ -458,18 +465,18 @@ func (suite *KeeperTestSuite) TestOnChanUpgradeTry() { nil, }, { - name: "failure: invalid port ID", + name: "success: change order", malleate: func() { - path.EndpointB.ChannelConfig.PortID = "invalid-port-id" + order = channeltypes.UNORDERED }, - expError: porttypes.ErrInvalidPort, + expError: nil, }, { - name: "failure: invalid order", + name: "failure: invalid port ID", malleate: func() { - order = channeltypes.UNORDERED + path.EndpointB.ChannelConfig.PortID = "invalid-port-id" }, - expError: channeltypes.ErrInvalidChannelOrdering, + expError: porttypes.ErrInvalidPort, }, { name: "failure: invalid proposed connectionHops", diff --git a/modules/apps/callbacks/callbacks_test.go b/modules/apps/callbacks/callbacks_test.go index 7f5d5185aa8..c1329417de9 100644 --- a/modules/apps/callbacks/callbacks_test.go +++ b/modules/apps/callbacks/callbacks_test.go @@ -24,6 +24,7 @@ import ( icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v8/modules/core/05-port/types" ibctesting "github.com/cosmos/ibc-go/v8/testing" ibcmock "github.com/cosmos/ibc-go/v8/testing/mock" @@ -153,7 +154,7 @@ func (s *CallbacksTestSuite) SetupICATest() string { // RegisterInterchainAccount submits a MsgRegisterInterchainAccount and updates the controller endpoint with the // channel created. func (s *CallbacksTestSuite) RegisterInterchainAccount(owner string) { - msgRegister := icacontrollertypes.NewMsgRegisterInterchainAccount(s.path.EndpointA.ConnectionID, owner, s.path.EndpointA.ChannelConfig.Version) + msgRegister := icacontrollertypes.NewMsgRegisterInterchainAccount(s.path.EndpointA.ConnectionID, owner, s.path.EndpointA.ChannelConfig.Version, channeltypes.ORDERED) res, err := s.chainA.SendMsgs(msgRegister) s.Require().NotEmpty(res) diff --git a/proto/ibc/applications/interchain_accounts/controller/v1/tx.proto b/proto/ibc/applications/interchain_accounts/controller/v1/tx.proto index 1287cfa2d37..db0c7b77e39 100644 --- a/proto/ibc/applications/interchain_accounts/controller/v1/tx.proto +++ b/proto/ibc/applications/interchain_accounts/controller/v1/tx.proto @@ -8,6 +8,7 @@ import "gogoproto/gogo.proto"; import "ibc/applications/interchain_accounts/v1/packet.proto"; import "ibc/applications/interchain_accounts/controller/v1/controller.proto"; import "cosmos/msg/v1/msg.proto"; +import "ibc/core/channel/v1/channel.proto"; // Msg defines the 27-interchain-accounts/controller Msg service. service Msg { @@ -27,9 +28,10 @@ message MsgRegisterInterchainAccount { option (gogoproto.goproto_getters) = false; - string owner = 1; - string connection_id = 2; - string version = 3; + string owner = 1; + string connection_id = 2; + string version = 3; + ibc.core.channel.v1.Order order = 4; } // MsgRegisterInterchainAccountResponse defines the response for Msg/RegisterAccount