diff --git a/.circleci/config.yml b/.circleci/config.yml index 45fb8d2bda..92a219805d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -101,7 +101,7 @@ jobs: test-system: executor: golang parallelism: 1 - resource_class: large + resource_class: xlarge steps: - attach_workspace: at: /tmp/workspace @@ -112,6 +112,13 @@ jobs: - run: name: Build and run system tests command: make test-system + - run: + command: | + mkdir -p /tmp/system-test-workspace + mv /home/circleci/project/tests/system/testnet /tmp/system-test-workspace + when: on_fail + - store_artifacts: + path: /tmp/system-test-workspace benchmark: executor: golang @@ -135,7 +142,7 @@ jobs: simulations: executor: golang parallelism: 1 - resource_class: large + resource_class: xlarge steps: - checkout - run: diff --git a/Makefile b/Makefile index 0ef07eca21..d4ef57779f 100644 --- a/Makefile +++ b/Makefile @@ -161,22 +161,22 @@ test-system: install format-tools: go install mvdan.cc/gofumpt@v0.4.0 go install github.com/client9/misspell/cmd/misspell@v0.3.4 - go install golang.org/x/tools/cmd/goimports@latest + go install github.com/daixiang0/gci@v0.11.2 -lint: format-tools +lint: golangci-lint run --tests=false find . -name '*.go' -type f -not -path "./vendor*" -not -path "./tests/system/vendor*" -not -path "*.git*" -not -path "*_test.go" | xargs gofumpt -d -format: format-tools +format: find . -name '*.go' -type f -not -path "./vendor*" -not -path "./tests/system/vendor*" -not -path "*.git*" -not -path "./client/lcd/statik/statik.go" | xargs gofumpt -w find . -name '*.go' -type f -not -path "./vendor*" -not -path "./tests/system/vendor*" -not -path "*.git*" -not -path "./client/lcd/statik/statik.go" | xargs misspell -w - find . -name '*.go' -type f -not -path "./vendor*" -not -path "./tests/system/vendor*" -not -path "*.git*" -not -path "./client/lcd/statik/statik.go" | xargs goimports -w -local github.com/CosmWasm/wasmd + find . -name '*.go' -type f -not -path "./vendor*" -not -path "./tests/system/vendor*" -not -path "*.git*" -not -path "./client/lcd/statik/statik.go" | xargs gci write --skip-generated -s standard -s default -s "prefix(cosmossdk.io)" -s "prefix(github.com/cosmos/cosmos-sdk)" -s "prefix(github.com/CosmWasm/wasmd)" --custom-order ############################################################################### ### Protobuf ### ############################################################################### -protoVer=0.13.1 +protoVer=0.14.0 protoImageName=ghcr.io/cosmos/proto-builder:$(protoVer) protoImage=$(DOCKER) run --rm -v $(CURDIR):/workspace --workdir /workspace $(protoImageName) diff --git a/app/upgrades.go b/app/upgrades.go index 775e28bf45..5332b5692e 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -1,62 +1,77 @@ package app import ( + "fmt" + icacontrollertypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/controller/types" icahosttypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/host/types" ibctransfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + ibcconnectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" + ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" - "github.com/cosmos/cosmos-sdk/baseapp" - storetypes "github.com/cosmos/cosmos-sdk/store/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/module" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - consensustypes "github.com/cosmos/cosmos-sdk/x/consensus/types" - crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" - distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" - govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" - minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" - slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/CosmWasm/wasmd/app/upgrades" + "github.com/CosmWasm/wasmd/app/upgrades/noop" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" ) -// UpgradeName defines the on-chain upgrade name for the sample SimApp upgrade -// from v046 to v047. -// -// NOTE: This upgrade defines a reference implementation of what an upgrade -// could look like when an application is migrating from Cosmos SDK version -// v0.46.x to v0.47.x. -const UpgradeName = "v046-to-v047" +// Upgrades list of chain upgrades +var Upgrades []upgrades.Upgrade +// RegisterUpgradeHandlers registers the chain upgrade handlers func (app WasmApp) RegisterUpgradeHandlers() { + setupLegacyKeyTables(app.ParamsKeeper) + if len(Upgrades) == 0 { + // always have a unique upgrade registered for the current version to test in system tests + Upgrades = append(Upgrades, noop.NewUpgrade(app.Version())) + } + + keepers := upgrades.AppKeepers{AccountKeeper: app.AccountKeeper} + // register all upgrade handlers + for _, upgrade := range Upgrades { + app.UpgradeKeeper.SetUpgradeHandler( + upgrade.UpgradeName, + upgrade.CreateUpgradeHandler( + app.ModuleManager, + app.configurator, + &keepers, + ), + ) + } + + upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() + if err != nil { + panic(fmt.Sprintf("failed to read upgrade info from disk %s", err)) + } + + if app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { + return + } + + // register store loader for current upgrade + for _, upgrade := range Upgrades { + if upgradeInfo.Name == upgrade.UpgradeName { + app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &upgrade.StoreUpgrades)) // nolint:gosec + break + } + } +} + +func setupLegacyKeyTables(k paramskeeper.Keeper) { // Set param key table for params module migration - for _, subspace := range app.ParamsKeeper.GetSubspaces() { + for _, subspace := range k.GetSubspaces() { subspace := subspace var keyTable paramstypes.KeyTable switch subspace.Name() { - case authtypes.ModuleName: - keyTable = authtypes.ParamKeyTable() //nolint:staticcheck - case banktypes.ModuleName: - keyTable = banktypes.ParamKeyTable() //nolint:staticcheck - case stakingtypes.ModuleName: - keyTable = stakingtypes.ParamKeyTable() - case minttypes.ModuleName: - keyTable = minttypes.ParamKeyTable() //nolint:staticcheck - case distrtypes.ModuleName: - keyTable = distrtypes.ParamKeyTable() //nolint:staticcheck - case slashingtypes.ModuleName: - keyTable = slashingtypes.ParamKeyTable() //nolint:staticcheck - case govtypes.ModuleName: - keyTable = govv1.ParamKeyTable() //nolint:staticcheck - case crisistypes.ModuleName: - keyTable = crisistypes.ParamKeyTable() //nolint:staticcheck - // ibc types + // ibc types + case ibcexported.ModuleName: + keyTable = ibcclienttypes.ParamKeyTable() + keyTable.RegisterParamSet(&ibcconnectiontypes.Params{}) case ibctransfertypes.ModuleName: keyTable = ibctransfertypes.ParamKeyTable() case icahosttypes.SubModuleName: @@ -74,36 +89,4 @@ func (app WasmApp) RegisterUpgradeHandlers() { subspace.WithKeyTable(keyTable) } } - - baseAppLegacySS := app.ParamsKeeper.Subspace(baseapp.Paramspace).WithKeyTable(paramstypes.ConsensusParamsKeyTable()) - - app.UpgradeKeeper.SetUpgradeHandler( - UpgradeName, - func(ctx sdk.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { - // Migrate Tendermint consensus parameters from x/params module to a dedicated x/consensus module. - baseapp.MigrateParams(ctx, baseAppLegacySS, &app.ConsensusParamsKeeper) - - // Note: this migration is optional, - // You can include x/gov proposal migration documented in [UPGRADING.md](https://github.com/cosmos/cosmos-sdk/blob/main/UPGRADING.md) - - return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM) - }, - ) - - upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() - if err != nil { - panic(err) - } - - if upgradeInfo.Name == UpgradeName && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { - storeUpgrades := storetypes.StoreUpgrades{ - Added: []string{ - consensustypes.ModuleName, - crisistypes.ModuleName, - }, - } - - // configure store loader that checks if version == upgradeHeight and applies store upgrades - app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) - } } diff --git a/app/upgrades/noop/upgrades.go b/app/upgrades/noop/upgrades.go new file mode 100644 index 0000000000..ffd68b422c --- /dev/null +++ b/app/upgrades/noop/upgrades.go @@ -0,0 +1,32 @@ +package noop + +import ( + store "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + + "github.com/CosmWasm/wasmd/app/upgrades" +) + +// NewUpgrade constructor +func NewUpgrade(semver string) upgrades.Upgrade { + return upgrades.Upgrade{ + UpgradeName: semver, + CreateUpgradeHandler: CreateUpgradeHandler, + StoreUpgrades: store.StoreUpgrades{ + Added: []string{}, + Deleted: []string{}, + }, + } +} + +func CreateUpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + ak *upgrades.AppKeepers, +) upgradetypes.UpgradeHandler { + return func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + return mm.RunMigrations(ctx, configurator, fromVM) + } +} diff --git a/app/upgrades/types.go b/app/upgrades/types.go new file mode 100644 index 0000000000..5cfce557f5 --- /dev/null +++ b/app/upgrades/types.go @@ -0,0 +1,25 @@ +package upgrades + +import ( + store "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/types/module" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) + +type AppKeepers struct { + authkeeper.AccountKeeper +} + +// Upgrade defines a struct containing necessary fields that a SoftwareUpgradeProposal +// must have written, in order for the state migration to go smoothly. +// An upgrade must implement this struct, and then set it in the app.go. +// The app.go will then define the handler. +type Upgrade struct { + // Upgrade version name, for the upgrade handler, e.g. `v7` + UpgradeName string + + // CreateUpgradeHandler defines the function that creates an upgrade handler + CreateUpgradeHandler func(*module.Manager, module.Configurator, *AppKeepers) upgradetypes.UpgradeHandler + StoreUpgrades store.StoreUpgrades +} diff --git a/cmd/wasmd/testnet.go b/cmd/wasmd/testnet.go index c1d8dfd0fc..4e7c973599 100644 --- a/cmd/wasmd/testnet.go +++ b/cmd/wasmd/testnet.go @@ -166,7 +166,7 @@ Example: addTestnetFlagsToCmd(cmd) cmd.Flags().String(flagNodeDirPrefix, "node", "Prefix the directory name for each node with (node results in node0, node1, ...)") - cmd.Flags().String(flagNodeDaemonHome, "wasmd", "Home directory of the node's daemon configuration") + cmd.Flags().String(flagNodeDaemonHome, version.AppName, "Home directory of the node's daemon configuration") cmd.Flags().String(flagStartingIPAddress, "192.168.0.1", "Starting IP address (192.168.0.1 results in persistent peers list ID0@192.168.0.1:46656, ID1@192.168.0.2:46656, ...)") cmd.Flags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|test)") cmd.Flags().Duration(flagCommitTimeout, 5*time.Second, "Time to wait after a block commit before starting on the new height") diff --git a/tests/system/.gitignore b/tests/system/.gitignore index 73423081ac..22873da42d 100644 --- a/tests/system/.gitignore +++ b/tests/system/.gitignore @@ -1 +1,2 @@ /testnet +/binaries diff --git a/tests/system/Makefile b/tests/system/Makefile index 641305df18..b2396fd3fb 100644 --- a/tests/system/Makefile +++ b/tests/system/Makefile @@ -10,7 +10,6 @@ test: format: find . -name '*.go' -type f -not -path "./vendor*" -not -path "*.git*" -not -path "./client/lcd/statik/statik.go" | xargs gofumpt -w find . -name '*.go' -type f -not -path "./vendor*" -not -path "*.git*" -not -path "./client/lcd/statik/statik.go" | xargs misspell -w - find . -name '*.go' -type f -not -path "./vendor*" -not -path "*.git*" -not -path "./client/lcd/statik/statik.go" | xargs goimports -w -local github.com/CosmWasm/wasmd - + find . -name '*.go' -type f -not -path "./vendor*" -not -path "./tests/system/vendor*" -not -path "*.git*" -not -path "./client/lcd/statik/statik.go" | xargs gci write --skip-generated -s standard -s default -s "prefix(cosmossdk.io)" -s "prefix(github.com/cosmos/cosmos-sdk)" -s "prefix(github.com/CosmWasm/wasmd)" --custom-order .PHONY: all test format diff --git a/tests/system/basic_test.go b/tests/system/basic_test.go index 1fba127648..a6178381f5 100644 --- a/tests/system/basic_test.go +++ b/tests/system/basic_test.go @@ -50,10 +50,13 @@ func TestBasicWasm(t *testing.T) { t.Cleanup(cleanupFn) t.Log("Instantiate wasm code") - initMsg := fmt.Sprintf(`{"verifier":%q, "beneficiary":%q}`, randomBech32Addr(), randomBech32Addr()) + verifierAddr := randomBech32Addr() + initMsg := fmt.Sprintf(`{"verifier":%q, "beneficiary":%q}`, verifierAddr, randomBech32Addr()) newContractAddr := cli.WasmInstantiate(codeID, initMsg, "--admin="+defaultSrcAddr, "--label=label1", "--from="+defaultSrcAddr) - assert.Equal(t, expContractAddr, newContractAddr) - assert.Len(t, done(), 1) + require.Equal(t, expContractAddr, newContractAddr) + require.Len(t, done(), 1) + gotRsp := cli.QuerySmart(newContractAddr, `{"verifier":{}}`) + require.Equal(t, fmt.Sprintf(`{"data":{"verifier":"%s"}}`, verifierAddr), gotRsp) t.Log("Update Instantiate Config") qResult = cli.CustomQuery("q", "wasm", "code-info", fmt.Sprint(codeID)) diff --git a/tests/system/cli.go b/tests/system/cli.go index 7aeddb38ab..b30b1e7068 100644 --- a/tests/system/cli.go +++ b/tests/system/cli.go @@ -3,6 +3,7 @@ package system import ( "fmt" "io" + "os" "os/exec" "path/filepath" "strconv" @@ -10,17 +11,17 @@ import ( "testing" "time" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" - "github.com/cosmos/cosmos-sdk/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "golang.org/x/exp/slices" "github.com/cosmos/cosmos-sdk/client/rpc" "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/std" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" - "golang.org/x/exp/slices" ) type ( @@ -42,17 +43,19 @@ type WasmdCli struct { awaitNextBlock awaitNextBlock expTXCommitted bool execBinary string + nodesCount int } // NewWasmdCLI constructor func NewWasmdCLI(t *testing.T, sut *SystemUnderTest, verbose bool) *WasmdCli { return NewWasmdCLIx( t, - sut.execBinary, + sut.ExecBinary, sut.rpcAddr, sut.chainID, sut.AwaitNextBlock, - filepath.Join(workDir, sut.outputDir), + sut.nodesCount, + filepath.Join(WorkDir, sut.outputDir), "1"+sdk.DefaultBondDenom, verbose, assert.NoError, @@ -67,6 +70,7 @@ func NewWasmdCLIx( nodeAddress string, chainID string, awaiter awaitNextBlock, + nodesCount int, homeDir string, fees string, debug bool, @@ -84,6 +88,7 @@ func NewWasmdCLIx( homeDir: homeDir, Debug: debug, awaitNextBlock: awaiter, + nodesCount: nodesCount, fees: fees, assertErrorFn: assertErrorFn, expTXCommitted: expTXCommitted, @@ -105,6 +110,7 @@ func (c WasmdCli) WithRunErrorMatcher(f RunErrorAssert) WasmdCli { c.nodeAddress, c.chainID, c.awaitNextBlock, + c.nodesCount, c.homeDir, c.fees, c.Debug, @@ -120,6 +126,7 @@ func (c WasmdCli) WithNodeAddress(nodeAddr string) WasmdCli { nodeAddr, c.chainID, c.awaitNextBlock, + c.nodesCount, c.homeDir, c.fees, c.Debug, @@ -135,6 +142,7 @@ func (c WasmdCli) WithAssertTXUncommitted() WasmdCli { c.nodeAddress, c.chainID, c.awaitNextBlock, + c.nodesCount, c.homeDir, c.fees, c.Debug, @@ -156,7 +164,7 @@ func (c WasmdCli) CustomCommand(args ...string) string { if !ok { return execOutput } - rsp, committed := c.awaitTxCommitted(execOutput, defaultWaitTime) + rsp, committed := c.awaitTxCommitted(execOutput, DefaultWaitTime) c.t.Logf("tx committed: %v", committed) require.Equal(c.t, c.expTXCommitted, committed, "expected tx committed: %v", c.expTXCommitted) return rsp @@ -210,13 +218,13 @@ func (c WasmdCli) runWithInput(args []string, input io.Reader) (output string, o err = fmt.Errorf("recovered from panic: %v", r) } }() - cmd := exec.Command(locateExecutable("wasmd"), args...) //nolint:gosec - cmd.Dir = workDir + cmd := exec.Command(locateExecutable(c.execBinary), args...) //nolint:gosec + cmd.Dir = WorkDir cmd.Stdin = input return cmd.CombinedOutput() }() ok = c.assertErrorFn(c.t, gotErr, string(gotOut)) - return string(gotOut), ok + return strings.TrimSpace(string(gotOut)), ok } func (c WasmdCli) withQueryFlags(args ...string) []string { @@ -236,7 +244,7 @@ func (c WasmdCli) withTXFlags(args ...string) []string { } func (c WasmdCli) withKeyringFlags(args ...string) []string { - r := append(args, //nolint:gocritic + r := append(args, "--home", c.homeDir, "--keyring-backend", "test", ) @@ -262,7 +270,7 @@ func (c WasmdCli) WasmExecute(contractAddr, msg, from string, args ...string) st // AddKey add key to default keyring. Returns address func (c WasmdCli) AddKey(name string) string { - cmd := c.withKeyringFlags("keys", "add", name) //, "--no-backup") + cmd := c.withKeyringFlags("keys", "add", name, "--no-backup") out, _ := c.run(cmd) addr := gjson.Get(out, "address").String() require.NotEmpty(c.t, addr, "got %q", out) @@ -302,7 +310,7 @@ func (c WasmdCli) FundAddress(destAddr, amount string) string { // WasmStore uploads a wasm contract to the chain. Returns code id func (c WasmdCli) WasmStore(file string, args ...string) int { if len(args) == 0 { - args = []string{"--from=" + defaultSrcAddr, "--gas=2500000"} + args = []string{"--from=" + defaultSrcAddr, "--gas=2500000", "--fees=3stake"} } cmd := append([]string{"tx", "wasm", "store", file}, args...) rsp := c.CustomCommand(cmd...) @@ -368,8 +376,8 @@ func (c WasmdCli) GetTendermintValidatorSet() rpc.ResultValidatorsOutput { return res } -// IsInTendermintValset returns true when the given pub key is in the current active tendermint validator set -func (c WasmdCli) IsInTendermintValset(valPubKey cryptotypes.PubKey) (rpc.ResultValidatorsOutput, bool) { +// IsInCometBftValset returns true when the given pub key is in the current active tendermint validator set +func (c WasmdCli) IsInCometBftValset(valPubKey cryptotypes.PubKey) (rpc.ResultValidatorsOutput, bool) { valResult := c.GetTendermintValidatorSet() var found bool for _, v := range valResult.Validators { @@ -381,6 +389,45 @@ func (c WasmdCli) IsInTendermintValset(valPubKey cryptotypes.PubKey) (rpc.Result return valResult, found } + +// SubmitGovProposal submit a gov v1 proposal +func (c WasmdCli) SubmitGovProposal(proposalJson string, args ...string) string { + if len(args) == 0 { + args = []string{"--from=" + defaultSrcAddr} + } + + pathToProposal := filepath.Join(c.t.TempDir(), "proposal.json") + err := os.WriteFile(pathToProposal, []byte(proposalJson), os.FileMode(0o744)) + require.NoError(c.t, err) + c.t.Log("Submit upgrade proposal") + return c.CustomCommand(append([]string{"tx", "gov", "submit-proposal", pathToProposal}, args...)...) +} + +// SubmitAndVoteGovProposal submit proposal, let all validators vote yes and return proposal id +func (c WasmdCli) SubmitAndVoteGovProposal(proposalJson string, args ...string) string { + rsp := c.SubmitGovProposal(proposalJson, args...) + RequireTxSuccess(c.t, rsp) + raw := c.CustomQuery("q", "gov", "proposals", "--depositor", c.GetKeyAddr(defaultSrcAddr)) + proposals := gjson.Get(raw, "proposals.#.id").Array() + require.NotEmpty(c.t, proposals, raw) + ourProposalID := proposals[len(proposals)-1].String() // last is ours + for i := 0; i < c.nodesCount; i++ { + go func(i int) { // do parallel + c.t.Logf("Voting: validator %d\n", i) + rsp = c.CustomCommand("tx", "gov", "vote", ourProposalID, "yes", "--from", c.GetKeyAddr(fmt.Sprintf("node%d", i))) + RequireTxSuccess(c.t, rsp) + }(i) + } + return ourProposalID +} + +// Version returns the current version of the client binary +func (c WasmdCli) Version() string { + v, ok := c.run([]string{"version"}) + require.True(c.t, ok) + return v +} + // RequireTxSuccess require the received response to contain the success code func RequireTxSuccess(t *testing.T, got string) { t.Helper() diff --git a/tests/system/cli_test.go b/tests/system/cli_test.go index 02b486aadd..09b8b7b080 100644 --- a/tests/system/cli_test.go +++ b/tests/system/cli_test.go @@ -9,11 +9,13 @@ import ( "testing" "time" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" ) func TestUnsafeResetAll(t *testing.T) { @@ -22,14 +24,14 @@ func TestUnsafeResetAll(t *testing.T) { // when `unsafe-reset-all` is executed // then the dir and all files in it are removed - wasmDir := filepath.Join(workDir, sut.nodePath(0), "wasm") + wasmDir := filepath.Join(WorkDir, sut.nodePath(0), "wasm") require.NoError(t, os.MkdirAll(wasmDir, os.ModePerm)) _, err := os.CreateTemp(wasmDir, "testing") require.NoError(t, err) // when - sut.ForEachNodeExecAndWait(t, []string{"tendermint", "unsafe-reset-all"}) + sut.ForEachNodeExecAndWait(t, []string{"comet", "unsafe-reset-all"}) // then sut.withEachNodeHome(func(i int, home string) { @@ -94,7 +96,7 @@ func TestVestingAccounts(t *testing.T) { assert.Equal(t, myStartTimestamp, accounts[0].Get("start_time").Int()) // check accounts have some balances - assert.Equal(t, sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100000000))), GetGenesisBalance([]byte(raw), vest1Addr)) - assert.Equal(t, sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100000001))), GetGenesisBalance([]byte(raw), vest2Addr)) - assert.Equal(t, sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(200000002))), GetGenesisBalance([]byte(raw), vest3Addr)) + assert.Equal(t, sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(100000000))), GetGenesisBalance([]byte(raw), vest1Addr)) + assert.Equal(t, sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(100000001))), GetGenesisBalance([]byte(raw), vest2Addr)) + assert.Equal(t, sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(200000002))), GetGenesisBalance([]byte(raw), vest3Addr)) } diff --git a/tests/system/fraud_test.go b/tests/system/fraud_test.go index 9e5e4d61d5..65e4335d24 100644 --- a/tests/system/fraud_test.go +++ b/tests/system/fraud_test.go @@ -7,9 +7,9 @@ import ( "math" "testing" - sdkmath "cosmossdk.io/math" - "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" ) func TestRecursiveMsgsExternalTrigger(t *testing.T) { diff --git a/tests/system/genesis_io.go b/tests/system/genesis_io.go index 23e54f09e8..fdb22557dc 100644 --- a/tests/system/genesis_io.go +++ b/tests/system/genesis_io.go @@ -3,11 +3,13 @@ package system import ( "fmt" "testing" + "time" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" "github.com/tidwall/sjson" + + sdk "github.com/cosmos/cosmos-sdk/types" ) // SetConsensusMaxGas max gas that can be consumed in a block @@ -43,3 +45,12 @@ func SetCodeUploadPermission(t *testing.T, permission string, addresses ...strin return []byte(state) } } + +func SetGovVotingPeriod(t *testing.T, period time.Duration) GenesisMutator { + return func(genesis []byte) []byte { + t.Helper() + state, err := sjson.SetRawBytes(genesis, "app_state.gov.params.voting_period", []byte(fmt.Sprintf("%q", period.String()))) + require.NoError(t, err) + return state + } +} diff --git a/tests/system/main_test.go b/tests/system/main_test.go index e806addd4f..0467cc2f8f 100644 --- a/tests/system/main_test.go +++ b/tests/system/main_test.go @@ -14,18 +14,20 @@ import ( "time" "github.com/cometbft/cometbft/libs/rand" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/address" ) var ( - sut *SystemUnderTest - verbose bool + sut *SystemUnderTest + verbose bool + execBinaryName string ) func TestMain(m *testing.M) { rebuild := flag.Bool("rebuild", false, "rebuild artifacts") - waitTime := flag.Duration("wait-time", defaultWaitTime, "time to wait for chain events") + waitTime := flag.Duration("wait-time", DefaultWaitTime, "time to wait for chain events") nodesCount := flag.Int("nodes-count", 4, "number of nodes in the cluster") blockTime := flag.Duration("block-time", 1000*time.Millisecond, "block creation time") execBinary := flag.String("binary", "wasmd", "executable binary for server/ client side") @@ -40,16 +42,17 @@ func TestMain(m *testing.M) { if err != nil { panic(err) } - workDir = dir + WorkDir = dir if verbose { - println("Work dir: ", workDir) + println("Work dir: ", WorkDir) } initSDKConfig(*bech32Prefix) - defaultWaitTime = *waitTime + DefaultWaitTime = *waitTime if *execBinary == "" { panic("executable binary name must not be empty") } + execBinaryName = *execBinary sut = NewSystemUnderTest(*execBinary, verbose, *nodesCount, *blockTime) if *rebuild { sut.BuildNewBinary() @@ -78,7 +81,7 @@ func requireEnoughFileHandlers(nodesCount int) { } cmd := exec.Command(ulimit, "-n") - cmd.Dir = workDir + cmd.Dir = WorkDir out, err := cmd.CombinedOutput() if err != nil { panic(fmt.Sprintf("unexpected error :%#+v, output: %s", err, string(out))) diff --git a/tests/system/staking_test.go b/tests/system/staking_test.go index 2cb25d9778..3d4ba19557 100644 --- a/tests/system/staking_test.go +++ b/tests/system/staking_test.go @@ -21,7 +21,7 @@ func TestStakeUnstake(t *testing.T) { // add genesis account with some tokens account1Addr := cli.AddKey("account1") sut.ModifyGenesisCLI(t, - []string{"genesis", "add-genesis-account", account1Addr, "100000000stake"}, + []string{"genesis", "add-genesis-account", account1Addr, "10000000stake"}, ) sut.StartChain(t) @@ -35,7 +35,7 @@ func TestStakeUnstake(t *testing.T) { RequireTxSuccess(t, rsp) t.Log(cli.QueryBalance(account1Addr, "stake")) - assert.Equal(t, int64(99989999), cli.QueryBalance(account1Addr, "stake")) + assert.Equal(t, int64(9989999), cli.QueryBalance(account1Addr, "stake")) rsp = cli.CustomQuery("q", "staking", "delegation", account1Addr, valAddr) assert.Equal(t, "10000", gjson.Get(rsp, "balance.amount").String()) diff --git a/tests/system/system.go b/tests/system/system.go index 89b5ca0543..8d55763b7f 100644 --- a/tests/system/system.go +++ b/tests/system/system.go @@ -7,33 +7,39 @@ import ( "context" "fmt" "io" - "io/ioutil" "os" "os/exec" "path/filepath" + "regexp" "strconv" "strings" "sync/atomic" + "syscall" "testing" "time" - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/tidwall/sjson" - "github.com/cometbft/cometbft/libs/sync" client "github.com/cometbft/cometbft/rpc/client/http" ctypes "github.com/cometbft/cometbft/rpc/core/types" tmtypes "github.com/cometbft/cometbft/types" - "github.com/cosmos/cosmos-sdk/server" "github.com/stretchr/testify/require" + "github.com/tidwall/sjson" + + "github.com/cosmos/cosmos-sdk/server" + sdk "github.com/cosmos/cosmos-sdk/types" ) -var workDir string +var ( + // WorkDir is the directory where tests are executed. Path should be relative to this dir + WorkDir string + + // ExecBinaryUnversionedRegExp regular expression to extract the unversioned binary name + ExecBinaryUnversionedRegExp = regexp.MustCompile(`^(\w+)-?.*$`) +) // SystemUnderTest blockchain provisioning type SystemUnderTest struct { - execBinary string + ExecBinary string blockListener *EventListener currentHeight int64 chainID string @@ -49,16 +55,25 @@ type SystemUnderTest struct { out io.Writer verbose bool ChainStarted bool + projectName string dirty bool // requires full reset when marked dirty + + pidsLock sync.RWMutex + pids map[int]struct{} } func NewSystemUnderTest(execBinary string, verbose bool, nodesCount int, blockTime time.Duration) *SystemUnderTest { if execBinary == "" { panic("executable binary name must not be empty") } + nameTokens := ExecBinaryUnversionedRegExp.FindAllString(execBinary, 1) + if len(nameTokens) == 0 || nameTokens[0] == "" { + panic("failed to parse project name from binary") + } + return &SystemUnderTest{ chainID: "testing", - execBinary: execBinary, + ExecBinary: execBinary, outputDir: "./testnet", blockTime: blockTime, rpcAddr: "tcp://localhost:26657", @@ -68,12 +83,14 @@ func NewSystemUnderTest(execBinary string, verbose bool, nodesCount int, blockTi out: os.Stdout, verbose: verbose, minGasPrice: fmt.Sprintf("0.000001%s", sdk.DefaultBondDenom), + projectName: nameTokens[0], + pids: make(map[int]struct{}, nodesCount), } } func (s *SystemUnderTest) SetupChain() { s.Logf("Setup chain: %s\n", s.outputDir) - if err := os.RemoveAll(filepath.Join(workDir, s.outputDir)); err != nil { + if err := os.RemoveAll(filepath.Join(WorkDir, s.outputDir)); err != nil { panic(err.Error()) } @@ -89,12 +106,12 @@ func (s *SystemUnderTest) SetupChain() { "--starting-ip-address", "", // empty to use host systems "--single-host", } - fmt.Printf("+++ %s %s", s.execBinary, strings.Join(args, " ")) + fmt.Printf("+++ %s %s\n", s.ExecBinary, strings.Join(args, " ")) cmd := exec.Command( //nolint:gosec - locateExecutable(s.execBinary), + locateExecutable(s.ExecBinary), args..., ) - cmd.Dir = workDir + cmd.Dir = WorkDir out, err := cmd.CombinedOutput() if err != nil { panic(fmt.Sprintf("unexpected error :%#+v, output: %s", err, string(out))) @@ -103,8 +120,8 @@ func (s *SystemUnderTest) SetupChain() { s.nodesCount = s.initialNodesCount // modify genesis with system test defaults - src := filepath.Join(workDir, s.nodePath(0), "config", "genesis.json") - genesisBz, err := ioutil.ReadFile(src) + src := filepath.Join(WorkDir, s.nodePath(0), "config", "genesis.json") + genesisBz, err := os.ReadFile(src) if err != nil { panic(fmt.Sprintf("failed to load genesis: %s", err)) } @@ -120,13 +137,13 @@ func (s *SystemUnderTest) SetupChain() { }) // backup genesis - dest := filepath.Join(workDir, s.nodePath(0), "config", "genesis.json.orig") + dest := filepath.Join(WorkDir, s.nodePath(0), "config", "genesis.json.orig") if _, err := copyFile(src, dest); err != nil { panic(fmt.Sprintf("copy failed :%#+v", err)) } // backup keyring - src = filepath.Join(workDir, s.nodePath(0), "keyring-test") - dest = filepath.Join(workDir, s.outputDir, "keyring-test") + src = filepath.Join(WorkDir, s.nodePath(0), "keyring-test") + dest = filepath.Join(WorkDir, s.outputDir, "keyring-test") if err := copyFilesInDir(src, dest); err != nil { panic(fmt.Sprintf("copy files from dir :%#+v", err)) } @@ -136,7 +153,7 @@ func (s *SystemUnderTest) StartChain(t *testing.T, xargs ...string) { t.Helper() s.Log("Start chain\n") s.ChainStarted = true - s.forEachNodesExecAsync(t, append([]string{"start", "--trace", "--log_level=info"}, xargs...)...) + s.startNodesAsync(t, append([]string{"start", "--trace", "--log_level=info"}, xargs...)...) s.AwaitNodeUp(t, s.rpcAddr) @@ -159,13 +176,13 @@ func (s *SystemUnderTest) MarkDirty() { } // IsDirty true when non default genesis or other state modification were applied that might create incompatibility for tests -func (s SystemUnderTest) IsDirty() bool { +func (s *SystemUnderTest) IsDirty() bool { return s.dirty } // watchLogs stores stdout/stderr in a file and in a ring buffer to output the last n lines on test error func (s *SystemUnderTest) watchLogs(node int, cmd *exec.Cmd) { - logfile, err := os.Create(filepath.Join(workDir, s.outputDir, fmt.Sprintf("node%d.out", node))) + logfile, err := os.Create(filepath.Join(WorkDir, s.outputDir, fmt.Sprintf("node%d.out", node))) if err != nil { panic(fmt.Sprintf("open logfile error %#+v", err)) } @@ -184,7 +201,7 @@ func (s *SystemUnderTest) watchLogs(node int, cmd *exec.Cmd) { go appendToBuf(io.TeeReader(outReader, logfile), s.outBuff, stopRingBuffer) s.cleanupFn = append(s.cleanupFn, func() { close(stopRingBuffer) - logfile.Close() + _ = logfile.Close() }) } @@ -217,11 +234,34 @@ func isLogNoise(text string) bool { return false } +// AwaitUpgradeInfo blocks util an upgrade info file is persisted to disk +func (s *SystemUnderTest) AwaitUpgradeInfo(t *testing.T) { + var found bool + for !found { + s.withEachNodeHome(func(i int, home string) { + _, err := os.Stat(filepath.Join(s.nodePath(0), "data", "upgrade-info.json")) + switch { + case err == nil: + found = true + case !os.IsNotExist(err): + t.Fatalf(err.Error()) + } + }) + time.Sleep(s.blockTime / 2) + } +} + +func (s *SystemUnderTest) AwaitChainStopped() { + for s.anyNodeRunning() { + time.Sleep(s.blockTime) + } +} + // AwaitNodeUp ensures the node is running func (s *SystemUnderTest) AwaitNodeUp(t *testing.T, rpcAddr string) { t.Helper() t.Logf("Await node is up: %s", rpcAddr) - timeout := defaultWaitTime + timeout := DefaultWaitTime ctx, done := context.WithTimeout(context.Background(), timeout) defer done() @@ -265,34 +305,45 @@ func (s *SystemUnderTest) StopChain() { } s.cleanupFn = nil // send SIGTERM - cmd := exec.Command(locateExecutable("pkill"), "-15", s.execBinary) //nolint:gosec - cmd.Dir = workDir - if _, err := cmd.CombinedOutput(); err != nil { - s.Logf("failed to stop chain: %s\n", err) - } - - var shutdown bool - for timeout := time.NewTimer(500 * time.Millisecond).C; !shutdown; { - select { - case <-timeout: - s.Log("killing nodes now") - cmd = exec.Command(locateExecutable("pkill"), "-9", s.execBinary) //nolint:gosec - cmd.Dir = workDir - if _, err := cmd.CombinedOutput(); err != nil { - s.Logf("failed to kill process: %s\n", err) + s.withEachPid(func(p *os.Process) { + go func() { + if err := p.Signal(syscall.SIGTERM); err != nil { + s.Logf("failed to stop node with pid %d: %s\n", p.Pid, err) } - shutdown = true - default: - if err := exec.Command(locateExecutable("pgrep"), s.execBinary).Run(); err != nil { //nolint:gosec - shutdown = true + }() + }) + // give some final time to shut down + s.withEachPid(func(p *os.Process) { + time.Sleep(200 * time.Millisecond) + }) + // goodbye + for ; s.anyNodeRunning(); time.Sleep(100 * time.Millisecond) { + s.withEachPid(func(p *os.Process) { + s.Logf("killing node %d\n", p.Pid) + if err := p.Kill(); err != nil { + s.Logf("failed to kill node with pid %d: %s\n", p.Pid, err) } - } + }) } s.ChainStarted = false } +func (s *SystemUnderTest) withEachPid(cb func(p *os.Process)) { + s.pidsLock.RLock() + pids := s.pids + s.pidsLock.RUnlock() + + for pid := range pids { + p, err := os.FindProcess(pid) + if err != nil { + continue + } + cb(p) + } +} + // PrintBuffer prints the chain logs to the console -func (s SystemUnderTest) PrintBuffer() { +func (s *SystemUnderTest) PrintBuffer() { s.outBuff.Do(func(v interface{}) { if v != nil { fmt.Fprintf(s.out, "out> %s\n", v) @@ -307,17 +358,41 @@ func (s SystemUnderTest) PrintBuffer() { } // BuildNewBinary builds and installs new executable binary -func (s SystemUnderTest) BuildNewBinary() { +func (s *SystemUnderTest) BuildNewBinary() { s.Log("Install binaries\n") makePath := locateExecutable("make") cmd := exec.Command(makePath, "clean", "install") - cmd.Dir = workDir + cmd.Dir = WorkDir out, err := cmd.CombinedOutput() if err != nil { panic(fmt.Sprintf("unexpected error %#v : output: %s", err, string(out))) } } +// AwaitBlockHeight blocks until te target height is reached. An optional timout parameter can be passed to abort early +func (s *SystemUnderTest) AwaitBlockHeight(t *testing.T, targetHeight int64, timeout ...time.Duration) { + t.Helper() + require.Greater(t, targetHeight, s.currentHeight) + var maxWaitTime time.Duration + if len(timeout) != 0 { + maxWaitTime = timeout[0] + } else { + maxWaitTime = time.Duration(targetHeight-s.currentHeight+3) * s.blockTime + } + abort := time.NewTimer(maxWaitTime).C + for { + select { + case <-abort: + t.Fatalf("Timeout - block %d not reached within %s", targetHeight, maxWaitTime) + return + default: + if current := s.AwaitNextBlock(t); current >= targetHeight { + return + } + } + } +} + // AwaitNextBlock is a first class function that any caller can use to ensure a new block was minted. // Returns the new height func (s *SystemUnderTest) AwaitNextBlock(t *testing.T, timeout ...time.Duration) int64 { @@ -355,18 +430,18 @@ func (s *SystemUnderTest) ResetChain(t *testing.T) { t.Helper() t.Log("Reset chain") s.StopChain() - restoreOriginalGenesis(t, *s) - restoreOriginalKeyring(t, *s) + restoreOriginalGenesis(t, s) + restoreOriginalKeyring(t, s) s.resetBuffers() // remove all additional nodes for i := s.initialNodesCount; i < s.nodesCount; i++ { - os.RemoveAll(filepath.Join(workDir, s.nodePath(i))) - os.Remove(filepath.Join(workDir, s.outputDir, fmt.Sprintf("node%d.out", i))) + _ = os.RemoveAll(filepath.Join(WorkDir, s.nodePath(i))) + _ = os.Remove(filepath.Join(WorkDir, s.outputDir, fmt.Sprintf("node%d.out", i))) } s.nodesCount = s.initialNodesCount - // reset all validataor nodes + // reset all validator nodes s.ForEachNodeExecAndWait(t, []string{"tendermint", "unsafe-reset-all"}) s.currentHeight = 0 s.dirty = false @@ -397,7 +472,7 @@ func (s *SystemUnderTest) ModifyGenesisJSON(t *testing.T, mutators ...GenesisMut // modify json without enforcing a reset func (s *SystemUnderTest) modifyGenesisJSON(t *testing.T, mutators ...GenesisMutator) { require.Empty(t, s.currentHeight, "forced chain reset required") - current, err := os.ReadFile(filepath.Join(workDir, s.nodePath(0), "config", "genesis.json")) + current, err := os.ReadFile(filepath.Join(WorkDir, s.nodePath(0), "config", "genesis.json")) require.NoError(t, err) for _, m := range mutators { current = m(current) @@ -410,7 +485,7 @@ func (s *SystemUnderTest) modifyGenesisJSON(t *testing.T, mutators ...GenesisMut // ReadGenesisJSON returns current genesis.json content as raw string func (s *SystemUnderTest) ReadGenesisJSON(t *testing.T) string { - content, err := os.ReadFile(filepath.Join(workDir, s.nodePath(0), "config", "genesis.json")) + content, err := os.ReadFile(filepath.Join(WorkDir, s.nodePath(0), "config", "genesis.json")) require.NoError(t, err) return string(content) } @@ -431,7 +506,7 @@ func (s *SystemUnderTest) setGenesis(t *testing.T, srcPath string) { } func saveGenesis(home string, content []byte) error { - out, err := os.Create(filepath.Join(workDir, home, "config", "genesis.json")) + out, err := os.Create(filepath.Join(WorkDir, home, "config", "genesis.json")) if err != nil { return fmt.Errorf("out file: %w", err) } @@ -455,12 +530,12 @@ func (s *SystemUnderTest) ForEachNodeExecAndWait(t *testing.T, cmds ...[]string) result[i] = make([]string, len(cmds)) for j, xargs := range cmds { xargs = append(xargs, "--home", home) - s.Logf("Execute `%s %s`\n", s.execBinary, strings.Join(xargs, " ")) + s.Logf("Execute `%s %s`\n", s.ExecBinary, strings.Join(xargs, " ")) cmd := exec.Command( //nolint:gosec - locateExecutable(s.execBinary), + locateExecutable(s.ExecBinary), xargs..., ) - cmd.Dir = workDir + cmd.Dir = WorkDir out, err := cmd.CombinedOutput() require.NoError(t, err, "node %d: %s", i, string(out)) s.Logf("Result: %s\n", string(out)) @@ -470,50 +545,62 @@ func (s *SystemUnderTest) ForEachNodeExecAndWait(t *testing.T, cmds ...[]string) return result } -// forEachNodesExecAsync runs the given app cli command for all cluster nodes and returns without waiting -func (s *SystemUnderTest) forEachNodesExecAsync(t *testing.T, xargs ...string) []func() error { - r := make([]func() error, s.nodesCount) +// startNodesAsync runs the given app cli command for all cluster nodes and returns without waiting +func (s *SystemUnderTest) startNodesAsync(t *testing.T, xargs ...string) { s.withEachNodeHome(func(i int, home string) { - args := append(xargs, "--home", home) //nolint:gocritic - s.Logf("Execute `%s %s`\n", s.execBinary, strings.Join(args, " ")) + args := append(xargs, "--home", home) + s.Logf("Execute `%s %s`\n", s.ExecBinary, strings.Join(args, " ")) cmd := exec.Command( //nolint:gosec - locateExecutable(s.execBinary), + locateExecutable(s.ExecBinary), args..., ) - cmd.Dir = workDir + cmd.Dir = WorkDir s.watchLogs(i, cmd) require.NoError(t, cmd.Start(), "node %d", i) - r[i] = cmd.Wait + + pid := cmd.Process.Pid + s.pidsLock.Lock() + s.pids[pid] = struct{}{} + s.pidsLock.Unlock() + s.Logf("Node started: %d\n", pid) + + // cleanup when stopped + go func(pid int) { + _ = cmd.Wait() // blocks until shutdown + s.pidsLock.Lock() + delete(s.pids, pid) + s.pidsLock.Unlock() + s.Logf("Node stopped: %d\n", pid) + }(pid) }) - return r } -func (s SystemUnderTest) withEachNodeHome(cb func(i int, home string)) { +func (s *SystemUnderTest) withEachNodeHome(cb func(i int, home string)) { for i := 0; i < s.nodesCount; i++ { cb(i, s.nodePath(i)) } } // nodePath returns the path of the node within the work dir. not absolute -func (s SystemUnderTest) nodePath(i int) string { - return fmt.Sprintf("%s/node%d/%s", s.outputDir, i, s.execBinary) +func (s *SystemUnderTest) nodePath(i int) string { + return fmt.Sprintf("%s/node%d/%s", s.outputDir, i, s.projectName) } -func (s SystemUnderTest) Log(msg string) { +func (s *SystemUnderTest) Log(msg string) { if s.verbose { - fmt.Fprint(s.out, msg) + _, _ = fmt.Fprint(s.out, msg) } } -func (s SystemUnderTest) Logf(msg string, args ...interface{}) { +func (s *SystemUnderTest) Logf(msg string, args ...interface{}) { s.Log(fmt.Sprintf(msg, args...)) } -func (s SystemUnderTest) RPCClient(t *testing.T) RPCClient { +func (s *SystemUnderTest) RPCClient(t *testing.T) RPCClient { return NewRPCClient(t, s.rpcAddr) } -func (s SystemUnderTest) AllPeers(t *testing.T) []string { +func (s *SystemUnderTest) AllPeers(t *testing.T) []string { result := make([]string, s.nodesCount) for i, n := range s.AllNodes(t) { result[i] = n.PeerAddr() @@ -521,7 +608,7 @@ func (s SystemUnderTest) AllPeers(t *testing.T) []string { return result } -func (s SystemUnderTest) AllNodes(t *testing.T) []Node { +func (s *SystemUnderTest) AllNodes(t *testing.T) []Node { result := make([]Node, s.nodesCount) outs := s.ForEachNodeExecAndWait(t, []string{"tendermint", "show-node-id"}) ip, err := server.ExternalIP() @@ -554,20 +641,20 @@ func (s *SystemUnderTest) AddFullnode(t *testing.T, beforeStart ...func(nodeNumb // prepare new node moniker := fmt.Sprintf("node%d", nodeNumber) args := []string{"init", moniker, "--home", nodePath, "--overwrite"} - s.Logf("Execute `%s %s`\n", s.execBinary, strings.Join(args, " ")) + s.Logf("Execute `%s %s`\n", s.ExecBinary, strings.Join(args, " ")) cmd := exec.Command( //nolint:gosec - locateExecutable(s.execBinary), + locateExecutable(s.ExecBinary), args..., ) - cmd.Dir = workDir + cmd.Dir = WorkDir s.watchLogs(nodeNumber, cmd) require.NoError(t, cmd.Run(), "failed to start node with id %d", nodeNumber) require.NoError(t, saveGenesis(nodePath, []byte(s.ReadGenesisJSON(t)))) // quick hack: copy config and overwrite by start params - configFile := filepath.Join(workDir, nodePath, "config", "config.toml") + configFile := filepath.Join(WorkDir, nodePath, "config", "config.toml") _ = os.Remove(configFile) - _, err := copyFile(filepath.Join(workDir, s.nodePath(0), "config", "config.toml"), configFile) + _, err := copyFile(filepath.Join(WorkDir, s.nodePath(0), "config", "config.toml"), configFile) require.NoError(t, err) // start node @@ -591,12 +678,12 @@ func (s *SystemUnderTest) AddFullnode(t *testing.T, beforeStart ...func(nodeNumb "--log_level=info", "--home", nodePath, } - s.Logf("Execute `%s %s`\n", s.execBinary, strings.Join(args, " ")) + s.Logf("Execute `%s %s`\n", s.ExecBinary, strings.Join(args, " ")) cmd = exec.Command( //nolint:gosec - locateExecutable(s.execBinary), + locateExecutable(s.ExecBinary), args..., ) - cmd.Dir = workDir + cmd.Dir = WorkDir s.watchLogs(nodeNumber, cmd) require.NoError(t, cmd.Start(), "node %d", nodeNumber) return node @@ -607,6 +694,13 @@ func (s *SystemUnderTest) NewEventListener(t *testing.T) *EventListener { return NewEventListener(t, s.rpcAddr) } +// is any process let running? +func (s *SystemUnderTest) anyNodeRunning() bool { + s.pidsLock.RLock() + defer s.pidsLock.RUnlock() + return len(s.pids) != 0 +} + type Node struct { ID string IP string @@ -651,7 +745,7 @@ func NewEventListener(t *testing.T, rpcAddr string) *EventListener { return &EventListener{client: httpClient, t: t} } -var defaultWaitTime = 30 * time.Second +var DefaultWaitTime = 30 * time.Second type ( CleanupFn func() @@ -666,7 +760,7 @@ func (l *EventListener) Subscribe(query string, cb EventConsumer) func() { eventsChan, err := l.client.WSEvents.Subscribe(ctx, "testing", query) require.NoError(l.t, err) cleanup := func() { - ctx, _ := context.WithTimeout(ctx, defaultWaitTime) //nolint:govet + ctx, _ := context.WithTimeout(ctx, DefaultWaitTime) //nolint:govet go l.client.WSEvents.Unsubscribe(ctx, "testing", query) //nolint:errcheck done() } @@ -684,7 +778,7 @@ func (l *EventListener) Subscribe(query string, cb EventConsumer) func() { // For query syntax See https://docs.cosmos.network/master/core/events.html#subscribing-to-events func (l *EventListener) AwaitQuery(query string, optMaxWaitTime ...time.Duration) *ctypes.ResultEvent { c, result := CaptureSingleEventConsumer() - maxWaitTime := defaultWaitTime + maxWaitTime := DefaultWaitTime if len(optMaxWaitTime) != 0 { maxWaitTime = optMaxWaitTime[0] } @@ -746,7 +840,7 @@ func CaptureSingleEventConsumer() (EventConsumer, *ctypes.ResultEvent) { // // assert.Len(t, done(), 1) // then verify your assumption func CaptureAllEventsConsumer(t *testing.T, optMaxWaitTime ...time.Duration) (c EventConsumer, done func() []ctypes.ResultEvent) { - maxWaitTime := defaultWaitTime + maxWaitTime := DefaultWaitTime if len(optMaxWaitTime) != 0 { maxWaitTime = optMaxWaitTime[0] } @@ -774,17 +868,17 @@ func CaptureAllEventsConsumer(t *testing.T, optMaxWaitTime ...time.Duration) (c } // restoreOriginalGenesis replace nodes genesis by the one created on setup -func restoreOriginalGenesis(t *testing.T, s SystemUnderTest) { - src := filepath.Join(workDir, s.nodePath(0), "config", "genesis.json.orig") +func restoreOriginalGenesis(t *testing.T, s *SystemUnderTest) { + src := filepath.Join(WorkDir, s.nodePath(0), "config", "genesis.json.orig") s.setGenesis(t, src) } // restoreOriginalKeyring replaces test keyring with original -func restoreOriginalKeyring(t *testing.T, s SystemUnderTest) { - dest := filepath.Join(workDir, s.outputDir, "keyring-test") +func restoreOriginalKeyring(t *testing.T, s *SystemUnderTest) { + dest := filepath.Join(WorkDir, s.outputDir, "keyring-test") require.NoError(t, os.RemoveAll(dest)) for i := 0; i < s.initialNodesCount; i++ { - src := filepath.Join(workDir, s.nodePath(i), "keyring-test") + src := filepath.Join(WorkDir, s.nodePath(i), "keyring-test") require.NoError(t, copyFilesInDir(src, dest)) } } @@ -812,7 +906,7 @@ func copyFilesInDir(src, dest string) error { if err != nil { return fmt.Errorf("mkdirs: %s", err) } - fs, err := ioutil.ReadDir(src) + fs, err := os.ReadDir(src) if err != nil { return fmt.Errorf("read dir: %s", err) } @@ -828,7 +922,7 @@ func copyFilesInDir(src, dest string) error { } func storeTempFile(t *testing.T, content []byte) *os.File { - out, err := ioutil.TempFile(t.TempDir(), "genesis") + out, err := os.CreateTemp(t.TempDir(), "genesis") require.NoError(t, err) _, err = io.Copy(out, bytes.NewReader(content)) require.NoError(t, err) diff --git a/tests/system/upgrade_test.go b/tests/system/upgrade_test.go new file mode 100644 index 0000000000..e84af0e36f --- /dev/null +++ b/tests/system/upgrade_test.go @@ -0,0 +1,153 @@ +//go:build system_test + +package system + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestChainUpgrade(t *testing.T) { + // Scenario: + // start a legacy chain with some state + // when a chain upgrade proposal is executed + // then the chain upgrades successfully + + cli := NewWasmdCLI(t, sut, verbose) + + legacyBinary := FetchExecutable(t, "v0.41.0") + t.Logf("+++ legacy binary: %s\n", legacyBinary) + currentBranchBinary := sut.ExecBinary + sut.ExecBinary = legacyBinary + sut.SetupChain() + votingPeriod := 5 * time.Second // enough time to vote + sut.ModifyGenesisJSON(t, SetGovVotingPeriod(t, votingPeriod)) + + const upgradeHeight int64 = 22 + upgradeName := cli.Version() + + sut.StartChain(t, fmt.Sprintf("--halt-height=%d", upgradeHeight)) + legacyCli := NewWasmdCLI(t, sut, verbose) + + // set some state to ensure that migrations work + verifierAddr := legacyCli.AddKey("verifier") + beneficiary := randomBech32Addr() + legacyCli.FundAddress(verifierAddr, "1000stake") + + t.Log("Launch hackatom contract") + codeID := legacyCli.WasmStore("./testdata/hackatom.wasm.gzip") + initMsg := fmt.Sprintf(`{"verifier":%q, "beneficiary":%q}`, verifierAddr, beneficiary) + contractAddr := legacyCli.WasmInstantiate(codeID, initMsg, "--admin="+defaultSrcAddr, "--label=label1", "--from="+defaultSrcAddr, "--amount=1000000stake") + + gotRsp := legacyCli.QuerySmart(contractAddr, `{"verifier":{}}`) + require.Equal(t, fmt.Sprintf(`{"data":{"verifier":"%s"}}`, verifierAddr), gotRsp) + + // submit upgrade proposal + proposal := fmt.Sprintf(` +{ + "messages": [ + { + "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", + "authority": "wasm10d07y265gmmuvt4z0w9aw880jnsr700js7zslc", + "plan": { + "name": %q, + "height": "%d" + } + } + ], + "metadata": "ipfs://CID", + "deposit": "100000000stake", + "title": "my upgrade", + "summary": "testing" +}`, upgradeName, upgradeHeight) + proposalID := legacyCli.SubmitAndVoteGovProposal(proposal) + t.Logf("current_height: %d\n", sut.currentHeight) + raw := legacyCli.CustomQuery("q", "gov", "proposal", proposalID) + t.Log(raw) + sut.AwaitBlockHeight(t, upgradeHeight-1) + t.Logf("current_height: %d\n", sut.currentHeight) + raw = legacyCli.CustomQuery("q", "gov", "proposal", proposalID) + proposalStatus := gjson.Get(raw, "status").String() + require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) + + t.Log("waiting for upgrade info") + sut.AwaitUpgradeInfo(t) + sut.StopChain() + + t.Log("Upgrade height was reached. Upgrading chain") + sut.ExecBinary = currentBranchBinary + sut.StartChain(t) + + // ensure that state matches expectations + gotRsp = cli.QuerySmart(contractAddr, `{"verifier":{}}`) + require.Equal(t, fmt.Sprintf(`{"data":{"verifier":"%s"}}`, verifierAddr), gotRsp) + // and contract execution works as expected + RequireTxSuccess(t, cli.WasmExecute(contractAddr, `{"release":{}}`, verifierAddr)) + assert.Equal(t, int64(1_000_000), cli.QueryBalance(beneficiary, "stake")) +} + +const cacheDir = "binaries" + +// FetchExecutable to download and extract tar.gz for linux +func FetchExecutable(t *testing.T, version string) string { + // use local cache + cacheFolder := filepath.Join(WorkDir, cacheDir) + err := os.MkdirAll(cacheFolder, 0o777) + if err != nil && !os.IsExist(err) { + panic(err) + } + + cacheFile := filepath.Join(cacheFolder, fmt.Sprintf("%s_%s", execBinaryName, version)) + if _, err := os.Stat(cacheFile); err == nil { + return cacheFile + } + t.Logf("+++ version not in cache, downloading from github") + + // then download from GH releases: only works with Linux currently as we are not publishing OSX binaries + const releaseUrl = "https://github.com/CosmWasm/wasmd/releases/download/%s/wasmd-%s-linux-amd64.tar.gz" + destDir := t.TempDir() + rsp, err := http.Get(fmt.Sprintf(releaseUrl, version, version)) + require.NoError(t, err) + defer rsp.Body.Close() + gzr, err := gzip.NewReader(rsp.Body) + require.NoError(t, err) + defer gzr.Close() + tr := tar.NewReader(gzr) + + var workFileName string + for { + header, err := tr.Next() + switch { + case err == io.EOF: + require.NotEmpty(t, workFileName) + require.NoError(t, os.Rename(workFileName, cacheFile)) + return cacheFile + case err != nil: + require.NoError(t, err) + case header == nil: + continue + } + workFileName = filepath.Join(destDir, header.Name) + switch header.Typeflag { + case tar.TypeDir: + t.Fatalf("unexpected type") + case tar.TypeReg: + f, err := os.OpenFile(workFileName, os.O_CREATE|os.O_RDWR, os.FileMode(0o755)) + require.NoError(t, err) + _, err = io.Copy(f, tr) + require.NoError(t, err) + _ = f.Close() + } + } +} diff --git a/x/wasm/keeper/testdata/contracts.go b/x/wasm/keeper/testdata/contracts.go index dc81aa23d5..299fc9a324 100644 --- a/x/wasm/keeper/testdata/contracts.go +++ b/x/wasm/keeper/testdata/contracts.go @@ -4,6 +4,7 @@ import ( _ "embed" typwasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/types" )