From 7ad02c0e646b500f88b0f1a09001fa21a7fcd0b1 Mon Sep 17 00:00:00 2001 From: Carlos Rodriguez Date: Tue, 16 Jan 2024 12:56:59 +0100 Subject: [PATCH] remaining review comments from #1613 (#5539) * e2e test: send incentivised packet after upgrade, add extra tests for cbs * update hermes docker image * add prune acknowledgements to successful upgrade test * use correct tx response * getting further with the e2e test, addressing a couple of other review items * refactor test to use sync incentivization instead of async * update hermes image tag * revert change that was breaking a test * Apply suggestions from code review Co-authored-by: Damian Nolan Co-authored-by: DimitrisJim * rename variables for consistency * rename variables for clarification --------- Co-authored-by: Damian Nolan Co-authored-by: DimitrisJim (cherry picked from commit 5d772213da10f17c9be02f7889bc25fb31df371e) # Conflicts: # e2e/README.md # e2e/tests/core/04-channel/upgrades_test.go # e2e/tests/transfer/incentivized_test.go # e2e/testsuite/grpc_query.go # e2e/testsuite/tx.go --- e2e/README.md | 421 ++++++++++ e2e/tests/core/04-channel/upgrades_test.go | 327 ++++++++ e2e/tests/transfer/incentivized_test.go | 730 ++++++++++++++++++ e2e/testsuite/grpc_query.go | 478 ++++++++++++ e2e/testsuite/tx.go | 299 +++++++ .../controller/ibc_middleware_test.go | 27 + .../host/ibc_module_test.go | 52 ++ modules/core/04-channel/types/msgs.go | 4 +- 8 files changed, 2337 insertions(+), 1 deletion(-) create mode 100644 e2e/README.md create mode 100644 e2e/tests/core/04-channel/upgrades_test.go create mode 100644 e2e/tests/transfer/incentivized_test.go create mode 100644 e2e/testsuite/grpc_query.go create mode 100644 e2e/testsuite/tx.go diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000000..306b52cf1ac --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,421 @@ +# Table of Contents + +1. [How to write tests](#how-to-write-tests) + - a. [Adding a new test](#adding-a-new-test) + - b. [Running the tests with custom images](#running-tests-with-custom-images) + - b. [Code samples](#code-samples) + - [Setup](#setup) + - [Creating test users](#creating-test-users) + - [Waiting](#waiting) + - [Query wallet balances](#query-wallet-balances) + - [Broadcasting messages](#broadcasting-messages) + - [Starting the relayer](#starting-the-relayer) + - [Arbitrary commands](#arbitrary-commands) + - [IBC transfer](#ibc-transfer) +2. [Test design](#test-design) + - a. [interchaintest](#interchaintest) + - b. [CI configuration](#ci-configuration) +3. [Github Workflows](#github-workflows) +4. [Running Compatibility Tests](#running-compatibility-tests) +5. [Troubleshooting](#troubleshooting) +6. [Importable Workflow](#importable-workflow) + +# How to write tests + +## Adding a new test + +All tests should go under the [e2e](https://github.com/cosmos/ibc-go/tree/main/e2e) directory. When adding a new test, either add a new test function +to an existing test suite ***in the same file***, or create a new test suite in a new file and add test functions there. +New test files should follow the convention of `module_name_test.go`. + +After creating a new test file, be sure to add a build constraint that ensures this file will **not** be included in the package to be built when +running tests locally via `make test`. For an example of this, see any of the existing test files. + +New test suites should be composed of `testsuite.E2ETestSuite`. This type has lots of useful helper functionality that will +be quite common in most tests. + +> Note: see [here](#how-tests-are-run) for details about these requirements. + +## Running tests with custom images + +Tests can be run using a Makefile target under the e2e directory. `e2e/Makefile` + +The tests can be configured using a configuration file or environment variables. + +See [the example](./sample.config.yaml) to get started. The default location the tests look is `~/.ibc-go-e2e-config.yaml` +But this can be specified directly using the `E2E_CONFIG_PATH` environment variable. + +There are several environment variables that alter the behaviour of the make target which will override any +options specified in your config file. + +| Environment Variable | Description | Default Value | +|----------------------|-------------------------------------------|---------------| +| CHAIN_IMAGE | The image that will be used for the chain | ibc-go-simd | +| CHAIN_A_TAG | The tag used for chain A | latest | +| CHAIN_B_TAG | The tag used for chain B | latest | +| CHAIN_BINARY | The binary used in the container | simd | +| RELAYER_TAG | The tag used for the relayer | main | +| RELAYER_ID | The type of relayer to use (rly/hermes) | hermes | + +> Note: when running tests locally, **no images are pushed** to the `ghcr.io/cosmos/ibc-go-simd` registry. +The images which are used only exist on your machine. + +These environment variables allow us to run tests with arbitrary versions (from branches or released) of simd +and the go relayer. + +Every time changes are pushed to a branch or to `main`, a new `simd` image is built and pushed [here](https://github.com/cosmos/ibc-go/pkgs/container/ibc-go-simd). + +### Example Command + +```sh +export CHAIN_IMAGE="ghcr.io/cosmos/ibc-go-simd" +export CHAIN_A_TAG="main" +export CHAIN_BINARY="simd" + +# We can also specify different values for the chains if needed. +# they will default to the same as chain a. +# export CHAIN_B_TAG="main" + +export RELAYER_TAG="v2.0.0" +make e2e-test entrypoint=TestInterchainAccountsTestSuite test=TestMsgSubmitTx_SuccessfulTransfer +``` + +If `jq` is installed, you only need to specify the `test`. + +If `fzf` is also installed, you only need to run `make e2e-test` and you will be prompted with interactive test selection. + +```sh +make e2e-test test=TestMsgSubmitTx_SuccessfulTransfer +``` + +> Note: sometimes it can be useful to make changes to [ibctest](https://github.com/strangelove-ventures/interchaintest) when running tests locally. In order to do this, add the following line to +e2e/go.mod + +`replace github.com/strangelove-ventures/interchaintest => ../ibctest` + +Or point it to any local checkout you have. + +### Running tests in CI + +To run tests in CI, you can checkout the ibc-go repo and provide these environment variables +to the CI task. + +[This repo](https://github.com/chatton/ibc-go-e2e-demo) contains an example of how to do this with Github Actions. + +## Code samples + +### Setup + +Every standard test will start with this. This creates two chains and a relayer, +initializes relayer accounts on both chains, establishes a connection and a channel +between the chains. + +Both chains have started, but the relayer is not yet started. + +The relayer should be started as part of the test if required. See [Starting the Relayer](#starting-the-relayer) + +```go +relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, feeMiddlewareChannelOptions()) +chainA, chainB := s.GetChains() +``` + +### Creating test users + +There are helper functions to easily create users on both chains. + +```go +chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) +chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) +``` + +### Waiting + +We can wait for some number of blocks on the specified chains if required. + +```go +chainA, chainB := s.GetChains() +err := test.WaitForBlocks(ctx, 1, chainA, chainB) +s.Require().NoError(err) +``` + +### Query wallet balances + +We can fetch balances of wallets on specific chains. + +```go +chainABalance, err := s.GetChainANativeBalance(ctx, chainAWallet) +s.Require().NoError(err) +``` + +### Broadcasting messages + +We can broadcast arbitrary messages which are signed on behalf of users created in the test. + +This example shows a multi message transaction being broadcast on chainA and signed on behalf of chainAWallet. + +```go +relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, feeMiddlewareChannelOptions()) +chainA, chainB := s.GetChains() + +chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) +chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + +t.Run("broadcast multi message transaction", func(t *testing.T){ + msgPayPacketFee := feetypes.NewMsgPayPacketFee(testFee, channelA.PortID, channelA.ChannelID, chainAWallet.Bech32Address(chainA.Config().Bech32Prefix), nil) + msgTransfer := transfertypes.NewMsgTransfer(channelA.PortID, channelA.ChannelID, transferAmount, chainAWallet.Bech32Address(chainA.Config().Bech32Prefix), chainBWallet.Bech32Address(chainB.Config().Bech32Prefix), clienttypes.NewHeight(1, 1000), 0) + resp, err := s.BroadcastMessages(ctx, chainA, chainAWallet, msgPayPacketFee, msgTransfer) + + s.AssertValidTxResponse(resp) + s.Require().NoError(err) +}) +``` + +### Starting the relayer + +The relayer can be started with the following. + +```go +t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) +}) +``` + +### Arbitrary commands + +Arbitrary commands can be executed on a given chain. + +> Note: these commands will be fully configured to run on the chain executed on (home directory, ports configured etc.) + +However, it is preferable to [broadcast messages](#broadcasting-messages) or use a gRPC query if possible. + +```go +stdout, stderr, err := chainA.Exec(ctx, []string{"tx", "..."}, nil) +``` + +### IBC transfer + +It is possible to send an IBC transfer in two ways. + +Use the ibctest `Chain` interface (this ultimately does a docker exec) + +```go +t.Run("send IBC transfer", func(t *testing.T) { + chainATx, err = chainA.SendIBCTransfer(ctx, channelA.ChannelID, chainAWallet.KeyName, walletAmount, nil) + s.Require().NoError(err) + s.Require().NoError(chainATx.Validate(), "chain-a ibc transfer tx is invalid") +}) +``` + +Broadcast a `MsgTransfer`. + +```go +t.Run("send IBC transfer", func(t *testing.T){ + transferMsg := transfertypes.NewMsgTransfer(channelA.PortID, channelA.ChannelID, transferAmount, chainAWallet.Bech32Address(chainA.Config().Bech32Prefix), chainBWallet.Bech32Address(chainB.Config().Bech32Prefix), clienttypes.NewHeight(1, 1000), 0) + resp, err := s.BroadcastMessages(ctx, chainA, chainAWallet, transferMsg) + s.AssertValidTxResponse(resp) + s.Require().NoError(err) +}) +``` + +## Test design + +### interchaintest + +These E2E tests use the [interchaintest framework](https://github.com/strangelove-ventures/interchaintest). This framework creates chains and relayers in containers and allows for arbitrary commands to be executed in the chain containers, +as well as allowing us to broadcast arbitrary messages which are signed on behalf of a user created in the test. + +### CI configuration + +There are two main github actions for e2e tests. + +[e2e.yaml](https://github.com/cosmos/ibc-go/blob/main/.github/workflows/e2e.yaml) which runs when collaborators create branches. + +[e2e-fork.yaml](https://github.com/cosmos/ibc-go/blob/main/.github/workflows/e2e-fork.yml) which runs when forks are created. + +In `e2e.yaml`, the `simd` image is built and pushed to [a registry](https://github.com/cosmos/ibc-go/pkgs/container/ibc-go-simd) and every test +that is run uses the image that was built. + +In `e2e-fork.yaml`, images are not pushed to this registry, but instead remain local to the host runner. + +## How tests are run + +The tests use the `matrix` feature of Github Actions. The matrix is +dynamically generated using [this command](https://github.com/cosmos/ibc-go/blob/main/cmd/build_test_matrix/main.go). + +> Note: there is currently a limitation that all tests belonging to a test suite must be in the same file. +In order to support test functions spread in different files, we would either need to manually maintain a matrix +or update the script to account for this. The script assumes there is a single test suite per test file to avoid an overly complex +generation process. + +Which looks under the `e2e` directory, and creates a task for each test suite function. + +### Example + +```go +// e2e/file_one_test.go +package e2e + +func TestFeeMiddlewareTestSuite(t *testing.T) { + suite.Run(t, new(FeeMiddlewareTestSuite)) +} + +type FeeMiddlewareTestSuite struct { + testsuite.E2ETestSuite +} + +func (s *FeeMiddlewareTestSuite) TestA() {} +func (s *FeeMiddlewareTestSuite) TestB() {} +func (s *FeeMiddlewareTestSuite) TestC() {} + +``` + +```go +// e2e/file_two_test.go +package e2e + +func TestTransferTestSuite(t *testing.T) { + suite.Run(t, new(TransferTestSuite)) +} + +type TransferTestSuite struct { + testsuite.E2ETestSuite +} + +func (s *TransferTestSuite) TestD() {} +func (s *TransferTestSuite) TestE() {} +func (s *TransferTestSuite) TestF() {} + +``` + +In the above example, the following would be generated. + +```json +{ + "include": [ + { + "entrypoint": "TestFeeMiddlewareTestSuite", + "test": "TestA" + }, + { + "entrypoint": "TestFeeMiddlewareTestSuite", + "test": "TestB" + }, + { + "entrypoint": "TestFeeMiddlewareTestSuite", + "test": "TestC" + }, + { + "entrypoint": "TestTransferTestSuite", + "test": "TestD" + }, + { + "entrypoint": "TestTransferTestSuite", + "test": "TestE" + }, + { + "entrypoint": "TestTransferTestSuite", + "test": "TestF" + } + ] +} +``` + +This string is used to generate a test matrix in the Github Action that runs the E2E tests. + +All tests will be run on different hosts. + +### Misceleneous + +## GitHub Workflows + +### Building and pushing a `simd` image + +If we ever need to manually build and push an image, we can do so with the [Build Simd Image](../.github/workflows/build-simd-image-from-tag.yml) GitHub workflow. + +This can be triggered manually from the UI by navigating to + +`Actions` -> `Build Simd Image` -> `Run Workflow` + +And providing the git tag. + +Alternatively, the [gh](https://cli.github.com/) CLI tool can be used to trigger this workflow. + +```bash +gh workflow run "Build Simd Image" -f tag=v3.0.0 +``` + +## Running Compatibility Tests + +To trigger the compatibility tests for a release branch, you can use the following command. + +```bash +make compatibility-tests release_branch=release/v5.0.x +``` + +This will build an image from the tip of the release branch and run all tests specified in the corresponding +json matrix files under .github/compatibility-test-matrices and is equivalent to going to the Github UI and navigating to + +`Actions` -> `Compatibility E2E` -> `Run Workflow` -> `release/v5.0.x` + +## Troubleshooting + +- On Mac, after running a lot of tests, it can happen that containers start failing. To fix this, you can try clearing existing containers and restarting the docker daemon. + + This generally manifests itself as relayer or simd containers timing out during setup stages of the test. This doesn't happen in CI. + + ```bash + # delete all images + docker system prune -af + ``` + + This issue doesn't seem to occur on other operating systems. + +### Accessing Logs + +- When a test fails in GitHub. The logs of the test will be uploaded (viewable in the summary page of the workflow). Note: There + may be some discrepancy in the logs collected and the output of interchain test. The containers may run for a some + time after the logs are collected, resulting in the displayed logs to differ slightly. + +## Importable Workflow + +This repository contains an [importable workflow](https://github.com/cosmos/ibc-go/blob/185a220244663457372185992cfc85ed9e458bf1/.github/workflows/e2e-compatibility-workflow-call.yaml) that can be used from any other repository to test chain upgrades. The workflow +can be used to test both non-IBC chains, and also IBC-enabled chains. + +### Prerequisites + +- In order to run this workflow, a docker container is required with tags for the versions you want to test. + +- Have an upgrade handler in the chain binary which is being upgraded to. + +> It's worth noting that all github repositories come with a built-in docker registry that makes it convenient to build and push images to. + +[This workflow](https://github.com/cosmos/ibc-go/blob/1da651e5e117872499e3558c2a92f887369ae262/.github/workflows/release.yml#L35-L61) can be used as a reference for how to build a docker image +whenever a git tag is pushed. + +### How to import the workflow + +You can refer to [this example](https://github.com/cosmos/ibc-go/blob/2933906d1ed25ae6dce7b7d93aa429dfa94c5a23/.github/workflows/e2e-upgrade.yaml#L9-L19) when including this workflow in your repo. + +The referenced job will do the following: + +- Create two chains using the image found at `ghcr.io/cosmos/ibc-go-simd:v4.3.0`. +- Perform IBC transfers verifying core functionality. +- Upgrade chain A to `ghcr.io/cosmos/ibc-go-simd:v5.1.0` by executing a governance proposal and using the plan name `normal upgrade`. +- Perform additional IBC transfers and verifies the upgrade and migrations ran successfully. + +> Note: The plan name will always be specific to your chain. In this instance `normal upgrade` is referring to [this upgrade handler](https://github.com/cosmos/ibc-go/blob/e9bc0bac38e84e1380ec08552cae15821143a6b6/testing/simapp/app.go#L923) + +### Workflow Options + +| Workflow Field | Purpose | +|-------------------|---------------------------------------------------| +| chain-image | The docker image to use for the test | +| chain-a-tag | The tag of chain A to use | +| chain-b-tag | The tag of chain B to use | +| chain-upgrade-tag | The tag chain A should be upgraded to | +| chain-binary | The chain binary name | +| upgrade-plan-name | The name of the upgrade plan to execute | +| test-entry-point | Always TestUpgradeTestSuite | +| test | Should be TestIBCChainUpgrade or TestChainUpgrade | + +> TestIBCChainUpgrade should be used for ibc tests, while TestChainUpgrade should be used for single chain tests. 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..2e7a8a8f60c --- /dev/null +++ b/e2e/tests/core/04-channel/upgrades_test.go @@ -0,0 +1,327 @@ +//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/transfer/incentivized_test.go b/e2e/tests/transfer/incentivized_test.go new file mode 100644 index 00000000000..4bcfc14b1af --- /dev/null +++ b/e2e/tests/transfer/incentivized_test.go @@ -0,0 +1,730 @@ +//go:build !test_e2e + +package transfer + +import ( + "context" + "testing" + "time" + + "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" + + "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" +) + +type IncentivizedTransferTestSuite struct { + TransferTestSuite +} + +func TestIncentivizedTransferTestSuite(t *testing.T) { + testifysuite.Run(t, new(IncentivizedTransferTestSuite)) +} + +func (s *IncentivizedTransferTestSuite) TestMsgPayPacketFee_AsyncSingleSender_Succeeds() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, feeMiddlewareChannelOptions()) + chainA, chainB := s.GetChains() + + var ( + chainADenom = chainA.Config().Denom + testFee = testvalues.DefaultFee(chainADenom) + chainATx ibc.Tx + payPacketFeeTxResp sdk.TxResponse + ) + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + 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, 1, chainA, chainB), "failed to wait for blocks") + + _, chainBRelayerUser := s.GetRelayerUsers(ctx) + + t.Run("register counterparty payee", func(t *testing.T) { + resp := s.RegisterCounterPartyPayee(ctx, chainB, chainBRelayerUser, channelA.Counterparty.PortID, channelA.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(), channelA.Counterparty.ChannelID) + s.Require().NoError(err) + s.Require().Equal(chainARelayerWallet.FormattedAddress(), address) + }) + + walletAmount := ibc.WalletAmount{ + Address: chainAWallet.FormattedAddress(), // destination address + Denom: chainADenom, + Amount: sdkmath.NewInt(testvalues.IBCTransferAmount), + } + + t.Run("send IBC transfer", func(t *testing.T) { + chainATx, err = chainA.SendIBCTransfer(ctx, channelA.ChannelID, chainAWallet.KeyName(), walletAmount, ibc.TransferOptions{}) + s.Require().NoError(err) + s.Require().NoError(chainATx.Validate(), "chain-a ibc transfer tx is invalid") + }) + + t.Run("tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - walletAmount.Amount.Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("pay packet fee", func(t *testing.T) { + t.Run("no incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + packetID := channeltypes.NewPacketID(channelA.PortID, channelA.ChannelID, chainATx.Packet.Sequence) + packetFee := feetypes.NewPacketFee(testFee, chainAWallet.FormattedAddress(), nil) + + t.Run("should succeed", func(t *testing.T) { + payPacketFeeTxResp = s.PayPacketFeeAsync(ctx, chainA, chainAWallet, packetID, packetFee) + s.AssertTxSuccess(payPacketFeeTxResp) + }) + + t.Run("there should be incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.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("balance should be lowered by sum of recv ack and timeout", func(t *testing.T) { + // The balance should be lowered by the sum of the recv, ack and timeout fees. + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - walletAmount.Amount.Int64() - testFee.Total().AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + }) + + 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, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + 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. + expected := testvalues.StartingTokenAmount - walletAmount.Amount.Int64() - testFee.AckFee.AmountOf(chainADenom).Int64() - testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) +} + +func (s *IncentivizedTransferTestSuite) TestMsgPayPacketFee_InvalidReceiverAccount() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, feeMiddlewareChannelOptions()) + chainA, chainB := s.GetChains() + + var ( + chainADenom = chainA.Config().Denom + testFee = testvalues.DefaultFee(chainADenom) + payPacketFeeTxResp sdk.TxResponse + ) + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + 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, 1, chainA, chainB), "failed to wait for blocks") + + _, chainBRelayerUser := s.GetRelayerUsers(ctx) + + t.Run("register counterparty payee", func(t *testing.T) { + resp := s.RegisterCounterPartyPayee(ctx, chainB, chainBRelayerUser, channelA.Counterparty.PortID, channelA.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(), channelA.Counterparty.ChannelID) + s.Require().NoError(err) + s.Require().Equal(chainARelayerWallet.FormattedAddress(), address) + }) + + transferAmount := testvalues.DefaultTransferAmount(chainADenom) + + t.Run("send IBC transfer", func(t *testing.T) { + msgTransfer := transfertypes.NewMsgTransfer(channelA.PortID, channelA.ChannelID, transferAmount, chainAWallet.FormattedAddress(), testvalues.InvalidAddress, s.GetTimeoutHeight(ctx, chainB), 0, "") + txResp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgTransfer) + // this message should be successful, as receiver account is not validated on the sending chain. + s.AssertTxSuccess(txResp) + }) + + t.Run("tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - transferAmount.Amount.Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("pay packet fee", func(t *testing.T) { + t.Run("no incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + packetID := channeltypes.NewPacketID(channelA.PortID, channelA.ChannelID, 1) + packetFee := feetypes.NewPacketFee(testFee, chainAWallet.FormattedAddress(), nil) + + t.Run("should succeed", func(t *testing.T) { + payPacketFeeTxResp = s.PayPacketFeeAsync(ctx, chainA, chainAWallet, packetID, packetFee) + s.AssertTxSuccess(payPacketFeeTxResp) + }) + + t.Run("there should be incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.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("balance should be lowered by sum of recv, ack and timeout", func(t *testing.T) { + // The balance should be lowered by the sum of the recv, ack and timeout fees. + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - transferAmount.Amount.Int64() - testFee.Total().AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + }) + + 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, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + t.Run("timeout fee and transfer amount 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. + // the address was invalid so the amount sent should be unescrowed. + expected := testvalues.StartingTokenAmount - testFee.AckFee.AmountOf(chainADenom).Int64() - testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance, "the amount sent and timeout fee should have been refunded as there was an invalid receiver address provided") + }) +} + +func (s *IncentivizedTransferTestSuite) TestMultiMsg_MsgPayPacketFeeSingleSender() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, feeMiddlewareChannelOptions()) + + chainA, chainB := s.GetChains() + + var ( + chainADenom = chainA.Config().Denom + testFee = testvalues.DefaultFee(chainADenom) + multiMsgTxResponse sdk.TxResponse + ) + + transferAmount := testvalues.DefaultTransferAmount(chainA.Config().Denom) + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + 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, 1, 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) + + t.Run("register counterparty payee", func(t *testing.T) { + multiMsgTxResponse = s.RegisterCounterPartyPayee(ctx, chainB, chainBRelayerUser, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, chainBRelayerWallet.FormattedAddress(), chainARelayerWallet.FormattedAddress()) + s.AssertTxSuccess(multiMsgTxResponse) + }) + + t.Run("verify counterparty payee", func(t *testing.T) { + address, err := s.QueryCounterPartyPayee(ctx, chainB, chainBRelayerWallet.FormattedAddress(), channelA.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, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + 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("there should be incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.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("balance should be lowered by sum of recv ack and timeout", func(t *testing.T) { + // The balance should be lowered by the sum of the recv, ack and timeout fees. + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount - testFee.Total().AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + + 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, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + 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. + expected := testvalues.StartingTokenAmount - 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, chainARelayerUser) + s.Require().NoError(err) + expected := relayerAStartingBalance + testFee.AckFee.AmountOf(chainADenom).Int64() + testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) +} + +func (s *IncentivizedTransferTestSuite) TestMsgPayPacketFee_SingleSender_TimesOut() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, feeMiddlewareChannelOptions()) + chainA, chainB := s.GetChains() + + var ( + chainADenom = chainA.Config().Denom + testFee = testvalues.DefaultFee(chainADenom) + chainATx ibc.Tx + payPacketFeeTxResp sdk.TxResponse + ) + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + + t.Run("relayer wallets recovered", func(t *testing.T) { + s.Require().NoError(s.RecoverRelayerWallets(ctx, relayer)) + }) + + chainARelayerWallet, chainBRelayerWallet, err := s.GetRelayerWallets(relayer) + t.Run("relayer wallets fetched", func(t *testing.T) { + s.Require().NoError(err) + }) + + _, chainBRelayerUser := s.GetRelayerUsers(ctx) + + t.Run("register counterparty payee", func(t *testing.T) { + resp := s.RegisterCounterPartyPayee(ctx, chainB, chainBRelayerUser, channelA.Counterparty.PortID, channelA.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(), channelA.Counterparty.ChannelID) + s.Require().NoError(err) + s.Require().Equal(chainARelayerWallet.FormattedAddress(), address) + }) + + chainBWalletAmount := ibc.WalletAmount{ + Address: chainBWallet.FormattedAddress(), // destination address + Denom: chainA.Config().Denom, + Amount: sdkmath.NewInt(testvalues.IBCTransferAmount), + } + + t.Run("Send IBC transfer", func(t *testing.T) { + chainATx, err = chainA.SendIBCTransfer(ctx, channelA.ChannelID, chainAWallet.KeyName(), chainBWalletAmount, ibc.TransferOptions{Timeout: testvalues.ImmediatelyTimeout()}) + s.Require().NoError(err) + s.Require().NoError(chainATx.Validate(), "source ibc transfer tx is invalid") + time.Sleep(time.Nanosecond * 1) // want it to timeout immediately + }) + + t.Run("tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - chainBWalletAmount.Amount.Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("pay packet fee", func(t *testing.T) { + packetID := channeltypes.NewPacketID(channelA.PortID, channelA.ChannelID, chainATx.Packet.Sequence) + packetFee := feetypes.NewPacketFee(testFee, chainAWallet.FormattedAddress(), nil) + + t.Run("no incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + t.Run("should succeed", func(t *testing.T) { + payPacketFeeTxResp = s.PayPacketFeeAsync(ctx, chainA, chainAWallet, packetID, packetFee) + s.AssertTxSuccess(payPacketFeeTxResp) + }) + + t.Run("there should be incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.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("balance should be lowered by sum of recv ack and timeout", func(t *testing.T) { + // The balance should be lowered by the sum of the recv, ack and timeout fees. + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - chainBWalletAmount.Amount.Int64() - testFee.Total().AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + }) + + 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, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + t.Run("recv and ack should be refunded", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testFee.TimeoutFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) +} + +func (s *IncentivizedTransferTestSuite) TestPayPacketFeeAsync_SingleSender_NoCounterPartyAddress() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, feeMiddlewareChannelOptions()) + chainA, _ := s.GetChains() + + var ( + chainADenom = chainA.Config().Denom + testFee = testvalues.DefaultFee(chainADenom) + chainATx ibc.Tx + payPacketFeeTxResp sdk.TxResponse + ) + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + t.Run("relayer wallets recovered", func(t *testing.T) { + err := s.RecoverRelayerWallets(ctx, relayer) + s.Require().NoError(err) + }) + + chainBWalletAmount := ibc.WalletAmount{ + Address: chainAWallet.FormattedAddress(), // destination address + Denom: chainADenom, + Amount: sdkmath.NewInt(testvalues.IBCTransferAmount), + } + + t.Run("send IBC transfer", func(t *testing.T) { + var err error + chainATx, err = chainA.SendIBCTransfer(ctx, channelA.ChannelID, chainAWallet.KeyName(), chainBWalletAmount, ibc.TransferOptions{}) + s.Require().NoError(err) + s.Require().NoError(chainATx.Validate(), "source ibc transfer tx is invalid") + }) + + t.Run("tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - chainBWalletAmount.Amount.Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("pay packet fee", func(t *testing.T) { + t.Run("no incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + packetID := channeltypes.NewPacketID(channelA.PortID, channelA.ChannelID, chainATx.Packet.Sequence) + packetFee := feetypes.NewPacketFee(testFee, chainAWallet.FormattedAddress(), nil) + + t.Run("should succeed", func(t *testing.T) { + payPacketFeeTxResp = s.PayPacketFeeAsync(ctx, chainA, chainAWallet, packetID, packetFee) + s.AssertTxSuccess(payPacketFeeTxResp) + }) + + t.Run("should be incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.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("balance should be lowered by sum of recv, ack and timeout", func(t *testing.T) { + // The balance should be lowered by the sum of the recv, ack and timeout fees. + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - chainBWalletAmount.Amount.Int64() - testFee.Total().AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("with no counterparty address", func(t *testing.T) { + 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("timeout and recv fee are refunded", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + // once the relayer has relayed the packets, the timeout and recv fee should be refunded. + expected := testvalues.StartingTokenAmount - chainBWalletAmount.Amount.Int64() - testFee.AckFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + }) +} + +func (s *IncentivizedTransferTestSuite) TestMsgPayPacketFee_AsyncMultipleSenders_Succeeds() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, feeMiddlewareChannelOptions()) + chainA, chainB := s.GetChains() + + var ( + chainADenom = chainA.Config().Denom + testFee = testvalues.DefaultFee(chainADenom) + chainATx ibc.Tx + payPacketFeeTxResp sdk.TxResponse + ) + + chainAWallet1 := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainAWallet2 := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + t.Run("relayer wallets recovered", func(t *testing.T) { + err := s.RecoverRelayerWallets(ctx, relayer) + s.Require().NoError(err) + }) + + chainARelayerWallet, chainBRelayerWallet, err := s.GetRelayerWallets(relayer) + s.Require().NoError(err) + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + + _, chainBRelayerUser := s.GetRelayerUsers(ctx) + + t.Run("register counterparty payee", func(t *testing.T) { + resp := s.RegisterCounterPartyPayee(ctx, chainB, chainBRelayerUser, channelA.Counterparty.PortID, channelA.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(), channelA.Counterparty.ChannelID) + s.Require().NoError(err) + s.Require().Equal(chainARelayerWallet.FormattedAddress(), address) + }) + + walletAmount1 := ibc.WalletAmount{ + Address: chainAWallet1.FormattedAddress(), // destination address + Denom: chainADenom, + Amount: sdkmath.NewInt(testvalues.IBCTransferAmount), + } + + t.Run("send IBC transfer", func(t *testing.T) { + chainATx, err = chainA.SendIBCTransfer(ctx, channelA.ChannelID, chainAWallet1.KeyName(), walletAmount1, ibc.TransferOptions{}) + s.Require().NoError(err) + s.Require().NoError(chainATx.Validate(), "chain-a ibc transfer tx is invalid") + }) + + t.Run("tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet1) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - walletAmount1.Amount.Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("pay packet fee", func(t *testing.T) { + t.Run("no incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + packetID := channeltypes.NewPacketID(channelA.PortID, channelA.ChannelID, chainATx.Packet.Sequence) + packetFee1 := feetypes.NewPacketFee(testFee, chainAWallet1.FormattedAddress(), nil) + packetFee2 := feetypes.NewPacketFee(testFee, chainAWallet2.FormattedAddress(), nil) + + t.Run("paying packetFee1 should succeed", func(t *testing.T) { + payPacketFeeTxResp = s.PayPacketFeeAsync(ctx, chainA, chainAWallet1, packetID, packetFee1) + s.AssertTxSuccess(payPacketFeeTxResp) + }) + t.Run("paying packetFee2 should succeed", func(t *testing.T) { + payPacketFeeTxResp = s.PayPacketFeeAsync(ctx, chainA, chainAWallet2, packetID, packetFee2) + s.AssertTxSuccess(payPacketFeeTxResp) + }) + + t.Run("there should be incentivized packets", func(t *testing.T) { + packets, err := s.QueryIncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Len(packets, 1) + actualFee1 := packets[0].PacketFees[0].Fee + actualFee2 := packets[0].PacketFees[1].Fee + s.Require().Len(packets[0].PacketFees, 2) + + s.Require().True(actualFee1.RecvFee.Equal(testFee.RecvFee)) + s.Require().True(actualFee1.AckFee.Equal(testFee.AckFee)) + s.Require().True(actualFee1.TimeoutFee.Equal(testFee.TimeoutFee)) + + s.Require().True(actualFee2.RecvFee.Equal(testFee.RecvFee)) + s.Require().True(actualFee2.AckFee.Equal(testFee.AckFee)) + s.Require().True(actualFee2.TimeoutFee.Equal(testFee.TimeoutFee)) + }) + + t.Run("balance of chainAWallet1 should be lowered by sum of recv ack and timeout", func(t *testing.T) { + actualBalance1, err := s.GetChainANativeBalance(ctx, chainAWallet1) + s.Require().NoError(err) + + expected1 := testvalues.StartingTokenAmount - walletAmount1.Amount.Int64() - testFee.Total().AmountOf(chainADenom).Int64() + s.Require().Equal(expected1, actualBalance1) + }) + + t.Run("balance of chainAWallet2 should be lowered by sum of recv ack and timeout", func(t *testing.T) { + actualBalance2, err := s.GetChainANativeBalance(ctx, chainAWallet2) + s.Require().NoError(err) + + expected2 := testvalues.StartingTokenAmount - testFee.Total().AmountOf(chainADenom).Int64() + s.Require().Equal(expected2, actualBalance2) + }) + }) + + 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, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + }) + + t.Run("timeout fee is refunded", func(t *testing.T) { + actualBalance1, err := s.GetChainANativeBalance(ctx, chainAWallet1) + s.Require().NoError(err) + + // once the relayer has relayed the packets, the timeout fee should be refunded. + expected1 := testvalues.StartingTokenAmount - walletAmount1.Amount.Int64() - testFee.AckFee.AmountOf(chainADenom).Int64() - testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected1, actualBalance1) + + actualBalance2, err := s.GetChainANativeBalance(ctx, chainAWallet2) + s.Require().NoError(err) + + // once the relayer has relayed the packets, the timeout fee should be refunded. + expected2 := testvalues.StartingTokenAmount - testFee.AckFee.AmountOf(chainADenom).Int64() - testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected2, actualBalance2) + }) +} + +// feeMiddlewareChannelOptions configures both of the chains to have fee middleware enabled. +func feeMiddlewareChannelOptions() func(options *ibc.CreateChannelOptions) { + return func(opts *ibc.CreateChannelOptions) { + opts.Version = "{\"fee_version\":\"ics29-1\",\"app_version\":\"ics20-1\"}" + opts.DestPortName = "transfer" + opts.SourcePortName = "transfer" + } +} diff --git a/e2e/testsuite/grpc_query.go b/e2e/testsuite/grpc_query.go new file mode 100644 index 00000000000..1d9dc8b15ee --- /dev/null +++ b/e2e/testsuite/grpc_query.go @@ -0,0 +1,478 @@ +package testsuite + +import ( + "context" + "fmt" + "sort" + "time" + + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "cosmossdk.io/math" + upgradetypes "cosmossdk.io/x/upgrade/types" + + "github.com/cosmos/cosmos-sdk/client/grpc/cmtservice" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govtypesv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + grouptypes "github.com/cosmos/cosmos-sdk/x/group" + paramsproposaltypes "github.com/cosmos/cosmos-sdk/x/params/types/proposal" + + wasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + 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" + feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/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" + ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" +) + +// GRPCClients holds a reference to any GRPC clients that are needed by the tests. +// These should typically be used for query clients only. If we need to make changes, we should +// use E2ETestSuite.BroadcastMessages to broadcast transactions instead. +type GRPCClients struct { + ClientQueryClient clienttypes.QueryClient + ConnectionQueryClient connectiontypes.QueryClient + ChannelQueryClient channeltypes.QueryClient + TransferQueryClient transfertypes.QueryClient + FeeQueryClient feetypes.QueryClient + ICAControllerQueryClient controllertypes.QueryClient + ICAHostQueryClient hosttypes.QueryClient + WasmQueryClient wasmtypes.QueryClient + + // SDK query clients + BankQueryClient banktypes.QueryClient + GovQueryClient govtypesv1beta1.QueryClient + GovQueryClientV1 govtypesv1.QueryClient + GroupsQueryClient grouptypes.QueryClient + ParamsQueryClient paramsproposaltypes.QueryClient + AuthQueryClient authtypes.QueryClient + AuthZQueryClient authz.QueryClient + UpgradeQueryClient upgradetypes.QueryClient + + ConsensusServiceClient cmtservice.ServiceClient +} + +// InitGRPCClients establishes GRPC clients with the given chain. +// The created GRPCClients can be retrieved with GetChainGRCPClients. +func (s *E2ETestSuite) InitGRPCClients(chain ibc.Chain) { + _, ok := chain.(*cosmos.CosmosChain) + if !ok { + return + } + + // Create a connection to the gRPC server. + grpcConn, err := grpc.Dial( + chain.GetHostGRPCAddress(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + s.Require().NoError(err) + s.T().Cleanup(func() { + if err := grpcConn.Close(); err != nil { + s.T().Logf("failed closing GRPC connection to chain %s: %s", chain.Config().ChainID, err) + } + }) + + if s.grpcClients == nil { + s.grpcClients = make(map[string]GRPCClients) + } + + s.grpcClients[chain.Config().ChainID] = GRPCClients{ + ClientQueryClient: clienttypes.NewQueryClient(grpcConn), + ConnectionQueryClient: connectiontypes.NewQueryClient(grpcConn), + ChannelQueryClient: channeltypes.NewQueryClient(grpcConn), + TransferQueryClient: transfertypes.NewQueryClient(grpcConn), + FeeQueryClient: feetypes.NewQueryClient(grpcConn), + ICAControllerQueryClient: controllertypes.NewQueryClient(grpcConn), + ICAHostQueryClient: hosttypes.NewQueryClient(grpcConn), + WasmQueryClient: wasmtypes.NewQueryClient(grpcConn), + BankQueryClient: banktypes.NewQueryClient(grpcConn), + GovQueryClient: govtypesv1beta1.NewQueryClient(grpcConn), + GovQueryClientV1: govtypesv1.NewQueryClient(grpcConn), + GroupsQueryClient: grouptypes.NewQueryClient(grpcConn), + ParamsQueryClient: paramsproposaltypes.NewQueryClient(grpcConn), + AuthQueryClient: authtypes.NewQueryClient(grpcConn), + AuthZQueryClient: authz.NewQueryClient(grpcConn), + ConsensusServiceClient: cmtservice.NewServiceClient(grpcConn), + UpgradeQueryClient: upgradetypes.NewQueryClient(grpcConn), + } +} + +// Header defines an interface which is implemented by both the sdk block header and the cometbft Block Header. +// this interfaces allows us to use the same function to fetch the block header for both chains. +type Header interface { + GetTime() time.Time + GetLastCommitHash() []byte +} + +// QueryClientState queries the client state on the given chain for the provided clientID. +func (s *E2ETestSuite) QueryClientState(ctx context.Context, chain ibc.Chain, clientID string) (ibcexported.ClientState, error) { + queryClient := s.GetChainGRCPClients(chain).ClientQueryClient + res, err := queryClient.ClientState(ctx, &clienttypes.QueryClientStateRequest{ + ClientId: clientID, + }) + if err != nil { + return nil, err + } + + cfg := EncodingConfig() + var clientState ibcexported.ClientState + if err := cfg.InterfaceRegistry.UnpackAny(res.ClientState, &clientState); err != nil { + return nil, err + } + + return clientState, nil +} + +// QueryUpgradedClientState queries the upgraded client state on the given chain for the provided clientID. +func (s *E2ETestSuite) QueryUpgradedClientState(ctx context.Context, chain ibc.Chain, clientID string) (ibcexported.ClientState, error) { + queryClient := s.GetChainGRCPClients(chain).ClientQueryClient + res, err := queryClient.UpgradedClientState(ctx, &clienttypes.QueryUpgradedClientStateRequest{}) + if err != nil { + return nil, err + } + + cfg := EncodingConfig() + var clientState ibcexported.ClientState + if err := cfg.InterfaceRegistry.UnpackAny(res.UpgradedClientState, &clientState); err != nil { + return nil, err + } + + return clientState, nil +} + +// QueryClientStatus queries the status of the client by clientID +func (s *E2ETestSuite) QueryClientStatus(ctx context.Context, chain ibc.Chain, clientID string) (string, error) { + queryClient := s.GetChainGRCPClients(chain).ClientQueryClient + res, err := queryClient.ClientStatus(ctx, &clienttypes.QueryClientStatusRequest{ + ClientId: clientID, + }) + if err != nil { + return "", err + } + + return res.Status, nil +} + +// QueryCurrentUpgradePlan queries the currently scheduled upgrade plans. +func (s *E2ETestSuite) QueryCurrentUpgradePlan(ctx context.Context, chain ibc.Chain) (upgradetypes.Plan, error) { + queryClient := s.GetChainGRCPClients(chain).UpgradeQueryClient + res, err := queryClient.CurrentPlan(ctx, &upgradetypes.QueryCurrentPlanRequest{}) + if err != nil { + return upgradetypes.Plan{}, err + } + + return *res.Plan, nil +} + +// QueryConnection queries the connection end using the given chain and connection id. +func (s *E2ETestSuite) QueryConnection(ctx context.Context, chain ibc.Chain, connectionID string) (connectiontypes.ConnectionEnd, error) { + queryClient := s.GetChainGRCPClients(chain).ConnectionQueryClient + res, err := queryClient.Connection(ctx, &connectiontypes.QueryConnectionRequest{ + ConnectionId: connectionID, + }) + if err != nil { + return connectiontypes.ConnectionEnd{}, err + } + + return *res.Connection, nil +} + +// QueryChannel queries the channel on a given chain for the provided portID and channelID +func (s *E2ETestSuite) QueryChannel(ctx context.Context, chain ibc.Chain, portID, channelID string) (channeltypes.Channel, error) { + queryClient := s.GetChainGRCPClients(chain).ChannelQueryClient + res, err := queryClient.Channel(ctx, &channeltypes.QueryChannelRequest{ + PortId: portID, + ChannelId: channelID, + }) + if err != nil { + return channeltypes.Channel{}, err + } + + return *res.Channel, nil +} + +// QueryPacketCommitment queries the packet commitment on the given chain for the provided channel and sequence. +func (s *E2ETestSuite) QueryPacketCommitment(ctx context.Context, chain ibc.Chain, portID, channelID string, sequence uint64) ([]byte, error) { + queryClient := s.GetChainGRCPClients(chain).ChannelQueryClient + res, err := queryClient.PacketCommitment(ctx, &channeltypes.QueryPacketCommitmentRequest{ + PortId: portID, + ChannelId: channelID, + Sequence: sequence, + }) + if err != nil { + return nil, err + } + return res.Commitment, nil +} + +// QueryPacketAcknowledgements queries the packet acknowledgements on the given chain for the provided channel (optional) list of packet commitment sequences. +func (s *E2ETestSuite) QueryPacketAcknowledgements(ctx context.Context, chain ibc.Chain, portID, channelID string, packetCommitmentSequences []uint64) ([]*channeltypes.PacketState, error) { + queryClient := s.GetChainGRCPClients(chain).ChannelQueryClient + res, err := queryClient.PacketAcknowledgements(ctx, &channeltypes.QueryPacketAcknowledgementsRequest{ + PortId: portID, + ChannelId: channelID, + PacketCommitmentSequences: packetCommitmentSequences, + }) + if err != nil { + return nil, err + } + return res.Acknowledgements, nil +} + +// QueryUpgradeError queries the upgrade error on the given chain for the provided channel. +func (s *E2ETestSuite) QueryUpgradeError(ctx context.Context, chain ibc.Chain, portID, channelID string) (channeltypes.ErrorReceipt, error) { + queryClient := s.GetChainGRCPClients(chain).ChannelQueryClient + res, err := queryClient.UpgradeError(ctx, &channeltypes.QueryUpgradeErrorRequest{ + PortId: portID, + ChannelId: channelID, + }) + if err != nil { + return channeltypes.ErrorReceipt{}, err + } + return res.ErrorReceipt, nil +} + +// QueryTotalEscrowForDenom queries the total amount of tokens in escrow for a denom +func (s *E2ETestSuite) QueryTotalEscrowForDenom(ctx context.Context, chain ibc.Chain, denom string) (sdk.Coin, error) { + queryClient := s.GetChainGRCPClients(chain).TransferQueryClient + res, err := queryClient.TotalEscrowForDenom(ctx, &transfertypes.QueryTotalEscrowForDenomRequest{ + Denom: denom, + }) + if err != nil { + return sdk.Coin{}, err + } + + return res.Amount, nil +} + +// QueryInterchainAccount queries the interchain account for the given owner and connectionID. +func (s *E2ETestSuite) QueryInterchainAccount(ctx context.Context, chain ibc.Chain, owner, connectionID string) (string, error) { + queryClient := s.GetChainGRCPClients(chain).ICAControllerQueryClient + res, err := queryClient.InterchainAccount(ctx, &controllertypes.QueryInterchainAccountRequest{ + Owner: owner, + ConnectionId: connectionID, + }) + if err != nil { + return "", err + } + return res.Address, nil +} + +// QueryIncentivizedPacketsForChannel queries the incentivized packets on the specified channel. +func (s *E2ETestSuite) QueryIncentivizedPacketsForChannel( + ctx context.Context, + chain ibc.Chain, + portID, + channelID string, +) ([]*feetypes.IdentifiedPacketFees, error) { + queryClient := s.GetChainGRCPClients(chain).FeeQueryClient + res, err := queryClient.IncentivizedPacketsForChannel(ctx, &feetypes.QueryIncentivizedPacketsForChannelRequest{ + PortId: portID, + ChannelId: channelID, + }) + if err != nil { + return nil, err + } + return res.IncentivizedPackets, err +} + +// QueryFeeEnabledChannel queries the fee-enabled status of a channel. +func (s *E2ETestSuite) QueryFeeEnabledChannel(ctx context.Context, chain ibc.Chain, portID, channelID string) (bool, error) { + queryClient := s.GetChainGRCPClients(chain).FeeQueryClient + res, err := queryClient.FeeEnabledChannel(ctx, &feetypes.QueryFeeEnabledChannelRequest{ + PortId: portID, + ChannelId: channelID, + }) + if err != nil { + return false, err + } + return res.FeeEnabled, nil +} + +// QueryCounterPartyPayee queries the counterparty payee of the given chain and relayer address on the specified channel. +func (s *E2ETestSuite) QueryCounterPartyPayee(ctx context.Context, chain ibc.Chain, relayerAddress, channelID string) (string, error) { + queryClient := s.GetChainGRCPClients(chain).FeeQueryClient + res, err := queryClient.CounterpartyPayee(ctx, &feetypes.QueryCounterpartyPayeeRequest{ + ChannelId: channelID, + Relayer: relayerAddress, + }) + if err != nil { + return "", err + } + return res.CounterpartyPayee, nil +} + +// QueryBalance returns the balance of a specific denomination for a given account by address. +func (s *E2ETestSuite) QueryBalance(ctx context.Context, chain ibc.Chain, address string, denom string) (math.Int, error) { + queryClient := s.GetChainGRCPClients(chain).BankQueryClient + res, err := queryClient.Balance(ctx, &banktypes.QueryBalanceRequest{ + Address: address, + Denom: denom, + }) + if err != nil { + return math.Int{}, err + } + + return res.Balance.Amount, nil +} + +// QueryProposalV1Beta1 queries the governance proposal on the given chain with the given proposal ID. +func (s *E2ETestSuite) QueryProposalV1Beta1(ctx context.Context, chain ibc.Chain, proposalID uint64) (govtypesv1beta1.Proposal, error) { + queryClient := s.GetChainGRCPClients(chain).GovQueryClient + res, err := queryClient.Proposal(ctx, &govtypesv1beta1.QueryProposalRequest{ + ProposalId: proposalID, + }) + if err != nil { + return govtypesv1beta1.Proposal{}, err + } + + return res.Proposal, nil +} + +func (s *E2ETestSuite) QueryProposalV1(ctx context.Context, chain ibc.Chain, proposalID uint64) (govtypesv1.Proposal, error) { + queryClient := s.GetChainGRCPClients(chain).GovQueryClientV1 + res, err := queryClient.Proposal(ctx, &govtypesv1.QueryProposalRequest{ + ProposalId: proposalID, + }) + if err != nil { + return govtypesv1.Proposal{}, err + } + + return *res.Proposal, nil +} + +// GetBlockHeaderByHeight fetches the block header at a given height. +func (s *E2ETestSuite) GetBlockHeaderByHeight(ctx context.Context, chain ibc.Chain, height uint64) (Header, error) { + consensusService := s.GetChainGRCPClients(chain).ConsensusServiceClient + res, err := consensusService.GetBlockByHeight(ctx, &cmtservice.GetBlockByHeightRequest{ + Height: int64(height), + }) + if err != nil { + return nil, err + } + + // Clean up when v4 is not supported, see: https://github.com/cosmos/ibc-go/issues/3540 + // versions newer than 0.46 SDK use the SdkBlock field while versions older + // than 0.46 SDK, which do not have the SdkBlock field, use the Block field. + if res.SdkBlock != nil { + return &res.SdkBlock.Header, nil + } + return &res.Block.Header, nil // needed for v4 (uses SDK v0.45) +} + +// GetValidatorSetByHeight returns the validators of the given chain at the specified height. The returned validators +// are sorted by address. +func (s *E2ETestSuite) GetValidatorSetByHeight(ctx context.Context, chain ibc.Chain, height uint64) ([]*cmtservice.Validator, error) { + consensusService := s.GetChainGRCPClients(chain).ConsensusServiceClient + res, err := consensusService.GetValidatorSetByHeight(ctx, &cmtservice.GetValidatorSetByHeightRequest{ + Height: int64(height), + }) + if err != nil { + return nil, err + } + + sort.SliceStable(res.Validators, func(i, j int) bool { + return res.Validators[i].Address < res.Validators[j].Address + }) + + return res.Validators, nil +} + +// QueryModuleAccountAddress returns the sdk.AccAddress of a given module name. +func (s *E2ETestSuite) QueryModuleAccountAddress(ctx context.Context, moduleName string, chain ibc.Chain) (sdk.AccAddress, error) { + authClient := s.GetChainGRCPClients(chain).AuthQueryClient + resp, err := authClient.ModuleAccountByName(ctx, &authtypes.QueryModuleAccountByNameRequest{ + Name: moduleName, + }) + if err != nil { + return nil, err + } + + cfg := EncodingConfig() + + var account sdk.AccountI + if err := cfg.InterfaceRegistry.UnpackAny(resp.Account, &account); err != nil { + return nil, err + } + moduleAccount, ok := account.(authtypes.ModuleAccountI) + if !ok { + return nil, fmt.Errorf("failed to cast account: %T as ModuleAccount", moduleAccount) + } + + return moduleAccount.GetAddress(), nil +} + +// QueryGranterGrants returns all GrantAuthorizations for the given granterAddress. +func (s *E2ETestSuite) QueryGranterGrants(ctx context.Context, chain ibc.Chain, granterAddress string) ([]*authz.GrantAuthorization, error) { + authzClient := s.GetChainGRCPClients(chain).AuthZQueryClient + queryRequest := &authz.QueryGranterGrantsRequest{ + Granter: granterAddress, + } + + grants, err := authzClient.GranterGrants(ctx, queryRequest) + if err != nil { + return nil, err + } + + return grants.Grants, nil +} + +// QueryBalances returns all the balances on the given chain for the provided address. +func (s *E2ETestSuite) QueryAllBalances(ctx context.Context, chain ibc.Chain, address string, resolveDenom bool) (sdk.Coins, error) { + queryClient := s.GetChainGRCPClients(chain).BankQueryClient + res, err := queryClient.AllBalances(ctx, &banktypes.QueryAllBalancesRequest{ + Address: address, + ResolveDenom: resolveDenom, + }) + if err != nil { + return sdk.Coins{}, err + } + + return res.Balances, nil +} + +// QueryDenomMetadata queries the metadata for the given denom. +func (s *E2ETestSuite) QueryDenomMetadata(ctx context.Context, chain ibc.Chain, denom string) (banktypes.Metadata, error) { + bankClient := s.GetChainGRCPClients(chain).BankQueryClient + queryRequest := &banktypes.QueryDenomMetadataRequest{ + Denom: denom, + } + res, err := bankClient.DenomMetadata(ctx, queryRequest) + if err != nil { + return banktypes.Metadata{}, err + } + return res.Metadata, nil +} + +// QueryWasmCode queries the code for a wasm contract. +func (s *E2ETestSuite) QueryWasmCode(ctx context.Context, chain ibc.Chain, checksum string) ([]byte, error) { + queryClient := s.GetChainGRCPClients(chain).WasmQueryClient + queryRequest := &wasmtypes.QueryCodeRequest{ + Checksum: checksum, + } + res, err := queryClient.Code(ctx, queryRequest) + if err != nil { + return nil, err + } + return res.Data, nil +} + +// QueryWasmChecksums queries the wasm code checksums stored within the 08-wasm module. +func (s *E2ETestSuite) QueryWasmChecksums(ctx context.Context, chain ibc.Chain) ([]string, error) { + queryClient := s.GetChainGRCPClients(chain).WasmQueryClient + res, err := queryClient.Checksums(ctx, &wasmtypes.QueryChecksumsRequest{}) + if err != nil { + return nil, err + } + + return res.Checksums, nil +} diff --git a/e2e/testsuite/tx.go b/e2e/testsuite/tx.go new file mode 100644 index 00000000000..c3f39b8685c --- /dev/null +++ b/e2e/testsuite/tx.go @@ -0,0 +1,299 @@ +package testsuite + +import ( + "context" + "fmt" + "slices" + "strconv" + "strings" + "time" + + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + test "github.com/strangelove-ventures/interchaintest/v8/testutil" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govtypesv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + + "github.com/cosmos/ibc-go/e2e/testsuite/sanitize" + "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" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" +) + +// BroadcastMessages broadcasts the provided messages to the given chain and signs them on behalf of the provided user. +// Once the broadcast response is returned, we wait for a few blocks to be created on both chain A and chain B. +func (s *E2ETestSuite) BroadcastMessages(ctx context.Context, chain ibc.Chain, user ibc.Wallet, msgs ...sdk.Msg) sdk.TxResponse { + cosmosChain, ok := chain.(*cosmos.CosmosChain) + if !ok { + panic("BroadcastMessages expects a cosmos.CosmosChain") + } + + broadcaster := cosmos.NewBroadcaster(s.T(), cosmosChain) + + // strip out any fields that may not be supported for the given chain version. + msgs = sanitize.Messages(cosmosChain.Nodes()[0].Image.Version, msgs...) + + broadcaster.ConfigureClientContextOptions(func(clientContext client.Context) client.Context { + // use a codec with all the types our tests care about registered. + // BroadcastTx will deserialize the response and will not be able to otherwise. + cdc := Codec() + return clientContext.WithCodec(cdc).WithTxConfig(authtx.NewTxConfig(cdc, []signingtypes.SignMode{signingtypes.SignMode_SIGN_MODE_DIRECT})) + }) + + broadcaster.ConfigureFactoryOptions(func(factory tx.Factory) tx.Factory { + return factory.WithGas(DefaultGasValue) + }) + + // Retry the operation a few times if the user signing the transaction is a relayer. (See issue #3264) + var resp sdk.TxResponse + var err error + broadcastFunc := func() (sdk.TxResponse, error) { + return cosmos.BroadcastTx(ctx, broadcaster, user, msgs...) + } + if s.relayers.ContainsRelayer(s.T().Name(), user) { + // Retry five times, the value of 5 chosen is arbitrary. + resp, err = s.retryNtimes(broadcastFunc, 5) + } else { + resp, err = broadcastFunc() + } + s.Require().NoError(err) + + chainA, chainB := s.GetChains() + s.Require().NoError(test.WaitForBlocks(ctx, 2, chainA, chainB)) + + return resp +} + +// retryNtimes retries the provided function up to the provided number of attempts. +func (s *E2ETestSuite) retryNtimes(f func() (sdk.TxResponse, error), attempts int) (sdk.TxResponse, error) { + // Ignore account sequence mismatch errors. + retryMessages := []string{"account sequence mismatch"} + var resp sdk.TxResponse + var err error + // If the response's raw log doesn't contain any of the allowed prefixes we return, else, we retry. + for i := 0; i < attempts; i++ { + resp, err = f() + if err != nil { + return sdk.TxResponse{}, err + } + // If the response's raw log doesn't contain any of the allowed prefixes we return, else, we retry. + if !slices.ContainsFunc(retryMessages, func(s string) bool { return strings.Contains(resp.RawLog, s) }) { + return resp, err + } + s.T().Logf("retrying tx due to non deterministic failure: %+v", resp) + } + return resp, err +} + +// AssertTxFailure verifies that an sdk.TxResponse has failed. +func (s *E2ETestSuite) AssertTxFailure(resp sdk.TxResponse, expectedError *errorsmod.Error) { + errorMsg := fmt.Sprintf("%+v", resp) + // In older versions, the codespace and abci codes were different. So in compatibility tests + // we can not make assertions on them. + if GetChainATag() == GetChainBTag() { + s.Require().Equal(expectedError.ABCICode(), resp.Code, errorMsg) + s.Require().Equal(expectedError.Codespace(), resp.Codespace, errorMsg) + } + s.Require().Contains(resp.RawLog, expectedError.Error(), errorMsg) +} + +// AssertTxSuccess verifies that an sdk.TxResponse has succeeded. +func (s *E2ETestSuite) AssertTxSuccess(resp sdk.TxResponse) { + errorMsg := addDebuggingInformation(fmt.Sprintf("%+v", resp)) + s.Require().Equal(resp.Code, uint32(0), errorMsg) + s.Require().NotEmpty(resp.TxHash, errorMsg) + s.Require().NotEqual(int64(0), resp.GasUsed, errorMsg) + s.Require().NotEqual(int64(0), resp.GasWanted, errorMsg) + s.Require().NotEmpty(resp.Events, errorMsg) + s.Require().NotEmpty(resp.Data, errorMsg) +} + +// addDebuggingInformation adds additional debugging information to the error message +// based on common types of errors that can occur. +func addDebuggingInformation(errorMsg string) string { + if strings.Contains(errorMsg, "errUnknownField") { + errorMsg += ` + +This error is likely due to a new an unrecognized proto field being provided to a chain using an older version of the sdk. +If this is a compatibility test, ensure that the fields are being sanitized in the sanitize.Messages function. + +` + } + return errorMsg +} + +// ExecuteAndPassGovV1Proposal submits a v1 governance proposal using the provided user and message and uses all validators +// to vote yes on the proposal. It ensures the proposal successfully passes. +func (s *E2ETestSuite) ExecuteAndPassGovV1Proposal(ctx context.Context, msg sdk.Msg, chain ibc.Chain, user ibc.Wallet) { + err := s.ExecuteGovV1Proposal(ctx, msg, chain, user) + s.Require().NoError(err) +} + +// ExecuteGovV1Proposal submits a v1 governance proposal using the provided user and message and uses all validators +// to vote yes on the proposal. +func (s *E2ETestSuite) ExecuteGovV1Proposal(ctx context.Context, msg sdk.Msg, chain ibc.Chain, user ibc.Wallet) error { + cosmosChain, ok := chain.(*cosmos.CosmosChain) + if !ok { + panic("ExecuteAndPassGovV1Proposal must be passed a cosmos.CosmosChain") + } + + sender, err := sdk.AccAddressFromBech32(user.FormattedAddress()) + s.Require().NoError(err) + + proposalID := s.proposalIDs[cosmosChain.Config().ChainID] + defer func() { + s.proposalIDs[cosmosChain.Config().ChainID] = proposalID + 1 + }() + + msgs := []sdk.Msg{msg} + + msgSubmitProposal, err := govtypesv1.NewMsgSubmitProposal( + msgs, + sdk.NewCoins(sdk.NewCoin(cosmosChain.Config().Denom, sdkmath.NewInt(testvalues.DefaultGovV1ProposalTokenAmount))), + sender.String(), + "", + fmt.Sprintf("e2e gov proposal: %d", proposalID), + fmt.Sprintf("executing gov proposal %d", proposalID), + false, + ) + s.Require().NoError(err) + + resp := s.BroadcastMessages(ctx, cosmosChain, user, msgSubmitProposal) + s.AssertTxSuccess(resp) + + s.Require().NoError(cosmosChain.VoteOnProposalAllValidators(ctx, strconv.Itoa(int(proposalID)), cosmos.ProposalVoteYes)) + + return s.waitForGovV1ProposalToPass(ctx, cosmosChain, proposalID) +} + +// waitForGovV1ProposalToPass polls for the entire voting period to see if the proposal has passed. +// if the proposal has not passed within the duration of the voting period, an error is returned. +func (s *E2ETestSuite) waitForGovV1ProposalToPass(ctx context.Context, chain ibc.Chain, proposalID uint64) error { + var govProposal govtypesv1.Proposal + // poll for the query for the entire voting period to see if the proposal has passed. + err := test.WaitForCondition(testvalues.VotingPeriod, 10*time.Second, func() (bool, error) { + proposal, err := s.QueryProposalV1(ctx, chain, proposalID) + if err != nil { + return false, err + } + + govProposal = proposal + return govProposal.Status == govtypesv1.StatusPassed, nil + }) + + // in the case of a failed proposal, we wrap the polling error with additional information about why the proposal failed. + if err != nil && govProposal.FailedReason != "" { + err = errorsmod.Wrap(err, govProposal.FailedReason) + } + return err +} + +// ExecuteAndPassGovV1Beta1Proposal submits the given v1beta1 governance proposal using the provided user and uses all validators to vote yes on the proposal. +// It ensures the proposal successfully passes. +func (s *E2ETestSuite) ExecuteAndPassGovV1Beta1Proposal(ctx context.Context, chain ibc.Chain, user ibc.Wallet, content govtypesv1beta1.Content) { + cosmosChain, ok := chain.(*cosmos.CosmosChain) + if !ok { + panic("ExecuteAndPassGovV1Beta1Proposal must be passed a cosmos.CosmosChain") + } + + proposalID := s.proposalIDs[chain.Config().ChainID] + defer func() { + s.proposalIDs[chain.Config().ChainID] = proposalID + 1 + }() + + txResp := s.ExecuteGovV1Beta1Proposal(ctx, cosmosChain, user, content) + s.AssertTxSuccess(txResp) + + // TODO: replace with parsed proposal ID from MsgSubmitProposalResponse + // https://github.com/cosmos/ibc-go/issues/2122 + + proposal, err := s.QueryProposalV1Beta1(ctx, cosmosChain, proposalID) + s.Require().NoError(err) + s.Require().Equal(govtypesv1beta1.StatusVotingPeriod, proposal.Status) + + err = cosmosChain.VoteOnProposalAllValidators(ctx, fmt.Sprintf("%d", proposalID), cosmos.ProposalVoteYes) + s.Require().NoError(err) + + // ensure voting period has not passed before validators finished voting + proposal, err = s.QueryProposalV1Beta1(ctx, cosmosChain, proposalID) + s.Require().NoError(err) + s.Require().Equal(govtypesv1beta1.StatusVotingPeriod, proposal.Status) + + err = s.waitForGovV1Beta1ProposalToPass(ctx, cosmosChain, proposalID) + s.Require().NoError(err) +} + +// waitForGovV1Beta1ProposalToPass polls for the entire voting period to see if the proposal has passed. +// if the proposal has not passed within the duration of the voting period, an error is returned. +func (s *E2ETestSuite) waitForGovV1Beta1ProposalToPass(ctx context.Context, chain ibc.Chain, proposalID uint64) error { + // poll for the query for the entire voting period to see if the proposal has passed. + return test.WaitForCondition(testvalues.VotingPeriod, 10*time.Second, func() (bool, error) { + proposal, err := s.QueryProposalV1Beta1(ctx, chain, proposalID) + if err != nil { + return false, err + } + return proposal.Status == govtypesv1beta1.StatusPassed, nil + }) +} + +// ExecuteGovV1Beta1Proposal submits a v1beta1 governance proposal using the provided content. +func (s *E2ETestSuite) ExecuteGovV1Beta1Proposal(ctx context.Context, chain ibc.Chain, user ibc.Wallet, content govtypesv1beta1.Content) sdk.TxResponse { + sender, err := sdk.AccAddressFromBech32(user.FormattedAddress()) + s.Require().NoError(err) + + msgSubmitProposal, err := govtypesv1beta1.NewMsgSubmitProposal(content, sdk.NewCoins(sdk.NewCoin(chain.Config().Denom, govtypesv1beta1.DefaultMinDepositTokens)), sender) + s.Require().NoError(err) + + return s.BroadcastMessages(ctx, chain, user, msgSubmitProposal) +} + +// Transfer broadcasts a MsgTransfer message. +func (s *E2ETestSuite) Transfer(ctx context.Context, chain ibc.Chain, user ibc.Wallet, + portID, channelID string, token sdk.Coin, sender, receiver string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, memo string, +) sdk.TxResponse { + msg := transfertypes.NewMsgTransfer(portID, channelID, token, sender, receiver, timeoutHeight, timeoutTimestamp, memo) + return s.BroadcastMessages(ctx, chain, user, msg) +} + +// RegisterCounterPartyPayee broadcasts a MsgRegisterCounterpartyPayee message. +func (s *E2ETestSuite) RegisterCounterPartyPayee(ctx context.Context, chain ibc.Chain, + user ibc.Wallet, portID, channelID, relayerAddr, counterpartyPayeeAddr string, +) sdk.TxResponse { + msg := feetypes.NewMsgRegisterCounterpartyPayee(portID, channelID, relayerAddr, counterpartyPayeeAddr) + return s.BroadcastMessages(ctx, chain, user, msg) +} + +// PayPacketFeeAsync broadcasts a MsgPayPacketFeeAsync message. +func (s *E2ETestSuite) PayPacketFeeAsync( + ctx context.Context, + chain ibc.Chain, + user ibc.Wallet, + packetID channeltypes.PacketId, + packetFee feetypes.PacketFee, +) sdk.TxResponse { + msg := feetypes.NewMsgPayPacketFeeAsync(packetID, packetFee) + return s.BroadcastMessages(ctx, chain, user, msg) +} + +// PruneAcknowledgements broadcasts a MsgPruneAcknowledgements message. +func (s *E2ETestSuite) PruneAcknowledgements( + ctx context.Context, + chain ibc.Chain, + user ibc.Wallet, + portID, channelID string, + limit uint64, +) sdk.TxResponse { + msg := channeltypes.NewMsgPruneAcknowledgements(portID, channelID, limit, user.FormattedAddress()) + return s.BroadcastMessages(ctx, chain, user, msg) +} 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 568edb2b031..fb5d54bf740 100644 --- a/modules/apps/27-interchain-accounts/controller/ibc_middleware_test.go +++ b/modules/apps/27-interchain-accounts/controller/ibc_middleware_test.go @@ -863,6 +863,33 @@ func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeInit() { } } +// OnChanUpgradeTry callback returns error on controller chains +func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeTry() { + suite.SetupTest() // reset + path := NewICAPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + + err := SetupICAPath(path, TestOwnerAddress) + suite.Require().NoError(err) + + // call application callback directly + module, _, err := suite.chainA.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID) + suite.Require().NoError(err) + + app, ok := suite.chainA.App.GetIBCKeeper().Router.GetRoute(module) + suite.Require().True(ok) + cbs, ok := app.(porttypes.UpgradableModule) + suite.Require().True(ok) + + version, err := cbs.OnChanUpgradeTry( + suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, + path.EndpointA.ChannelConfig.Order, []string{path.EndpointA.ConnectionID}, path.EndpointB.ChannelConfig.Version, + ) + suite.Require().Error(err) + suite.Require().ErrorIs(err, icatypes.ErrInvalidChannelFlow) + suite.Require().Equal("", version) +} + func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeAck() { var ( path *ibctesting.Path 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 f5fdde6c5c1..3491f47d52d 100644 --- a/modules/apps/27-interchain-accounts/host/ibc_module_test.go +++ b/modules/apps/27-interchain-accounts/host/ibc_module_test.go @@ -604,6 +604,33 @@ func (suite *InterchainAccountsTestSuite) TestOnTimeoutPacket() { } } +// OnChanUpgradeInit callback returns error on host chains +func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeInit() { + path := NewICAPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + + err := SetupICAPath(path, TestOwnerAddress) + suite.Require().NoError(err) + + // call application callback directly + module, _, err := suite.chainB.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID) + suite.Require().NoError(err) + + app, ok := suite.chainB.App.GetIBCKeeper().Router.GetRoute(module) + suite.Require().True(ok) + cbs, ok := app.(porttypes.UpgradableModule) + suite.Require().True(ok) + + version, err := cbs.OnChanUpgradeInit( + suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, + path.EndpointB.ChannelConfig.Order, []string{path.EndpointB.ConnectionID}, path.EndpointB.ChannelConfig.Version, + ) + + suite.Require().Error(err) + suite.Require().ErrorIs(err, icatypes.ErrInvalidChannelFlow) + suite.Require().Equal("", version) +} + func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeTry() { testCases := []struct { name string @@ -672,6 +699,31 @@ func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeTry() { } } +// OnChanUpgradeAck callback returns error on host chains +func (suite *InterchainAccountsTestSuite) TestOnChanUpgradeAck() { + path := NewICAPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + + err := SetupICAPath(path, TestOwnerAddress) + suite.Require().NoError(err) + + // call application callback directly + module, _, err := suite.chainB.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID) + suite.Require().NoError(err) + + app, ok := suite.chainB.App.GetIBCKeeper().Router.GetRoute(module) + suite.Require().True(ok) + cbs, ok := app.(porttypes.UpgradableModule) + suite.Require().True(ok) + + err = cbs.OnChanUpgradeAck( + suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, path.EndpointA.ChannelConfig.Version, + ) + + suite.Require().Error(err) + suite.Require().ErrorIs(err, icatypes.ErrInvalidChannelFlow) +} + func (suite *InterchainAccountsTestSuite) fundICAWallet(ctx sdk.Context, portID string, amount sdk.Coins) { interchainAccountAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(ctx, ibctesting.FirstConnectionID, portID) suite.Require().True(found) diff --git a/modules/core/04-channel/types/msgs.go b/modules/core/04-channel/types/msgs.go index 47e0103ec8d..e07d17c5670 100644 --- a/modules/core/04-channel/types/msgs.go +++ b/modules/core/04-channel/types/msgs.go @@ -832,7 +832,9 @@ func NewMsgChannelUpgradeCancel( } } -// ValidateBasic implements sdk.Msg +// ValidateBasic implements sdk.Msg. No checks are done for ErrorReceipt and ProofErrorReceipt +// since they are not required if the current channel state is not in FLUSHCOMPLETE and the signer +// is the designated authority (e.g. the governance module). func (msg MsgChannelUpgradeCancel) ValidateBasic() error { if err := host.PortIdentifierValidator(msg.PortId); err != nil { return errorsmod.Wrap(err, "invalid port ID")