diff --git a/.github/workflows/e2e-fork.yml b/.github/workflows/e2e-fork.yml index 382f703da86..da683c4660f 100644 --- a/.github/workflows/e2e-fork.yml +++ b/.github/workflows/e2e-fork.yml @@ -24,7 +24,7 @@ jobs: - id: set-matrix run: echo "matrix=$(go run cmd/build_test_matrix/main.go)" >> $GITHUB_OUTPUT env: - TEST_EXCLUSIONS: 'TestUpgradeTestSuite,TestGrandpaTestSuite' + TEST_EXCLUSIONS: 'TestUpgradeTestSuite,TestGrandpaTestSuite,TestIBCWasmChainUpgrade' # dynamically build a matrix of test/test suite pairs to run build-test-matrix-wasm: diff --git a/.github/workflows/e2e-upgrade.yaml b/.github/workflows/e2e-upgrade.yaml index 64b3fcda44d..0a8f43daafd 100644 --- a/.github/workflows/e2e-upgrade.yaml +++ b/.github/workflows/e2e-upgrade.yaml @@ -131,3 +131,17 @@ jobs: relayer-type: rly relayer-image: ghcr.io/cosmos/relayer relayer-tag: latest + + upgrade-ibcwasm-v8-hermes: + uses: ./.github/workflows/e2e-test-workflow-call.yml + with: + chain-image: ghcr.io/cosmos/ibc-go-wasm-simd + chain-binary: simd + chain-a-tag: v7.0.0 + chain-b-tag: v7.0.0 + chain-upgrade-tag: v8.0.0-dev1 + upgrade-plan-name: "ibcwasm-v8" + test-entry-point: "IBCWasmUpgradeTestSuite" + test: "TestIBCWasmChainUpgrade" + upload-logs: true + relayer-type: hermes \ No newline at end of file diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 95fe919befa..4fe89f9f348 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -63,4 +63,4 @@ jobs: chain-b-tag: '${{ needs.determine-image-tag.outputs.simd-tag }}' chain-binary: 'simd' # on regular PRs we won't run upgrade tests. - test-exclusions: 'TestUpgradeTestSuite,TestGrandpaTestSuite' + test-exclusions: 'TestUpgradeTestSuite,TestGrandpaTestSuite,TestIBCWasmChainUpgrade' diff --git a/e2e/tests/wasm/upgrade_test.go b/e2e/tests/wasm/upgrade_test.go new file mode 100644 index 00000000000..b62e592cdfe --- /dev/null +++ b/e2e/tests/wasm/upgrade_test.go @@ -0,0 +1,157 @@ +//go:build !test_e2e + +package wasm + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "testing" + "time" + + testifysuite "github.com/stretchr/testify/suite" + + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/testutil" + + upgradetypes "cosmossdk.io/x/upgrade/types" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + wasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" +) + +const ( + haltHeight = uint64(350) + blocksAfterUpgrade = uint64(10) +) + +func TestIBCWasmUpgradeTestSuite(t *testing.T) { + testCfg := testsuite.LoadConfig() + if testCfg.UpgradeConfig.Tag == "" || testCfg.UpgradeConfig.PlanName == "" { + t.Fatalf("%s and %s must be set when running an upgrade test", testsuite.ChainUpgradeTagEnv, testsuite.ChainUpgradePlanEnv) + } + + // wasm tests require a longer voting period to account for the time it takes to upload a contract. + testvalues.VotingPeriod = time.Minute * 5 + + testifysuite.Run(t, new(IBCWasmUpgradeTestSuite)) +} + +type IBCWasmUpgradeTestSuite struct { + testsuite.E2ETestSuite +} + +func (s *IBCWasmUpgradeTestSuite) TestIBCWasmChainUpgrade() { + t := s.T() + + ctx := context.Background() + chain := s.SetupSingleChain(ctx) + checksum := "" + + userWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + s.Require().NoError(testutil.WaitForBlocks(ctx, 1, chain), "failed to wait for blocks") + + t.Run("create and exec store code proposal", func(t *testing.T) { + file, err := os.Open("contracts/ics10_grandpa_cw.wasm.gz") + s.Require().NoError(err) + + checksum = s.ExecStoreCodeProposal(ctx, chain.(*cosmos.CosmosChain), userWallet, file) + s.Require().NotEmpty(checksum, "checksum must not be empty") + }) + + t.Run("upgrade chain", func(t *testing.T) { + testCfg := testsuite.LoadConfig() + s.UpgradeChain(ctx, chain.(*cosmos.CosmosChain), userWallet, testCfg.UpgradeConfig.PlanName, testCfg.ChainConfigs[0].Tag, testCfg.UpgradeConfig.Tag) + }) + + t.Run("query wasm checksums", func(t *testing.T) { + checksums, err := s.QueryWasmChecksums(ctx, chain) + s.Require().NoError(err) + s.Require().Contains(checksums, checksum) + }) +} + +// UpgradeChain upgrades a chain to a specific version using the planName provided. +// The software upgrade proposal is broadcast by the provided wallet. +func (s *IBCWasmUpgradeTestSuite) UpgradeChain(ctx context.Context, chain *cosmos.CosmosChain, wallet ibc.Wallet, planName, currentVersion, upgradeVersion string) { + plan := upgradetypes.Plan{ + Name: planName, + Height: int64(haltHeight), + Info: fmt.Sprintf("upgrade version test from %s to %s", currentVersion, upgradeVersion), + } + + upgradeProposal := upgradetypes.NewSoftwareUpgradeProposal(fmt.Sprintf("upgrade from %s to %s", currentVersion, upgradeVersion), "upgrade chain E2E test", plan) + s.ExecuteAndPassGovV1Beta1Proposal(ctx, chain, wallet, upgradeProposal) + + height, err := chain.Height(ctx) + s.Require().NoError(err, "error fetching height before upgrade") + + timeoutCtx, timeoutCtxCancel := context.WithTimeout(ctx, time.Minute*2) + defer timeoutCtxCancel() + + err = testutil.WaitForBlocks(timeoutCtx, int(haltHeight-height)+1, chain) + s.Require().Error(err, "chain did not halt at halt height") + + err = chain.StopAllNodes(ctx) + s.Require().NoError(err, "error stopping node(s)") + + repository := chain.Nodes()[0].Image.Repository + chain.UpgradeVersion(ctx, s.DockerClient, repository, upgradeVersion) + + err = chain.StartAllNodes(ctx) + s.Require().NoError(err, "error starting upgraded node(s)") + + // we are reinitializing the clients because we need to update the hostGRPCAddress after + // the upgrade and subsequent restarting of nodes + s.InitGRPCClients(chain) + + timeoutCtx, timeoutCtxCancel = context.WithTimeout(ctx, time.Minute*2) + defer timeoutCtxCancel() + + err = testutil.WaitForBlocks(timeoutCtx, int(blocksAfterUpgrade), chain) + s.Require().NoError(err, "chain did not produce blocks after upgrade") + + height, err = chain.Height(ctx) + s.Require().NoError(err, "error fetching height after upgrade") + + s.Require().Greater(height, haltHeight, "height did not increment after upgrade") +} + +func (s *IBCWasmUpgradeTestSuite) ExecStoreCodeProposal(ctx context.Context, chain *cosmos.CosmosChain, wallet ibc.Wallet, proposalContentReader io.Reader) string { + zippedContent, err := io.ReadAll(proposalContentReader) + s.Require().NoError(err) + + computedChecksum := s.extractChecksumFromGzippedContent(zippedContent) + + msgStoreCode := wasmtypes.MsgStoreCode{ + Signer: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + WasmByteCode: zippedContent, + } + + s.ExecuteAndPassGovV1Proposal(ctx, &msgStoreCode, chain, wallet) + + checksumBz, err := s.QueryWasmCode(ctx, chain, computedChecksum) + s.Require().NoError(err) + + checksum32 := sha256.Sum256(checksumBz) + actualChecksum := hex.EncodeToString(checksum32[:]) + s.Require().Equal(computedChecksum, actualChecksum, "checksum returned from query did not match the computed checksum") + + return actualChecksum +} + +// extractChecksumFromGzippedContent takes a gzipped wasm contract and returns the checksum. +func (s *IBCWasmUpgradeTestSuite) extractChecksumFromGzippedContent(zippedContent []byte) string { + content, err := wasmtypes.Uncompress(zippedContent, wasmtypes.MaxWasmByteSize()) + s.Require().NoError(err) + + checksum32 := sha256.Sum256(content) + return hex.EncodeToString(checksum32[:]) +} diff --git a/e2e/testsuite/grpc_query.go b/e2e/testsuite/grpc_query.go index 32ac37f278f..ee72396a56f 100644 --- a/e2e/testsuite/grpc_query.go +++ b/e2e/testsuite/grpc_query.go @@ -425,3 +425,14 @@ func (s *E2ETestSuite) QueryWasmCode(ctx context.Context, chain ibc.Chain, check } 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 +}