diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..e9846e5a5 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,42 @@ +name: Run End-To-End Tests + +on: + workflow_dispatch: + pull_request: + push: + branches: + - frag/foundation +permissions: + contents: read + packages: write + +jobs: + e2e: + name: Tests + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - + name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.20.4' + - + name: Check out repository code + uses: actions/checkout@v4 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Build e2e image + uses: docker/build-push-action@v3 + with: + file: ./tests/e2e/e2e.Dockerfile + context: . + platforms: linux/amd64 + tags: terra:debug + build-args: | + BASE_IMG_TAG=debug + - + name: Testing + run: make test-e2e \ No newline at end of file diff --git a/Makefile b/Makefile index 344b4b783..9065df090 100755 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ #!/usr/bin/make -f +include tests/e2e/e2e.mk + PACKAGES_SIMTEST=$(shell go list ./... | grep '/simulation') VERSION := $(shell echo $(shell git describe --tags) | sed 's/^v//') COMMIT := $(shell git log -1 --format='%H') diff --git a/app/app.go b/app/app.go index 9e41a3c9c..cdd662ab8 100644 --- a/app/app.go +++ b/app/app.go @@ -49,6 +49,7 @@ import ( v5 "github.com/classic-terra/core/v2/app/upgrades/v5" v6 "github.com/classic-terra/core/v2/app/upgrades/v6" v6_1 "github.com/classic-terra/core/v2/app/upgrades/v6_1" + v7 "github.com/classic-terra/core/v2/app/upgrades/v7" customante "github.com/classic-terra/core/v2/custom/auth/ante" custompost "github.com/classic-terra/core/v2/custom/auth/post" @@ -67,7 +68,7 @@ var ( DefaultNodeHome string // Upgrades defines upgrades to be applied to the network - Upgrades = []upgrades.Upgrade{v2.Upgrade, v3.Upgrade, v4.Upgrade, v5.Upgrade, v6.Upgrade, v6_1.Upgrade} + Upgrades = []upgrades.Upgrade{v2.Upgrade, v3.Upgrade, v4.Upgrade, v5.Upgrade, v6.Upgrade, v6_1.Upgrade, v7.Upgrade} // Forks defines forks to be applied to the network Forks = []upgrades.Fork{} diff --git a/app/upgrades/v7/constants.go b/app/upgrades/v7/constants.go new file mode 100644 index 000000000..4b8dab9ea --- /dev/null +++ b/app/upgrades/v7/constants.go @@ -0,0 +1,21 @@ +package v7 + +import ( + "github.com/classic-terra/core/v2/app/upgrades" + store "github.com/cosmos/cosmos-sdk/store/types" + forwardtypes "github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v6/router/types" + ibc_hooks_types "github.com/terra-money/core/v2/x/ibc-hooks/types" +) + +const UpgradeName = "v7" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateV7UpgradeHandler, + StoreUpgrades: store.StoreUpgrades{ + Added: []string{ + ibc_hooks_types.StoreKey, + forwardtypes.StoreKey, + }, + }, +} diff --git a/app/upgrades/v7/upgrades.go b/app/upgrades/v7/upgrades.go new file mode 100644 index 000000000..950a70059 --- /dev/null +++ b/app/upgrades/v7/upgrades.go @@ -0,0 +1,20 @@ +package v7 + +import ( + "github.com/classic-terra/core/v2/app/keepers" + "github.com/classic-terra/core/v2/app/upgrades" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) + +func CreateV7UpgradeHandler( + mm *module.Manager, + cfg module.Configurator, + _ upgrades.BaseAppParamManager, + _ *keepers.AppKeepers, +) upgradetypes.UpgradeHandler { + return func(ctx sdk.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + return mm.RunMigrations(ctx, cfg, fromVM) + } +} diff --git a/go.mod b/go.mod index 6cb7a7f92..67f9af51e 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,28 @@ require ( gopkg.in/yaml.v2 v2.4.0 ) +require ( + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/docker/cli v20.10.17+incompatible // indirect + github.com/docker/docker v20.10.19+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect + github.com/opencontainers/image-spec v1.1.0-rc2 // indirect + github.com/opencontainers/runc v1.1.5 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/mod v0.11.0 // indirect + golang.org/x/tools v0.7.0 // indirect +) + require ( cloud.google.com/go v0.110.8 // indirect cloud.google.com/go/compute v1.23.0 // indirect @@ -58,7 +80,7 @@ require ( github.com/confio/ics23/go v0.9.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.3 // indirect - github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/gorocksdb v1.2.0 // indirect github.com/cosmos/iavl v0.19.7 // indirect github.com/cosmos/ledger-cosmos-go v0.12.2 // indirect @@ -125,6 +147,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/ory/dockertest/v3 v3.10.0 github.com/pelletier/go-toml/v2 v2.0.7 // indirect github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -139,7 +162,7 @@ require ( github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/viper v1.15.0 // indirect + github.com/spf13/viper v1.15.0 github.com/subosito/gotenv v1.4.2 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect diff --git a/go.sum b/go.sum index aa79c8997..a46e25154 100644 --- a/go.sum +++ b/go.sum @@ -206,6 +206,7 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3 github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= @@ -215,7 +216,9 @@ github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3 github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -319,6 +322,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= @@ -329,6 +333,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/classic-terra/cometbft v0.34.29-terra.0 h1:HnRGt7tijI2n5zSVrg/xh1mYYm4Gb4QFlknq+dRP8Jw= @@ -368,11 +373,14 @@ github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/ github.com/consensys/bavard v0.1.8-0.20210915155054-088da2f7f54a/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= github.com/consensys/gnark-crypto v0.5.3/go.mod h1:hOdPlWQV1gDLp7faZVeg8Y0iEPFaOUnCc4XeCCk96p0= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= @@ -405,7 +413,10 @@ github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6V github.com/creachadair/taskgroup v0.3.2/go.mod h1:wieWwecHVzsidg2CsUnFinW1faVN4+kq+TDlRJQ0Wbk= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= @@ -438,11 +449,18 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= +github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.19+incompatible h1:lzEmjivyNHFHMNAFLXORMBXyGIhw/UP4DvJwvyKYq64= +github.com/docker/docker v20.10.19+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -480,6 +498,7 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -524,6 +543,7 @@ github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVL github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -536,6 +556,7 @@ github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/gateway v1.1.0 h1:u0SuhL9+Il+UbjM9VIE3ntfRujKbvVpFvNB4HbjeVQ0= github.com/gogo/gateway v1.1.0/go.mod h1:S7rR8FRQyG3QFESeSv4l2WnsyzlCLG0CzBbUUo/mbic= @@ -640,6 +661,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -746,6 +769,8 @@ github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6 github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -894,6 +919,9 @@ github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -902,6 +930,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= @@ -941,7 +970,11 @@ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWEr github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= -github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -952,6 +985,8 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -1049,6 +1084,7 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0 github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= @@ -1059,7 +1095,9 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -1110,6 +1148,7 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= @@ -1149,9 +1188,17 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= @@ -1263,6 +1310,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1394,6 +1442,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1403,6 +1452,7 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1459,9 +1509,12 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1476,6 +1529,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1530,6 +1584,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1579,7 +1634,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1867,9 +1923,13 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 000000000..9349234e4 --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,131 @@ +# End-to-end Tests + +## Structure + +### `e2e` Package + +The `e2e` package defines an integration testing suite used for full +end-to-end testing functionality. This package is decoupled from +depending on the Terra codebase. It initializes the chains for testing +via Docker files. As a result, the test suite may provide the desired +Terra version to Docker containers during the initialization. + +The file e2e\_setup\_test.go defines the testing suite and contains the +core bootstrapping logic that creates a testing environment via Docker +containers. A testing network is created dynamically with 2 test +validators. + +The file `e2e_test.go` contains the actual end-to-end integration tests +that utilize the testing suite. + +Currently, there are 5 tests in `e2e_test.go`. + +Additionally, there is an ability to disable certain components +of the e2e suite. This can be done by setting the environment +variables. See "Environment variables" section below for more details. + +## How to run + +To run all e2e-tests in the package, run: + +```sh + make test-e2e +``` + +## `initialization` Package + +The `initialization` package introduces the logic necessary for initializing a +chain by creating a genesis file and all required configuration files +such as the `app.toml`. This package directly depends on the Terra +codebase. + +## `upgrade` Package + +The `upgrade` package starts chain initialization. In addition, there is +a Dockerfile `init.Dockerfile`. When executed, its container +produces all files necessary for starting up a new chain. These +resulting files can be mounted on a volume and propagated to our +production Terra container to start the `terrad` service. + +The decoupling between chain initialization and start-up allows to +minimize the differences between our test suite and the production +environment. + +## `containers` Package + +Introduces an abstraction necessary for creating and managing +Docker containers. Currently, validator containers are created +with a name of the corresponding validator struct that is initialized +in the `chain` package. + +## Running From Current Branch + +### To build chain initialization image + +```sh +make docker-build-e2e-init-chain +``` + +### To build the debug Terra image + +```sh + make docker-build-e2e-debug +``` + +### Environment variables + +Some tests take a long time to run. Sometimes, we would like to disable them +locally or in CI. The following are the environment variables to disable +certain components of e2e testing. + +- `TERRA_E2E_SKIP_IBC` - when true, skips the IBC tests tests. + +- `TERRA_E2E_SKIP_STATE_SYNC` - when true, skips the state sync tests. + +- `TERRA_E2E_SKIP_CLEANUP` - when true, avoids cleaning up the e2e Docker +containers. + +- `TERRA_E2E_FORK_HEIGHT` - when the above "IS_FORK" env variable is set to true, this is the string +of the height in which the network should fork. This should match the ForkHeight set in constants.go + +- `TERRA_E2E_UPGRADE_VERSION` - string of what version will be upgraded to (for example, "v10") + +- `TERRA_E2E_DEBUG_LOG` - when true, prints debug logs from executing CLI commands +via Docker containers. Set to trus in CI by default. + +#### VS Code Debug Configuration + +This debug configuration helps to run e2e tests locally and skip the desired tests. + +```json +{ + "name": "E2E IntegrationTestSuite", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/tests/e2e", + "args": [ + "-test.timeout", + "30m", + "-test.run", + "IntegrationTestSuite", + "-test.v" + ], + "buildFlags": "-tags e2e", + "env": { + "TERRA_E2E_SKIP_IBC": "true", + "TERRA_E2E_SKIP_CLEANUP": "true", + "TERRA_E2E_SKIP_STATE_SYNC": "true", + "TERRA_E2E_UPGRADE_VERSION": "v5", + "TERRA_E2E_DEBUG_LOG": "true", + "TERRA_E2E_FORK_HEIGHT": "" + } +} +``` + +### Common Problems + +Please note that if the tests are stopped mid-way, the e2e framework might fail to start again due to duplicated containers. Make sure that +containers are removed before running the tests again: `docker containers rm -f $(docker containers ls -a -q)`. + +Additionally, Docker networks do not get auto-removed. Therefore, you can manually remove them by running `docker network prune`. diff --git a/tests/e2e/configurer/base.go b/tests/e2e/configurer/base.go new file mode 100644 index 000000000..f91bcbb27 --- /dev/null +++ b/tests/e2e/configurer/base.go @@ -0,0 +1,199 @@ +package configurer + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/classic-terra/core/v2/tests/e2e/configurer/chain" + "github.com/classic-terra/core/v2/tests/e2e/containers" + "github.com/classic-terra/core/v2/tests/e2e/initialization" + "github.com/classic-terra/core/v2/tests/e2e/util" +) + +// baseConfigurer is the base implementation for the +// other 2 types of configurers. It is not meant to be used +// on its own. Instead, it is meant to be embedded +// by composition into more concrete configurers. +type baseConfigurer struct { + chainConfigs []*chain.Config + containerManager *containers.Manager + setupTests setupFn + syncUntilHeight int64 // the height until which to wait for validators to sync when first started. + t *testing.T +} + +// defaultSyncUntilHeight arbitrary small height to make sure the chain is making progress. +const defaultSyncUntilHeight = 3 + +func (bc *baseConfigurer) ClearResources() error { + bc.t.Log("tearing down e2e integration test suite...") + + if err := bc.containerManager.ClearResources(); err != nil { + return err + } + + for _, chainConfig := range bc.chainConfigs { + os.RemoveAll(chainConfig.DataDir) + } + return nil +} + +func (bc *baseConfigurer) GetChainConfig(chainIndex int) *chain.Config { + return bc.chainConfigs[chainIndex] +} + +func (bc *baseConfigurer) RunValidators() error { + for _, chainConfig := range bc.chainConfigs { + if err := bc.runValidators(chainConfig); err != nil { + return err + } + } + return nil +} + +func (bc *baseConfigurer) runValidators(chainConfig *chain.Config) error { + bc.t.Logf("starting %s validator containers...", chainConfig.ID) + for _, node := range chainConfig.NodeConfigs { + if err := node.Run(); err != nil { + return err + } + } + return nil +} + +func (bc *baseConfigurer) RunIBC() error { + bc.t.Log("Run relayer 1 between chain a and chain b") + if err := bc.runIBCRelayer(bc.chainConfigs[0], bc.chainConfigs[1], containers.HermesContainerName1); err != nil { + return err + } + bc.t.Log("Run relayer 2 between chain b and chain c") + if err := bc.runIBCRelayer(bc.chainConfigs[1], bc.chainConfigs[2], containers.HermesContainerName2); err != nil { + return err + } + + return nil +} + +func (bc *baseConfigurer) runIBCRelayer(chainConfigA *chain.Config, chainConfigB *chain.Config, hermesContainerName string) error { + bc.t.Log("starting Hermes relayer 1 container...") + + tmpDir, err := os.MkdirTemp("", "terra-e2e-testnet-hermes-1") + if err != nil { + return err + } + + hermesCfgPath := path.Join(tmpDir, "hermes") + + if err := os.MkdirAll(hermesCfgPath, 0o755); err != nil { + return err + } + + _, err = util.CopyFile( + filepath.Join("./scripts/", "hermes_bootstrap.sh"), + filepath.Join(hermesCfgPath, "hermes_bootstrap.sh"), + ) + if err != nil { + return err + } + + relayerNodeA := chainConfigA.NodeConfigs[0] + relayerNodeB := chainConfigB.NodeConfigs[0] + + err = util.WritePublicFile(filepath.Join(hermesCfgPath, "mnemonicA.json"), []byte(relayerNodeA.Mnemonic)) + if err != nil { + return err + } + + err = util.WritePublicFile(filepath.Join(hermesCfgPath, "mnemonicB.json"), []byte(relayerNodeB.Mnemonic)) + if err != nil { + return err + } + + hermesResource, err := bc.containerManager.RunHermesResource( + chainConfigA.ID, + relayerNodeA.Name, + filepath.Join("/root/hermes", "mnemonicA.json"), + chainConfigB.ID, + relayerNodeB.Name, + filepath.Join("/root/hermes", "mnemonicB.json"), + hermesContainerName, + hermesCfgPath) + if err != nil { + return err + } + + endpoint := fmt.Sprintf("http://%s/state", hermesResource.GetHostPort("3031/tcp")) + + require.Eventually(bc.t, func() bool { + resp, err := http.Get(endpoint) //nolint + if err != nil { + return false + } + + defer resp.Body.Close() + + bz, err := io.ReadAll(resp.Body) + if err != nil { + return false + } + + var respBody map[string]interface{} + if err := json.Unmarshal(bz, &respBody); err != nil { + return false + } + + status, ok := respBody["status"].(string) + require.True(bc.t, ok) + result, ok := respBody["result"].(map[string]interface{}) + require.True(bc.t, ok) + + chains, ok := result["chains"].([]interface{}) + require.True(bc.t, ok) + + return status == "success" && len(chains) == 2 + }, + initialization.OneMin, + time.Second, + "hermes relayer not healthy") + + bc.t.Logf("started Hermes relayer container: %s", hermesResource.Container.ID) + + // XXX: Give time to both networks to start, otherwise we might see gRPC + // transport errors. + time.Sleep(10 * time.Second) + + // create the client, connection and channel between the two Terra chains + return bc.connectIBCChains(chainConfigA, chainConfigB, hermesContainerName) +} + +func (bc *baseConfigurer) connectIBCChains(chainA *chain.Config, chainB *chain.Config, hermesContainerName string) error { + bc.t.Logf("connecting %s and %s chains via IBC", chainA.ChainMeta.ID, chainB.ChainMeta.ID) + + cmd := []string{"hermes", "create", "channel", "--a-chain", chainA.ChainMeta.ID, "--b-chain", chainB.ChainMeta.ID, "--a-port", "transfer", "--b-port", "transfer", "--new-client-connection", "--yes"} + bc.t.Log(cmd) + _, _, err := bc.containerManager.ExecHermesCmd(bc.t, cmd, hermesContainerName, "SUCCESS") + if err != nil { + return err + } + bc.t.Logf("connected %s and %s chains via IBC", chainA.ChainMeta.ID, chainB.ChainMeta.ID) + return nil +} + +func (bc *baseConfigurer) initializeChainConfigFromInitChain(initializedChain *initialization.Chain, chainConfig *chain.Config) { + chainConfig.ChainMeta = initializedChain.ChainMeta + chainConfig.NodeConfigs = make([]*chain.NodeConfig, 0, len(initializedChain.Nodes)) + setupTime := time.Now() + for i, validator := range initializedChain.Nodes { + conf := chain.NewNodeConfig(bc.t, validator, chainConfig.ValidatorInitConfigs[i], chainConfig.ID, bc.containerManager).WithSetupTime(setupTime) + chainConfig.NodeConfigs = append(chainConfig.NodeConfigs, conf) + } +} diff --git a/tests/e2e/configurer/chain/chain.go b/tests/e2e/configurer/chain/chain.go new file mode 100644 index 000000000..8667a808a --- /dev/null +++ b/tests/e2e/configurer/chain/chain.go @@ -0,0 +1,206 @@ +package chain + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + coretypes "github.com/tendermint/tendermint/rpc/core/types" + + "github.com/classic-terra/core/v2/tests/e2e/configurer/config" + "github.com/classic-terra/core/v2/tests/e2e/containers" + "github.com/classic-terra/core/v2/tests/e2e/initialization" + treasurytypes "github.com/classic-terra/core/v2/x/treasury/types" +) + +type Config struct { + initialization.ChainMeta + + ValidatorInitConfigs []*initialization.NodeConfig + // voting period is number of blocks it takes to deposit, 1.2 seconds per validator to vote on the prop, and a buffer. + VotingPeriod float32 + ExpeditedVotingPeriod float32 + // upgrade proposal height for chain. + UpgradePropHeight int64 + LatestProposalNumber int + LatestLockNumber int + NodeConfigs []*NodeConfig + + LatestCodeID int + + t *testing.T + containerManager *containers.Manager +} + +const ( + // defaultNodeIndex to use for querying and executing transactions. + // It is used when we are indifferent about the node we are working with. + defaultNodeIndex = 0 + // waitUntilRepeatPauseTime is the time to wait between each check of the node status. + waitUntilRepeatPauseTime = 2 * time.Second + // waitUntilrepeatMax is the maximum number of times to repeat the wait until condition. + waitUntilrepeatMax = 60 +) + +func New(t *testing.T, containerManager *containers.Manager, id string, initValidatorConfigs []*initialization.NodeConfig) *Config { + numVal := float32(len(initValidatorConfigs)) + return &Config{ + ChainMeta: initialization.ChainMeta{ + ID: id, + }, + ValidatorInitConfigs: initValidatorConfigs, + VotingPeriod: config.PropDepositBlocks + numVal*config.PropVoteBlocks + config.PropBufferBlocks, + ExpeditedVotingPeriod: config.PropDepositBlocks + numVal*config.PropVoteBlocks + config.PropBufferBlocks - 2, + t: t, + containerManager: containerManager, + } +} + +// CreateNode returns new initialized NodeConfig. +func (c *Config) CreateNode(initNode *initialization.Node) *NodeConfig { + nodeConfig := &NodeConfig{ + Node: *initNode, + chainID: c.ID, + containerManager: c.containerManager, + t: c.t, + } + c.NodeConfigs = append(c.NodeConfigs, nodeConfig) + return nodeConfig +} + +// RemoveNode removes node and stops it from running. +func (c *Config) RemoveNode(nodeName string) error { + for i, node := range c.NodeConfigs { + if node.Name == nodeName { + c.NodeConfigs = append(c.NodeConfigs[:i], c.NodeConfigs[i+1:]...) + return node.Stop() + } + } + return fmt.Errorf("node %s not found", nodeName) +} + +// WaitUntilHeight waits for all validators to reach the specified height at the minimum. +// returns error, if any. +func (c *Config) WaitUntilHeight(height int64) { + // Ensure the nodes are making progress. + doneCondition := func(syncInfo coretypes.SyncInfo) bool { + curHeight := syncInfo.LatestBlockHeight + + if curHeight < height { + c.t.Logf("current block height is %d, waiting to reach: %d", curHeight, height) + return false + } + + return !syncInfo.CatchingUp + } + + for _, node := range c.NodeConfigs { + c.t.Logf("node container: %s, waiting to reach height %d", node.Name, height) + node.WaitUntil(doneCondition) + } +} + +// WaitForNumHeights waits for all nodes to go through a given number of heights. +func (c *Config) WaitForNumHeights(heightsToWait int64) { + node, err := c.GetDefaultNode() + require.NoError(c.t, err) + currentHeight, err := node.QueryCurrentHeight() + require.NoError(c.t, err) + c.WaitUntilHeight(currentHeight + heightsToWait) +} + +func (c *Config) SendIBC(dstChain *Config, recipient string, token sdk.Coin, hermesContainerName string) { + c.t.Logf("IBC sending %s from %s to %s (%s)", token, c.ID, dstChain.ID, recipient) + + dstNode, err := dstChain.GetDefaultNode() + require.NoError(c.t, err) + + balancesDstPre, err := dstNode.QueryBalances(recipient) + require.NoError(c.t, err) + + cmd := []string{"hermes", "tx", "raw", "ft-transfer", dstChain.ID, c.ID, "transfer", "channel-0", token.Amount.String(), fmt.Sprintf("--denom=%s", token.Denom), fmt.Sprintf("--receiver=%s", recipient), "--timeout-height-offset=1000"} + _, _, err = c.containerManager.ExecHermesCmd(c.t, cmd, hermesContainerName, "Success") + require.NoError(c.t, err) + + require.Eventually( + c.t, + func() bool { + balancesDstPost, err := dstNode.QueryBalances(recipient) + require.NoError(c.t, err) + ibcCoin := balancesDstPost.Sub(balancesDstPre...) + if ibcCoin.Len() == 1 { + tokenPre := balancesDstPre.AmountOfNoDenomValidation(ibcCoin[0].Denom) + tokenPost := balancesDstPost.AmountOfNoDenomValidation(ibcCoin[0].Denom) + resPre := token.Amount + resPost := tokenPost.Sub(tokenPre) + return resPost.Uint64() == resPre.Uint64() + } + return false + }, + initialization.FiveMin, + time.Second, + "tx not received on destination chain", + ) + + c.t.Log("successfully sent IBC tokens") +} + +func (c *Config) GetDefaultNode() (*NodeConfig, error) { + return c.getNodeAtIndex(defaultNodeIndex) +} + +// GetPersistentPeers returns persistent peers from every node +// associated with a chain. +func (c *Config) GetPersistentPeers() []string { + peers := make([]string, len(c.NodeConfigs)) + for i, node := range c.NodeConfigs { + peers[i] = node.PeerID + } + return peers +} + +func (c *Config) getNodeAtIndex(nodeIndex int) (*NodeConfig, error) { + if nodeIndex > len(c.NodeConfigs) { + return nil, fmt.Errorf("node index (%d) is greter than the number of nodes available (%d)", nodeIndex, len(c.NodeConfigs)) + } + return c.NodeConfigs[nodeIndex], nil +} + +func (c *Config) AddBurnTaxExemptionAddressProposal(chainANode *NodeConfig, addresses ...string) { + proposal := treasurytypes.AddBurnTaxExemptionAddressProposal{ + Title: "Add Burn Tax Exemption Address", + Description: fmt.Sprintf("Add %s to the burn tax exemption address list", strings.Join(addresses, ",")), + Addresses: addresses, + } + proposalJSON, err := json.Marshal(proposal) + require.NoError(c.t, err) + + wd, err := os.Getwd() + require.NoError(c.t, err) + localProposalFile := wd + "/scripts/add_burn_tax_exemption_address_proposal.json" + f, err := os.Create(localProposalFile) + require.NoError(c.t, err) + _, err = f.WriteString(string(proposalJSON)) + require.NoError(c.t, err) + err = f.Close() + require.NoError(c.t, err) + + propNumber := chainANode.SubmitAddBurnTaxExemptionAddressProposal(addresses, initialization.ValidatorWalletName) + + chainANode.DepositProposal(propNumber) + AllValsVoteOnProposal(c, propNumber) + + time.Sleep(initialization.TwoMin) + require.Eventually(c.t, func() bool { + status, err := chainANode.QueryPropStatus(propNumber) + if err != nil { + return false + } + return status == "PROPOSAL_STATUS_PASSED" + }, initialization.OneMin, 10*time.Millisecond) +} diff --git a/tests/e2e/configurer/chain/commands.go b/tests/e2e/configurer/chain/commands.go new file mode 100644 index 000000000..d0a45bd88 --- /dev/null +++ b/tests/e2e/configurer/chain/commands.go @@ -0,0 +1,313 @@ +package chain + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "os" + "regexp" + "strconv" + "strings" + + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/libs/bytes" + "github.com/tendermint/tendermint/p2p" + coretypes "github.com/tendermint/tendermint/rpc/core/types" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + app "github.com/classic-terra/core/v2/app" + "github.com/classic-terra/core/v2/tests/e2e/initialization" + "github.com/classic-terra/core/v2/types/assets" +) + +func (n *NodeConfig) StoreWasmCode(wasmFile, from string) { + n.LogActionF("storing wasm code from file %s", wasmFile) + cmd := []string{"terrad", "tx", "wasm", "store", wasmFile, fmt.Sprintf("--from=%s", from), "--gas=auto", "--gas-prices=0uluna", "--gas-adjustment=1.3"} + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully stored") +} + +func (n *NodeConfig) InstantiateWasmContract(codeID, initMsg, amount, from string) { + n.LogActionF("instantiating wasm contract %s with %s", codeID, initMsg) + cmd := []string{"terrad", "tx", "wasm", "instantiate", codeID, initMsg, fmt.Sprintf("--from=%s", from), "--no-admin", "--label=ratelimit", "--gas=auto", "--gas-prices=0.0uluna", "--gas-adjustment=1.3"} + if amount != "" { + cmd = append(cmd, fmt.Sprintf("--amount=%s", amount)) + } + n.LogActionF(strings.Join(cmd, " ")) + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + + require.NoError(n.t, err) + + n.LogActionF("successfully initialized") +} + +func (n *NodeConfig) Instantiate2WasmContract(codeID, initMsg, salt, amount, fee, from string) { + n.LogActionF("instantiating wasm contract %s with %s", codeID, initMsg) + encodedSalt := make([]byte, hex.EncodedLen(len([]byte(salt)))) + hex.Encode(encodedSalt, []byte(salt)) + cmd := []string{"terrad", "tx", "wasm", "instantiate2", codeID, initMsg, string(encodedSalt), fmt.Sprintf("--from=%s", from), "--no-admin", "--label=ratelimit", "--gas=auto", "--gas-prices=0.0uluna", "--gas-adjustment=1.3"} + if amount != "" { + cmd = append(cmd, fmt.Sprintf("--amount=%s", amount)) + } + if fee != "" { + cmd = append(cmd, fmt.Sprintf("--fees=%s", fee)) + } + n.LogActionF(strings.Join(cmd, " ")) + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully initialized") +} + +func (n *NodeConfig) WasmExecute(contract, execMsg, amount, fee, from string) { + n.LogActionF("executing %s on wasm contract %s from %s", execMsg, contract, from) + cmd := []string{"terrad", "tx", "wasm", "execute", contract, execMsg, fmt.Sprintf("--from=%s", from), "--gas=auto", "--gas-prices=0.0uluna", "--gas-adjustment=1.3"} + if amount != "" { + cmd = append(cmd, fmt.Sprintf("--amount=%s", amount)) + } + if fee != "" { + cmd = append(cmd, fmt.Sprintf("--fees=%s", fee)) + } + n.LogActionF(strings.Join(cmd, " ")) + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully executed") +} + +// QueryParams extracts the params for a given subspace and key. This is done generically via json to avoid having to +// specify the QueryParamResponse type (which may not exist for all params). +func (n *NodeConfig) QueryParams(subspace, key string, result any) { + cmd := []string{"terrad", "query", "params", "subspace", subspace, key, "--output=json"} + + out, _, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "") + require.NoError(n.t, err) + + err = json.Unmarshal(out.Bytes(), &result) + require.NoError(n.t, err) +} + +func (n *NodeConfig) SubmitParamChangeProposal(proposalJSON, from string) { + n.LogActionF("submitting param change proposal %s", proposalJSON) + // ToDo: Is there a better way to do this? + wd, err := os.Getwd() + require.NoError(n.t, err) + localProposalFile := wd + "/scripts/param_change_proposal.json" + f, err := os.Create(localProposalFile) + require.NoError(n.t, err) + _, err = f.WriteString(proposalJSON) + require.NoError(n.t, err) + err = f.Close() + require.NoError(n.t, err) + + cmd := []string{"terrad", "tx", "gov", "submit-proposal", "/terra/param_change_proposal.json", fmt.Sprintf("--from=%s", from)} + + _, _, err = n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + + err = os.Remove(localProposalFile) + require.NoError(n.t, err) + + n.LogActionF("successfully submitted param change proposal") +} + +func (n *NodeConfig) SubmitAddBurnTaxExemptionAddressProposal(addresses []string, walletName string) int { + n.LogActionF("submitting add burn tax exemption address proposal %s", addresses) + + cmd := []string{ + "terrad", "tx", "gov", "submit-legacy-proposal", + "add-burn-tax-exemption-address", strings.Join(addresses, ","), + "--title=\"burn tax exemption address\"", + "--description=\"\"burn tax exemption address", + fmt.Sprintf("--from=%s", walletName), + } + + resp, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + + proposalID, err := extractProposalIDFromResponse(resp.String()) + require.NoError(n.t, err) + + n.LogActionF("successfully submitted add burn tax exemption address proposal") + return proposalID +} + +func (n *NodeConfig) FailIBCTransfer(from, recipient, amount string) { + n.LogActionF("IBC sending %s from %s to %s", amount, from, recipient) + + cmd := []string{"terrad", "tx", "ibc-transfer", "transfer", "transfer", "channel-0", recipient, amount, fmt.Sprintf("--from=%s", from)} + + _, _, err := n.containerManager.ExecTxCmdWithSuccessString(n.t, n.chainID, n.Name, cmd, "rate limit exceeded") + require.NoError(n.t, err) + + n.LogActionF("Failed to send IBC transfer (as expected)") +} + +func (n *NodeConfig) SendIBCTransfer(from, recipient, amount, memo string) { + n.LogActionF("IBC sending %s from %s to %s. memo: %s", amount, from, recipient, memo) + + cmd := []string{"terrad", "tx", "ibc-transfer", "transfer", "transfer", "channel-0", recipient, amount, fmt.Sprintf("--from=%s", from), "--memo", memo} + + _, _, err := n.containerManager.ExecTxCmdWithSuccessString(n.t, n.chainID, n.Name, cmd, "code: 0") + require.NoError(n.t, err) + + n.LogActionF("successfully submitted sent IBC transfer") +} + +func (n *NodeConfig) SubmitTextProposal(text string, initialDeposit sdk.Coin) { + n.LogActionF("submitting text gov proposal") + cmd := []string{"terrad", "tx", "gov", "submit-proposal", "--type=text", fmt.Sprintf("--title=\"%s\"", text), "--description=\"test text proposal\"", "--from=val", fmt.Sprintf("--deposit=%s", initialDeposit)} + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully submitted text gov proposal") +} + +func (n *NodeConfig) DepositProposal(proposalNumber int) { + n.LogActionF("depositing on proposal: %d", proposalNumber) + deposit := sdk.NewCoin(initialization.TerraDenom, sdk.NewInt(20*assets.MicroUnit)).String() + cmd := []string{"terrad", "tx", "gov", "deposit", fmt.Sprintf("%d", proposalNumber), deposit, "--from=val"} + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully deposited on proposal %d", proposalNumber) +} + +func (n *NodeConfig) VoteYesProposal(from string, proposalNumber int) { + n.LogActionF("voting yes on proposal: %d", proposalNumber) + cmd := []string{"terrad", "tx", "gov", "vote", fmt.Sprintf("%d", proposalNumber), "yes", fmt.Sprintf("--from=%s", from)} + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully voted yes on proposal %d", proposalNumber) +} + +func (n *NodeConfig) VoteNoProposal(from string, proposalNumber int) { + n.LogActionF("voting no on proposal: %d", proposalNumber) + cmd := []string{"terrad", "tx", "gov", "vote", fmt.Sprintf("%d", proposalNumber), "no", fmt.Sprintf("--from=%s", from)} + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully voted no on proposal: %d", proposalNumber) +} + +func AllValsVoteOnProposal(chain *Config, propNumber int) { + for _, n := range chain.NodeConfigs { + n.VoteYesProposal(initialization.ValidatorWalletName, propNumber) + } +} + +func extractProposalIDFromResponse(response string) (int, error) { + // Extract the proposal ID from the response + startIndex := strings.Index(response, `[{"key":"proposal_id","value":"`) + len(`[{"key":"proposal_id","value":"`) + endIndex := strings.Index(response[startIndex:], `"`) + + // Extract the proposal ID substring + proposalIDStr := response[startIndex : startIndex+endIndex] + + // Convert the proposal ID from string to int + proposalID, err := strconv.Atoi(proposalIDStr) + if err != nil { + return 0, err + } + + return proposalID, nil +} + +func (n *NodeConfig) BankSend(amount string, sendAddress string, receiveAddress string) { + n.BankSendWithWallet(amount, sendAddress, receiveAddress, "val") +} + +func (n *NodeConfig) BankSendWithWallet(amount string, sendAddress string, receiveAddress string, walletName string) { + n.LogActionF("bank sending %s from address %s to %s", amount, sendAddress, receiveAddress) + cmd := []string{"terrad", "tx", "bank", "send", sendAddress, receiveAddress, amount, fmt.Sprintf("--from=%s", walletName)} + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully sent bank sent %s from address %s to %s", amount, sendAddress, receiveAddress) +} + +func (n *NodeConfig) BankSendFeeGrantWithWallet(amount string, sendAddress string, receiveAddress string, feeGranter string, walletName string) { + n.LogActionF("bank sending %s from address %s to %s", amount, sendAddress, receiveAddress) + cmd := []string{"terrad", "tx", "bank", "send", sendAddress, receiveAddress, amount, fmt.Sprintf("--fee-granter=%s", feeGranter), fmt.Sprintf("--from=%s", walletName)} + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + + n.LogActionF("successfully sent bank sent %s from address %s to %s", amount, sendAddress, receiveAddress) +} + +func (n *NodeConfig) BankMultiSend(amount string, split bool, sendAddress string, receiveAddresses ...string) { + n.LogActionF("bank multisending from %s to %s", sendAddress, strings.Join(receiveAddresses, ",")) + cmd := []string{"terrad", "tx", "bank", "multi-send", sendAddress} + cmd = append(cmd, receiveAddresses...) + cmd = append(cmd, amount, "--from=val") + if split { + cmd = append(cmd, "--split") + } + + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully multisent %s to %s", sendAddress, strings.Join(receiveAddresses, ",")) +} + +func (n *NodeConfig) GrantAddress(granter, gratee string, spendLimit string, walletName string) { + n.LogActionF("granting for address %s", gratee) + cmd := []string{"terrad", "tx", "feegrant", "grant", granter, gratee, fmt.Sprintf("--from=%s", walletName), fmt.Sprintf("--spend-limit=%s", spendLimit)} + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully granted for address %s", gratee) +} + +func (n *NodeConfig) CreateWallet(walletName string) string { + n.LogActionF("creating wallet %s", walletName) + cmd := []string{"terrad", "keys", "add", walletName, "--keyring-backend=test"} + outBuf, _, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "") + require.NoError(n.t, err) + re := regexp.MustCompile("terra1(.{38})") + walletAddr := fmt.Sprintf("%s\n", re.FindString(outBuf.String())) + walletAddr = strings.TrimSuffix(walletAddr, "\n") + n.LogActionF("created wallet %s, waller address - %s", walletName, walletAddr) + return walletAddr +} + +func (n *NodeConfig) GetWallet(walletName string) string { + n.LogActionF("retrieving wallet %s", walletName) + cmd := []string{"terrad", "keys", "show", walletName, "--keyring-backend=test"} + outBuf, _, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "") + require.NoError(n.t, err) + re := regexp.MustCompile("terra1(.{38})") + walletAddr := fmt.Sprintf("%s\n", re.FindString(outBuf.String())) + walletAddr = strings.TrimSuffix(walletAddr, "\n") + n.LogActionF("wallet %s found, waller address - %s", walletName, walletAddr) + return walletAddr +} + +type validatorInfo struct { + Address bytes.HexBytes + PubKey cryptotypes.PubKey + VotingPower int64 +} + +// ResultStatus is node's info, same as Tendermint, except that we use our own +// PubKey. +type resultStatus struct { + NodeInfo p2p.DefaultNodeInfo + SyncInfo coretypes.SyncInfo + ValidatorInfo validatorInfo +} + +func (n *NodeConfig) Status() (resultStatus, error) { //nolint + cmd := []string{"terrad", "status"} + _, errBuf, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "") + if err != nil { + return resultStatus{}, err + } + + cfg := app.MakeEncodingConfig() + legacyAmino := cfg.Amino + var result resultStatus + err = legacyAmino.UnmarshalJSON(errBuf.Bytes(), &result) + fmt.Println("result", result) + + if err != nil { + return resultStatus{}, err + } + return result, nil +} diff --git a/tests/e2e/configurer/chain/node.go b/tests/e2e/configurer/chain/node.go new file mode 100644 index 000000000..85336f230 --- /dev/null +++ b/tests/e2e/configurer/chain/node.go @@ -0,0 +1,145 @@ +package chain + +import ( + "context" + "fmt" + "regexp" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + rpchttp "github.com/tendermint/tendermint/rpc/client/http" + coretypes "github.com/tendermint/tendermint/rpc/core/types" + + "github.com/classic-terra/core/v2/tests/e2e/containers" + "github.com/classic-terra/core/v2/tests/e2e/initialization" +) + +type NodeConfig struct { + initialization.Node + + OperatorAddress string + SnapshotInterval uint64 + chainID string + rpcClient *rpchttp.HTTP + t *testing.T + containerManager *containers.Manager + + // Add this to help with logging / tracking time since start. + setupTime time.Time +} + +// NewNodeConfig returens new initialized NodeConfig. +func NewNodeConfig(t *testing.T, initNode *initialization.Node, initConfig *initialization.NodeConfig, chainID string, containerManager *containers.Manager) *NodeConfig { + return &NodeConfig{ + Node: *initNode, + SnapshotInterval: initConfig.SnapshotInterval, + chainID: chainID, + containerManager: containerManager, + t: t, + setupTime: time.Now(), + } +} + +// Run runs a node container for the given nodeIndex. +// The node configuration must be already added to the chain config prior to calling this +// method. +func (n *NodeConfig) Run() error { + n.t.Logf("starting node container: %s", n.Name) + resource, err := n.containerManager.RunNodeResource(n.Name, n.ConfigDir) + if err != nil { + return err + } + + hostPort := resource.GetHostPort("26657/tcp") + rpcClient, err := rpchttp.New("tcp://"+hostPort, "/websocket") + if err != nil { + return err + } + + n.rpcClient = rpcClient + + require.Eventually( + n.t, + func() bool { + // This fails if unsuccessful. + _, err := n.QueryCurrentHeight() + if err != nil { + return false + } + n.t.Logf("started node container: %s", n.Name) + return true + }, + initialization.TwoMin, + time.Second, + "Terra node failed to produce blocks", + ) + + if err := n.extractOperatorAddressIfValidator(); err != nil { + return err + } + + return nil +} + +// Stop stops the node from running and removes its container. +func (n *NodeConfig) Stop() error { + n.t.Logf("stopping node container: %s", n.Name) + if err := n.containerManager.RemoveNodeResource(n.Name); err != nil { + return err + } + n.t.Logf("stopped node container: %s", n.Name) + return nil +} + +// WaitUntil waits until node reaches doneCondition. Return nil +// if reached, error otherwise. +func (n *NodeConfig) WaitUntil(doneCondition func(syncInfo coretypes.SyncInfo) bool) { + var latestBlockHeight int64 + for i := 0; i < waitUntilrepeatMax; i++ { + status, err := n.rpcClient.Status(context.Background()) + require.NoError(n.t, err) + latestBlockHeight = status.SyncInfo.LatestBlockHeight + // let the node produce a few blocks + if !doneCondition(status.SyncInfo) { + time.Sleep(waitUntilRepeatPauseTime) + continue + } + return + } + n.t.Errorf("node %s timed out waiting for condition, latest block height was %d", n.Name, latestBlockHeight) +} + +func (n *NodeConfig) extractOperatorAddressIfValidator() error { + if !n.IsValidator { + n.t.Logf("node (%s) is not a validator, skipping", n.Name) + return nil + } + + cmd := []string{"terrad", "debug", "addr", n.PublicKey} + n.t.Logf("extracting validator operator addresses for validator: %s", n.Name) + _, errBuf, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "") + if err != nil { + return err + } + re := regexp.MustCompile("terravaloper(.{39})") + operAddr := fmt.Sprintf("%s\n", re.FindString(errBuf.String())) + n.OperatorAddress = strings.TrimSuffix(operAddr, "\n") + return nil +} + +func (n *NodeConfig) GetHostPort(portID string) (string, error) { + return n.containerManager.GetHostPort(n.Name, portID) +} + +func (n *NodeConfig) WithSetupTime(t time.Time) *NodeConfig { + n.setupTime = t + return n +} + +func (n *NodeConfig) LogActionF(msg string, args ...interface{}) { + timeSinceStart := time.Since(n.setupTime).Round(time.Millisecond) + s := fmt.Sprintf(msg, args...) + n.t.Logf("[%s] %s. From container %s", timeSinceStart, s, n.Name) +} diff --git a/tests/e2e/configurer/chain/queries.go b/tests/e2e/configurer/chain/queries.go new file mode 100644 index 000000000..e9f6619a6 --- /dev/null +++ b/tests/e2e/configurer/chain/queries.go @@ -0,0 +1,242 @@ +package chain + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/stretchr/testify/require" + tmabcitypes "github.com/tendermint/tendermint/abci/types" + + "github.com/classic-terra/core/v2/tests/e2e/initialization" + "github.com/classic-terra/core/v2/tests/e2e/util" + + treasurytypes "github.com/classic-terra/core/v2/x/treasury/types" +) + +func (n *NodeConfig) QueryGRPCGateway(path string, parameters ...string) ([]byte, error) { + if len(parameters)%2 != 0 { + return nil, fmt.Errorf("invalid number of parameters, must follow the format of key + value") + } + + // add the URL for the given validator ID, and pre-pend to to path. + hostPort, err := n.containerManager.GetHostPort(n.Name, "1317/tcp") + require.NoError(n.t, err) + endpoint := fmt.Sprintf("http://%s", hostPort) + fullQueryPath := fmt.Sprintf("%s/%s", endpoint, path) + + var resp *http.Response + require.Eventually(n.t, func() bool { + req, err := http.NewRequest("GET", fullQueryPath, nil) + if err != nil { + return false + } + + if len(parameters) > 0 { + q := req.URL.Query() + for i := 0; i < len(parameters); i += 2 { + q.Add(parameters[i], parameters[i+1]) + } + req.URL.RawQuery = q.Encode() + } + + resp, err = http.DefaultClient.Do(req) + if err != nil { + n.t.Logf("error while executing HTTP request: %s", err.Error()) + return false + } + + return resp.StatusCode != http.StatusServiceUnavailable + }, initialization.OneMin, time.Millisecond*10, "failed to execute HTTP request") + + defer resp.Body.Close() + + bz, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(bz)) + } + return bz, nil +} + +// QueryBalancer returns balances at the address. +func (n *NodeConfig) QueryBalances(address string) (sdk.Coins, error) { + path := fmt.Sprintf("cosmos/bank/v1beta1/balances/%s", address) + bz, err := n.QueryGRPCGateway(path) + require.NoError(n.t, err) + + var balancesResp banktypes.QueryAllBalancesResponse + if err := util.Cdc.UnmarshalJSON(bz, &balancesResp); err != nil { + return sdk.Coins{}, err + } + return balancesResp.GetBalances(), nil +} + +// if coin is zero, return empty coin. +func (n *NodeConfig) QuerySpecificBalance(addr, denom string) (sdk.Coin, error) { + balances, err := n.QueryBalances(addr) + if err != nil { + return sdk.Coin{}, err + } + for _, c := range balances { + if c.Denom == denom { + return c, nil + } + } + return sdk.Coin{}, nil +} + +func (n *NodeConfig) QuerySupplyOf(denom string) (sdk.Int, error) { + path := fmt.Sprintf("cosmos/bank/v1beta1/supply/%s", denom) + bz, err := n.QueryGRPCGateway(path) + require.NoError(n.t, err) + + var supplyResp banktypes.QuerySupplyOfResponse + if err := util.Cdc.UnmarshalJSON(bz, &supplyResp); err != nil { + return sdk.NewInt(0), err + } + return supplyResp.Amount.Amount, nil +} + +func (n *NodeConfig) QueryTaxRate() (sdk.Dec, error) { + path := "terra/treasury/v1beta1/tax_rate" + bz, err := n.QueryGRPCGateway(path) + require.NoError(n.t, err) + + var taxRateResp treasurytypes.QueryTaxRateResponse + if err := util.Cdc.UnmarshalJSON(bz, &taxRateResp); err != nil { + return sdk.ZeroDec(), err + } + return taxRateResp.TaxRate, nil +} + +func (n *NodeConfig) QueryBurnTaxExemptionList() ([]string, error) { + path := "terra/treasury/v1beta1/burn_tax_exemption_list" + bz, err := n.QueryGRPCGateway(path) + require.NoError(n.t, err) + + var taxRateResp treasurytypes.QueryBurnTaxExemptionListResponse + if err := util.Cdc.UnmarshalJSON(bz, &taxRateResp); err != nil { + return nil, err + } + + return taxRateResp.Addresses, nil +} + +func (n *NodeConfig) QueryContractsFromID(codeID int) ([]string, error) { + path := fmt.Sprintf("/cosmwasm/wasm/v1/code/%d/contracts", codeID) + bz, err := n.QueryGRPCGateway(path) + + require.NoError(n.t, err) + + var contractsResponse wasmtypes.QueryContractsByCodeResponse + if err := util.Cdc.UnmarshalJSON(bz, &contractsResponse); err != nil { + return nil, err + } + + return contractsResponse.Contracts, nil +} + +func (n *NodeConfig) QueryLatestWasmCodeID() uint64 { + path := "/cosmwasm/wasm/v1/code" + + bz, err := n.QueryGRPCGateway(path) + require.NoError(n.t, err) + + var response wasmtypes.QueryCodesResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + require.NoError(n.t, err) + if len(response.CodeInfos) == 0 { + return 0 + } + return response.CodeInfos[len(response.CodeInfos)-1].CodeID +} + +func (n *NodeConfig) QueryWasmSmart(contract string, msg string) (interface{}, error) { + // base64-encode the msg + encodedMsg := base64.StdEncoding.EncodeToString([]byte(msg)) + path := fmt.Sprintf("/cosmwasm/wasm/v1/contract/%s/smart/%s", contract, encodedMsg) + + bz, err := n.QueryGRPCGateway(path) + if err != nil { + return nil, err + } + + var response wasmtypes.QuerySmartContractStateResponse + err = util.Cdc.UnmarshalJSON(bz, &response) + if err != nil { + return nil, err + } + + var responseJSON interface{} + err = json.Unmarshal(response.Data, &responseJSON) + if err != nil { + return nil, err + } + return responseJSON, nil +} + +func (n *NodeConfig) QueryPropStatus(proposalNumber int) (string, error) { + path := fmt.Sprintf("cosmos/gov/v1beta1/proposals/%d", proposalNumber) + bz, err := n.QueryGRPCGateway(path) + require.NoError(n.t, err) + + var resp map[string]interface{} + err = json.Unmarshal(bz, &resp) + require.NoError(n.t, err) + + status := resp["proposal"].(map[string]interface{})["status"].(string) + + return status, nil +} + +// QueryHashFromBlock gets block hash at a specific height. Otherwise, error. +func (n *NodeConfig) QueryHashFromBlock(height int64) (string, error) { + block, err := n.rpcClient.Block(context.Background(), &height) + if err != nil { + return "", err + } + return block.BlockID.Hash.String(), nil +} + +// QueryCurrentHeight returns the current block height of the node or error. +func (n *NodeConfig) QueryCurrentHeight() (int64, error) { + status, err := n.rpcClient.Status(context.Background()) + if err != nil { + return 0, err + } + return status.SyncInfo.LatestBlockHeight, nil +} + +// QueryLatestBlockTime returns the latest block time. +func (n *NodeConfig) QueryLatestBlockTime() time.Time { + status, err := n.rpcClient.Status(context.Background()) + require.NoError(n.t, err) + return status.SyncInfo.LatestBlockTime +} + +// QueryListSnapshots gets all snapshots currently created for a node. +func (n *NodeConfig) QueryListSnapshots() ([]*tmabcitypes.Snapshot, error) { + abciResponse, err := n.rpcClient.ABCIQuery(context.Background(), "/app/snapshots", nil) + if err != nil { + return nil, err + } + + var listSnapshots tmabcitypes.ResponseListSnapshots + if err := json.Unmarshal(abciResponse.Response.Value, &listSnapshots); err != nil { + return nil, err + } + + return listSnapshots.Snapshots, nil +} diff --git a/tests/e2e/configurer/config/constants.go b/tests/e2e/configurer/config/constants.go new file mode 100644 index 000000000..783b59710 --- /dev/null +++ b/tests/e2e/configurer/config/constants.go @@ -0,0 +1,25 @@ +package config + +import govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + +const ( + // if not skipping upgrade, how many blocks we allow for fork to run pre upgrade state creation + ForkHeightPreUpgradeOffset int64 = 60 + // estimated number of blocks it takes to submit for a proposal + PropSubmitBlocks float32 = 10 + // estimated number of blocks it takes to deposit for a proposal + PropDepositBlocks float32 = 10 + // number of blocks it takes to vote for a single validator to vote for a proposal + PropVoteBlocks float32 = 1.2 + // number of blocks used as a calculation buffer + PropBufferBlocks float32 = 6 + // max retries for json unmarshalling + MaxRetries = 60 +) + +var ( + // Minimum deposit value for a proposal to enter a voting period. + MinDepositValue = govv1.DefaultMinDepositTokens.Int64() + // Minimum deposit value for proposal to be submitted. + InitialMinDeposit = MinDepositValue / 4 +) diff --git a/tests/e2e/configurer/current.go b/tests/e2e/configurer/current.go new file mode 100644 index 000000000..94a2c0a9e --- /dev/null +++ b/tests/e2e/configurer/current.go @@ -0,0 +1,57 @@ +package configurer + +import ( + "os" + "testing" + + "github.com/classic-terra/core/v2/tests/e2e/configurer/chain" + "github.com/classic-terra/core/v2/tests/e2e/containers" + "github.com/classic-terra/core/v2/tests/e2e/initialization" +) + +type CurrentBranchConfigurer struct { + baseConfigurer +} + +var _ Configurer = (*CurrentBranchConfigurer)(nil) + +func NewCurrentBranchConfigurer(t *testing.T, chainConfigs []*chain.Config, setupTests setupFn, containerManager *containers.Manager) Configurer { + return &CurrentBranchConfigurer{ + baseConfigurer: baseConfigurer{ + chainConfigs: chainConfigs, + containerManager: containerManager, + setupTests: setupTests, + syncUntilHeight: defaultSyncUntilHeight, + t: t, + }, + } +} + +func (cb *CurrentBranchConfigurer) ConfigureChains() error { + for _, chainConfig := range cb.chainConfigs { + if err := cb.ConfigureChain(chainConfig); err != nil { + return err + } + } + return nil +} + +func (cb *CurrentBranchConfigurer) ConfigureChain(chainConfig *chain.Config) error { + cb.t.Logf("starting e2e infrastructure from current branch for chain-id: %s", chainConfig.ID) + tmpDir, err := os.MkdirTemp("", "terra-e2e-testnet-") + if err != nil { + return err + } + cb.t.Logf("temp directory for chain-id %v: %v", chainConfig.ID, tmpDir) + + initializedChain, err := initialization.InitChain(chainConfig.ID, tmpDir, chainConfig.ValidatorInitConfigs, 0) + if err != nil { + return err + } + cb.initializeChainConfigFromInitChain(initializedChain, chainConfig) + return nil +} + +func (cb *CurrentBranchConfigurer) RunSetup() error { + return cb.setupTests(cb) +} diff --git a/tests/e2e/configurer/factory.go b/tests/e2e/configurer/factory.go new file mode 100644 index 000000000..d64efde4f --- /dev/null +++ b/tests/e2e/configurer/factory.go @@ -0,0 +1,157 @@ +package configurer + +import ( + "testing" + + "github.com/classic-terra/core/v2/tests/e2e/configurer/chain" + "github.com/classic-terra/core/v2/tests/e2e/containers" + "github.com/classic-terra/core/v2/tests/e2e/initialization" +) + +type Configurer interface { + ConfigureChains() error + + ClearResources() error + + GetChainConfig(chainIndex int) *chain.Config + + RunSetup() error + + RunValidators() error + + RunIBC() error +} + +var ( + // each started validator containers corresponds to one of + // the configurations below. + validatorConfigsChainA = []*initialization.NodeConfig{ + { + // this is a node that is used to state-sync from so its snapshot-interval + // is frequent. + Name: "prune-default-snapshot-state-sync-from", + Pruning: "default", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 25, + SnapshotKeepRecent: 10, + IsValidator: true, + }, + { + Name: "prune-nothing-snapshot", + Pruning: "nothing", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, + }, + { + Name: "prune-custom-10000-13-snapshot", + Pruning: "custom", + PruningKeepRecent: "10000", + PruningInterval: "13", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, + }, + { + Name: "prune-everything-no-snapshot", + Pruning: "everything", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 0, + SnapshotKeepRecent: 0, + IsValidator: true, + }, + } + validatorConfigsChainB = []*initialization.NodeConfig{ + { + Name: "prune-default-snapshot", + Pruning: "default", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, + }, + { + Name: "prune-nothing-snapshot", + Pruning: "nothing", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, + }, + { + Name: "prune-custom-snapshot", + Pruning: "custom", + PruningKeepRecent: "10000", + PruningInterval: "13", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, + }, + } + validatorConfigsChainC = []*initialization.NodeConfig{ + { + Name: "prune-default-snapshot", + Pruning: "default", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, + }, + { + Name: "prune-nothing-snapshot", + Pruning: "nothing", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, + }, + { + Name: "prune-custom-snapshot", + Pruning: "custom", + PruningKeepRecent: "10000", + PruningInterval: "13", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, + }, + } +) + +// New returns a new Configurer depending on the values of its parameters. +// - If only isIBCEnabled, we want to have 2 chains initialized at the current +// Git branch version of Terra codebase. +func New(t *testing.T, isIBCEnabled, isDebugLogEnabled bool) (Configurer, error) { + containerManager, err := containers.NewManager(isDebugLogEnabled) + if err != nil { + return nil, err + } + if isIBCEnabled { + // configure two chains from current Git branch + return NewCurrentBranchConfigurer(t, + []*chain.Config{ + chain.New(t, containerManager, initialization.ChainAID, validatorConfigsChainA), + chain.New(t, containerManager, initialization.ChainBID, validatorConfigsChainB), + chain.New(t, containerManager, initialization.ChainCID, validatorConfigsChainC), + }, + withIBC(baseSetup), // base set up with IBC + containerManager, + ), nil + } + + // configure one chain from current Git branch + return NewCurrentBranchConfigurer(t, + []*chain.Config{ + chain.New(t, containerManager, initialization.ChainAID, validatorConfigsChainA), + }, + baseSetup, // base set up only + containerManager, + ), nil +} diff --git a/tests/e2e/configurer/setup.go b/tests/e2e/configurer/setup.go new file mode 100644 index 000000000..4eea29e7d --- /dev/null +++ b/tests/e2e/configurer/setup.go @@ -0,0 +1,24 @@ +package configurer + +type setupFn func(configurer Configurer) error + +func baseSetup(configurer Configurer) error { + if err := configurer.RunValidators(); err != nil { + return err + } + return nil +} + +func withIBC(setupHandler setupFn) setupFn { + return func(configurer Configurer) error { + if err := setupHandler(configurer); err != nil { + return err + } + + if err := configurer.RunIBC(); err != nil { + return err + } + + return nil + } +} diff --git a/tests/e2e/containers/config.go b/tests/e2e/containers/config.go new file mode 100644 index 000000000..75f42cbd3 --- /dev/null +++ b/tests/e2e/containers/config.go @@ -0,0 +1,37 @@ +package containers + +// ImageConfig contains all images and their respective tags +// needed for running e2e tests. +type ImageConfig struct { + InitRepository string + InitTag string + + TerraRepository string + TerraTag string + + RelayerRepository string + RelayerTag string +} + +//nolint:deadcode +const ( + // Current Git branch Terra repo/version. It is meant to be built locally. + // This image should be pre-built with `make docker-build-debug` either in CI or locally. + CurrentBranchTerraRepository = "terra" + CurrentBranchTerraTag = "debug" + // Hermes repo/version for relayer + relayerRepository = "informalsystems/hermes" + relayerTag = "1.5.1" +) + +// Returns ImageConfig needed for running e2e test. +func NewImageConfig() ImageConfig { + config := ImageConfig{ + RelayerRepository: relayerRepository, + RelayerTag: relayerTag, + } + + config.TerraRepository = CurrentBranchTerraRepository + config.TerraTag = CurrentBranchTerraTag + return config +} diff --git a/tests/e2e/containers/containers.go b/tests/e2e/containers/containers.go new file mode 100644 index 000000000..e914f9041 --- /dev/null +++ b/tests/e2e/containers/containers.go @@ -0,0 +1,327 @@ +package containers + +import ( + "bytes" + "context" + "fmt" + "os" + "regexp" + "strings" + "testing" + "time" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/stretchr/testify/require" +) + +const ( + HermesContainerName1 = "hermes-relayer" + HermesContainerName2 = "hermes-relayer2" + // The maximum number of times debug logs are printed to console + // per CLI command. + maxDebugLogsPerCommand = 3 +) + +var defaultErrRegex = regexp.MustCompile(`(E|e)rror`) + +// Manager is a wrapper around all Docker instances, and the Docker API. +// It provides utilities to run and interact with all Docker containers used within e2e testing. +type Manager struct { + ImageConfig + pool *dockertest.Pool + network *dockertest.Network + resources map[string]*dockertest.Resource + isDebugLogEnabled bool +} + +// NewManager creates a new Manager instance and initializes +// all Docker specific utilies. Returns an error if initialiation fails. +func NewManager(isDebugLogEnabled bool) (docker *Manager, err error) { + docker = &Manager{ + ImageConfig: NewImageConfig(), + resources: make(map[string]*dockertest.Resource), + isDebugLogEnabled: isDebugLogEnabled, + } + docker.pool, err = dockertest.NewPool("") + if err != nil { + return nil, err + } + docker.network, err = docker.pool.CreateNetwork("terra-testnet") + if err != nil { + return nil, err + } + return docker, nil +} + +// ExecTxCmd Runs ExecTxCmdWithSuccessString searching for `code: 0` +func (m *Manager) ExecTxCmd(t *testing.T, chainID string, containerName string, command []string) (bytes.Buffer, bytes.Buffer, error) { + return m.ExecTxCmdWithSuccessString(t, chainID, containerName, command, "code: 0") +} + +// ExecTxCmdWithSuccessString Runs ExecCmd, with flags for txs added. +// namely adding flags `--chain-id={chain-id} -b=block --yes --keyring-backend=test "--log_format=json"`, +// and searching for `successStr` +func (m *Manager) ExecTxCmdWithSuccessString(t *testing.T, chainID string, containerName string, command []string, successStr string) (bytes.Buffer, bytes.Buffer, error) { + allTxArgs := []string{fmt.Sprintf("--chain-id=%s", chainID), "-b=block", "--yes", "--keyring-backend=test", "--log_format=json"} + txCommand := append(command, allTxArgs...) //nolint + return m.ExecCmd(t, containerName, txCommand, successStr) +} + +// ExecHermesCmd executes command on the hermes relayer 1 container. +func (m *Manager) ExecHermesCmd(t *testing.T, command []string, hermesContainerName string, success string) (bytes.Buffer, bytes.Buffer, error) { + return m.ExecCmd(t, hermesContainerName, command, success) +} + +func (m *Manager) ExecCmd(t *testing.T, containerName string, command []string, success string) (bytes.Buffer, bytes.Buffer, error) { + if _, ok := m.resources[containerName]; !ok { + return bytes.Buffer{}, bytes.Buffer{}, fmt.Errorf("no resource %s found", containerName) + } + containerID := m.resources[containerName].Container.ID + + var ( + outBuf bytes.Buffer + errBuf bytes.Buffer + ) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + if m.isDebugLogEnabled { + t.Logf("\n\nRunning: \"%s\", success condition is \"%s\"", command, success) + } + maxDebugLogTriesLeft := maxDebugLogsPerCommand + + // We use the `require.Eventually` function because it is only allowed to do one transaction per block without + // sequence numbers. For simplicity, we avoid keeping track of the sequence number and just use the `require.Eventually`. + require.Eventually( + t, + func() bool { + exec, err := m.pool.Client.CreateExec(docker.CreateExecOptions{ + Context: ctx, + AttachStdout: true, + AttachStderr: true, + Container: containerID, + User: "root", + Cmd: command, + }) + require.NoError(t, err) + + err = m.pool.Client.StartExec(exec.ID, docker.StartExecOptions{ + Context: ctx, + Detach: false, + OutputStream: &outBuf, + ErrorStream: &errBuf, + }) + if err != nil { + return false + } + + errBufString := errBuf.String() + // Note that this does not match all errors. + // This only works if CLI outpurs "Error" or "error" + // to stderr. + if (defaultErrRegex.MatchString(errBufString) || m.isDebugLogEnabled) && maxDebugLogTriesLeft > 0 { + t.Log("\nstderr:") + t.Log(errBufString) + + t.Log("\nstdout:") + t.Log(outBuf.String()) + // N.B: We should not be returning false here + // because some applications such as Hermes might log + // "error" to stderr when they function correctly, + // causing test flakiness. This log is needed only for + // debugging purposes. + maxDebugLogTriesLeft-- + } + + if success != "" { + return strings.Contains(outBuf.String(), success) || strings.Contains(errBufString, success) + } + + return true + }, + time.Minute, + 50*time.Millisecond, + fmt.Sprintf("success condition (%s) was not met.\nstdout:\n %s\nstderr:\n %s\n", + success, outBuf.String(), errBuf.String()), + ) + + return outBuf, errBuf, nil +} + +// RunHermesResource runs a Hermes container. Returns the container resource and error if any. +// the name of the hermes container is "--relayer" +func (m *Manager) RunHermesResource(chainAID, terraARelayerNodeName, terraAValMnemonic, chainBID, terraBRelayerNodeName, terraBValMnemonic string, hermesContainerName string, hermesCfgPath string) (*dockertest.Resource, error) { + hermesResource, err := m.pool.RunWithOptions( + &dockertest.RunOptions{ + Name: hermesContainerName, + Repository: m.RelayerRepository, + Tag: m.RelayerTag, + NetworkID: m.network.Network.ID, + Cmd: []string{ + "start", + }, + User: "root:root", + Mounts: []string{ + fmt.Sprintf("%s/:/root/hermes", hermesCfgPath), + }, + ExposedPorts: []string{ + "3031", + }, + PortBindings: map[docker.Port][]docker.PortBinding{ + "3031/tcp": {{HostIP: "", HostPort: "3031"}}, + }, + Env: []string{ + fmt.Sprintf("TERRA_A_E2E_CHAIN_ID=%s", chainAID), + fmt.Sprintf("TERRA_B_E2E_CHAIN_ID=%s", chainBID), + fmt.Sprintf("TERRA_A_E2E_VAL_MNEMONIC=%s", terraAValMnemonic), + fmt.Sprintf("TERRA_B_E2E_VAL_MNEMONIC=%s", terraBValMnemonic), + fmt.Sprintf("TERRA_A_E2E_VAL_HOST=%s", terraARelayerNodeName), + fmt.Sprintf("TERRA_B_E2E_VAL_HOST=%s", terraBRelayerNodeName), + }, + Entrypoint: []string{ + "sh", + "-c", + "chmod +x /root/hermes/hermes_bootstrap.sh && /root/hermes/hermes_bootstrap.sh", + }, + }, + noRestart, + ) + if err != nil { + return nil, err + } + m.resources[hermesContainerName] = hermesResource + return hermesResource, nil +} + +// RunNodeResource runs a node container. Assings containerName to the container. +// Mounts the container on valConfigDir volume on the running host. Returns the container resource and error if any. +func (m *Manager) RunNodeResource(containerName, valCondifDir string) (*dockertest.Resource, error) { + pwd, err := os.Getwd() + if err != nil { + return nil, err + } + + runOpts := &dockertest.RunOptions{ + Name: containerName, + Repository: m.TerraRepository, + Tag: m.TerraTag, + NetworkID: m.network.Network.ID, + User: "root:root", + Cmd: []string{"start"}, + Mounts: []string{ + fmt.Sprintf("%s/:/terra/.terra", valCondifDir), + fmt.Sprintf("%s/scripts:/terra", pwd), + }, + } + + resource, err := m.pool.RunWithOptions(runOpts, noRestart) + if err != nil { + return nil, err + } + + m.resources[containerName] = resource + + return resource, nil +} + +// RunChainInitResource runs a chain init container to initialize genesis and configs for a chain with chainID. +// The chain is to be configured with chainVotingPeriod and validators deserialized from validatorConfigBytes. +// The genesis and configs are to be mounted on the init container as volume on mountDir path. +// Returns the container resource and error if any. This method does not Purge the container. The caller +// must deal with removing the resource. +func (m *Manager) RunChainInitResource(chainID string, chainVotingPeriod, chainExpeditedVotingPeriod int, validatorConfigBytes []byte, mountDir string, forkHeight int) (*dockertest.Resource, error) { + votingPeriodDuration := time.Duration(chainVotingPeriod * 1000000000) + expeditedVotingPeriodDuration := time.Duration(chainExpeditedVotingPeriod * 1000000000) + + initResource, err := m.pool.RunWithOptions( + &dockertest.RunOptions{ + Name: chainID, + Repository: m.ImageConfig.InitRepository, + Tag: m.ImageConfig.InitTag, + NetworkID: m.network.Network.ID, + Cmd: []string{ + fmt.Sprintf("--data-dir=%s", mountDir), + fmt.Sprintf("--chain-id=%s", chainID), + fmt.Sprintf("--config=%s", validatorConfigBytes), + fmt.Sprintf("--voting-period=%v", votingPeriodDuration), + fmt.Sprintf("--expedited-voting-period=%v", expeditedVotingPeriodDuration), + fmt.Sprintf("--fork-height=%v", forkHeight), + }, + User: "root:root", + Mounts: []string{ + fmt.Sprintf("%s:%s", mountDir, mountDir), + }, + }, + noRestart, + ) + if err != nil { + return nil, err + } + return initResource, nil +} + +// PurgeResource purges the container resource and returns an error if any. +func (m *Manager) PurgeResource(resource *dockertest.Resource) error { + return m.pool.Purge(resource) +} + +// GetNodeResource returns the node resource for containerName. +func (m *Manager) GetNodeResource(containerName string) (*dockertest.Resource, error) { + resource, exists := m.resources[containerName] + if !exists { + return nil, fmt.Errorf("node resource not found: container name: %s", containerName) + } + return resource, nil +} + +// GetHostPort returns the port-forwarding address of the running host +// necessary to connect to the portId exposed inside the container. +// The container is determined by containerName. +// Returns the host-port or error if any. +func (m *Manager) GetHostPort(containerName string, portID string) (string, error) { + resource, err := m.GetNodeResource(containerName) + if err != nil { + return "", err + } + return resource.GetHostPort(portID), nil +} + +// RemoveNodeResource removes a node container specified by containerName. +// Returns error if any. +func (m *Manager) RemoveNodeResource(containerName string) error { + resource, err := m.GetNodeResource(containerName) + if err != nil { + return err + } + var opts docker.RemoveContainerOptions + opts.ID = resource.Container.ID + opts.Force = true + if err := m.pool.Client.RemoveContainer(opts); err != nil { + return err + } + delete(m.resources, containerName) + return nil +} + +// ClearResources removes all outstanding Docker resources created by the Manager. +func (m *Manager) ClearResources() error { + for _, resource := range m.resources { + if err := m.pool.Purge(resource); err != nil { + return err + } + } + + if err := m.pool.RemoveNetwork(m.network); err != nil { + return err + } + return nil +} + +func noRestart(config *docker.HostConfig) { + // in this case we don't want the nodes to restart on failure + config.RestartPolicy = docker.RestartPolicy{ + Name: "no", + } +} diff --git a/tests/e2e/e2e.Dockerfile b/tests/e2e/e2e.Dockerfile new file mode 100644 index 000000000..6d5c54d06 --- /dev/null +++ b/tests/e2e/e2e.Dockerfile @@ -0,0 +1,123 @@ +# syntax=docker/dockerfile:1 + +ARG source=./ +ARG GO_VERSION="1.20" +ARG BUILDPLATFORM=linux/amd64 +ARG BASE_IMAGE="golang:${GO_VERSION}-alpine3.18" +FROM --platform=${BUILDPLATFORM} ${BASE_IMAGE} as base + +############################################################################### +# Builder +############################################################################### + +FROM base as builder-stage-1 + +ARG source +ARG GIT_COMMIT +ARG GIT_VERSION +ARG BUILDPLATFORM +ARG GOOS=linux \ + GOARCH=amd64 + +ENV GOOS=$GOOS \ + GOARCH=$GOARCH + +# NOTE: add libusb-dev to run with LEDGER_ENABLED=true +RUN set -eux &&\ + apk update &&\ + apk add --no-cache \ + ca-certificates \ + linux-headers \ + build-base \ + cmake \ + git + +# install mimalloc for musl +WORKDIR ${GOPATH}/src/mimalloc +RUN set -eux &&\ + git clone --depth 1 --branch v2.1.2 \ + https://github.com/microsoft/mimalloc . &&\ + mkdir -p build &&\ + cd build &&\ + cmake .. &&\ + make -j$(nproc) &&\ + make install + +# download dependencies to cache as layer +WORKDIR ${GOPATH}/src/app +COPY ${source}go.mod ${source}go.sum ./ +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/root/go/pkg/mod \ + go mod download -x + +# Cosmwasm - Download correct libwasmvm version and verify checksum +RUN set -eux &&\ + WASMVM_VERSION=$(go list -m github.com/CosmWasm/wasmvm | cut -d ' ' -f 5) && \ + WASMVM_DOWNLOADS="https://github.com/classic-terra/wasmvm/releases/download/${WASMVM_VERSION}"; \ + wget ${WASMVM_DOWNLOADS}/checksums.txt -O /tmp/checksums.txt; \ + if [ ${BUILDPLATFORM} = "linux/amd64" ]; then \ + WASMVM_URL="${WASMVM_DOWNLOADS}/libwasmvm_muslc.x86_64.a"; \ + elif [ ${BUILDPLATFORM} = "linux/arm64" ]; then \ + WASMVM_URL="${WASMVM_DOWNLOADS}/libwasmvm_muslc.aarch64.a"; \ + else \ + echo "Unsupported Build Platfrom ${BUILDPLATFORM}"; \ + exit 1; \ + fi; \ + wget ${WASMVM_URL} -O /lib/libwasmvm_muslc.a; \ + CHECKSUM=`sha256sum /lib/libwasmvm_muslc.a | cut -d" " -f1`; \ + grep ${CHECKSUM} /tmp/checksums.txt; \ + rm /tmp/checksums.txt + +############################################################################### + +FROM builder-stage-1 as builder-stage-2 + +ARG source +ARG GOOS=linux \ + GOARCH=amd64 + +ENV GOOS=$GOOS \ + GOARCH=$GOARCH + +# Copy the remaining files +COPY ${source} . + +# Build app binary +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/root/go/pkg/mod \ + go install \ + -mod=readonly \ + -tags "netgo,muslc" \ + -ldflags " \ + -w -s -linkmode=external -extldflags \ + '-L/go/src/mimalloc/build -lmimalloc -Wl,-z,muldefs -static' \ + -X github.com/cosmos/cosmos-sdk/version.Name='terra' \ + -X github.com/cosmos/cosmos-sdk/version.AppName='terrad' \ + -X github.com/cosmos/cosmos-sdk/version.Version=${GIT_VERSION} \ + -X github.com/cosmos/cosmos-sdk/version.Commit=${GIT_COMMIT} \ + -X github.com/cosmos/cosmos-sdk/version.BuildTags='netgo,muslc' \ + " \ + -trimpath \ + ./... + +################################################################################ + +FROM alpine as terra-core + +RUN apk update && apk add wget lz4 aria2 curl jq gawk coreutils "zlib>1.2.12-r2" libssl3 + +COPY --from=builder-stage-2 /go/bin/terrad /usr/local/bin/terrad + +ENV HOME /terra +WORKDIR $HOME + +# rest server +EXPOSE 1317 +# grpc +EXPOSE 9090 +# tendermint p2p +EXPOSE 26656 +# tendermint rpc +EXPOSE 26657 + +ENTRYPOINT ["terrad"] \ No newline at end of file diff --git a/tests/e2e/e2e.mk b/tests/e2e/e2e.mk new file mode 100644 index 000000000..bde203884 --- /dev/null +++ b/tests/e2e/e2e.mk @@ -0,0 +1,43 @@ +#!/usr/bin/make -f + +VERSION := $(shell echo $(shell git describe --tags) | sed 's/^v//') +COMMIT := $(shell git log -1 --format='%H') +PACKAGES_E2E=$(shell go list ./... | grep '/e2e') +BUILDDIR ?= $(CURDIR)/build + +test-e2e: e2e-setup test-e2e-ci + +# test-e2e-ci runs a full e2e test suite +# does not do any validation about the state of the Docker environment +# As a result, avoid using this locally. +test-e2e-ci: + @VERSION=$(VERSION) TERRA_E2E=True TERRA_E2E_DEBUG_LOG=True go test -mod=readonly -timeout=25m -v $(PACKAGES_E2E) + +# test-e2e-debug runs a full e2e test suite but does +# not attempt to delete Docker resources at the end. +test-e2e-debug: e2e-setup + @VERSION=$(VERSION) TERRA_E2E=True TERRA_E2E_DEBUG_LOG=True TERRA_E2E_SKIP_CLEANUP=True go test -mod=readonly -timeout=25m -v $(PACKAGES_E2E) -count=1 + +# test-e2e-short runs the e2e test with only short tests. +# Does not delete any of the containers after running. +# Deletes any existing containers before running. +# Does not use Go cache. +test-e2e-short: e2e-setup + @VERSION=$(VERSION) TERRA_E2E=True TERRA_E2E_DEBUG_LOG=True TERRA_E2E_SKIP_CLEANUP=True go test -mod=readonly -timeout=25m -v $(PACKAGES_E2E) -count=1 + +build-e2e-script: + mkdir -p $(BUILDDIR) + go build -mod=readonly $(BUILD_FLAGS) -o $(BUILDDIR)/ ./tests/e2e/initialization/$(E2E_SCRIPT_NAME) + +docker-build-debug: + @DOCKER_BUILDKIT=1 docker build -t terra:${COMMIT} --platform linux/amd64 --build-arg BASE_IMG_TAG=debug -f ./tests/e2e/e2e.Dockerfile . + @DOCKER_BUILDKIT=1 docker tag terra:${COMMIT} terra:debug + +e2e-setup: e2e-check-image-sha e2e-remove-resources + @echo Finished e2e environment setup, ready to start the test + +e2e-check-image-sha: + tests/e2e/scripts/run/check_image_sha.sh + +e2e-remove-resources: + tests/e2e/scripts/run/remove_stale_resources.sh diff --git a/tests/e2e/e2e_setup_test.go b/tests/e2e/e2e_setup_test.go new file mode 100644 index 000000000..60bd98bb0 --- /dev/null +++ b/tests/e2e/e2e_setup_test.go @@ -0,0 +1,98 @@ +package e2e + +import ( + "os" + "strconv" + "testing" + + "github.com/stretchr/testify/suite" + + configurer "github.com/classic-terra/core/v2/tests/e2e/configurer" +) + +const ( + // Environment variable signifying whether to run e2e tests. + e2eEnabledEnv = "TERRA_E2E" + // Environment variable name to skip the IBC tests + skipIBCEnv = "TERRA_E2E_SKIP_IBC" + // Environment variable name to skip cleaning up Docker resources in teardown + skipCleanupEnv = "TERRA_E2E_SKIP_CLEANUP" +) + +type IntegrationTestSuite struct { + suite.Suite + + configurer configurer.Configurer + skipIBC bool + skipStateSync bool +} + +func TestIntegrationTestSuite(t *testing.T) { + isEnabled := os.Getenv(e2eEnabledEnv) + if isEnabled != "True" { + t.Skipf("e2e test is disabled. To run, set %s to True", e2eEnabledEnv) + } + suite.Run(t, new(IntegrationTestSuite)) +} + +func (s *IntegrationTestSuite) SetupSuite() { + s.T().Log("setting up e2e integration test suite...") + var err error + + // The e2e test flow is as follows: + // + // 1. Configure two chains - chan A and chain B. + // * For each chain, set up several validator nodes + // * Initialize configs and genesis for all them. + // 2. Start both networks. + // 3. Run IBC relayer betweeen the two chains. + // 4. Execute various e2e tests, including IBC. + if str := os.Getenv(skipIBCEnv); len(str) > 0 { + s.skipIBC, err = strconv.ParseBool(str) + s.Require().NoError(err) + if s.skipIBC { + s.T().Logf("%s was true, skipping IBC tests", skipIBCEnv) + } + } + + if str := os.Getenv("TERRA_E2E_SKIP_STATE_SYNC"); len(str) > 0 { + s.skipStateSync, err = strconv.ParseBool(str) + s.Require().NoError(err) + if s.skipStateSync { + s.T().Log("skipping state sync testing") + } + } + + isDebugLogEnabled := false + if str := os.Getenv("TERRA_E2E_DEBUG_LOG"); len(str) > 0 { + isDebugLogEnabled, err = strconv.ParseBool(str) + s.Require().NoError(err) + if isDebugLogEnabled { + s.T().Log("debug logging is enabled. container logs from running cli commands will be printed to stdout") + } + } + + s.configurer, err = configurer.New(s.T(), !s.skipIBC, isDebugLogEnabled) + s.Require().NoError(err) + + err = s.configurer.ConfigureChains() + s.Require().NoError(err) + + err = s.configurer.RunSetup() + s.Require().NoError(err) +} + +func (s *IntegrationTestSuite) TearDownSuite() { + if str := os.Getenv(skipCleanupEnv); len(str) > 0 { + skipCleanup, err := strconv.ParseBool(str) + s.Require().NoError(err) + + if skipCleanup { + s.T().Log("skipping e2e resources clean up...") + return + } + } + + err := s.configurer.ClearResources() + s.Require().NoError(err) +} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go new file mode 100644 index 000000000..afe3ef479 --- /dev/null +++ b/tests/e2e/e2e_test.go @@ -0,0 +1,278 @@ +package e2e + +import ( + "fmt" + "strconv" + "time" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/classic-terra/core/v2/tests/e2e/initialization" +) + +func (s *IntegrationTestSuite) TestIBCWasmHooks() { + if s.skipIBC { + s.T().Skip("Skipping IBC tests") + } + chainA := s.configurer.GetChainConfig(0) + chainB := s.configurer.GetChainConfig(1) + + nodeA, err := chainA.GetDefaultNode() + s.NoError(err) + nodeB, err := chainB.GetDefaultNode() + s.NoError(err) + + nodeA.StoreWasmCode("counter.wasm", initialization.ValidatorWalletName) + chainA.LatestCodeID = int(nodeA.QueryLatestWasmCodeID()) + nodeA.InstantiateWasmContract( + strconv.Itoa(chainA.LatestCodeID), + `{"count": "0"}`, "", + initialization.ValidatorWalletName) + + contracts, err := nodeA.QueryContractsFromID(chainA.LatestCodeID) + s.NoError(err) + s.Len(contracts, 1, "Wrong number of contracts for the counter") + contractAddr := contracts[0] + + transferAmount := sdk.NewInt(10000000) + validatorAddr := nodeB.GetWallet(initialization.ValidatorWalletName) + nodeB.SendIBCTransfer(validatorAddr, contractAddr, fmt.Sprintf("%duluna", transferAmount.Int64()), + fmt.Sprintf(`{"wasm":{"contract":"%s","msg": {"increment": {}} }}`, contractAddr)) + + // check the balance of the contract + s.Eventually(func() bool { + balance, err := nodeA.QueryBalances(contractAddr) + s.Require().NoError(err) + if len(balance) == 0 { + return false + } + return balance[0].Amount.Equal(transferAmount) + }, + initialization.OneMin, + 10*time.Millisecond) + + // sender wasm addr + // senderBech32, err := ibchookskeeper.DeriveIntermediateSender("channel-0", validatorAddr, "terra") + var response interface{} + response, err = nodeA.QueryWasmSmart(contractAddr, `{"get_total_funds": {}}`) + s.Require().NoError(err) + + s.Eventually(func() bool { + response, err = nodeA.QueryWasmSmart(contractAddr, `{"get_total_funds": {}}`) + if err != nil { + return false + } + + totalFunds := response.([]interface{})[0] + amount, err := strconv.ParseInt(totalFunds.(map[string]interface{})["amount"].(string), 10, 64) + if err != nil { + return false + } + denom := totalFunds.(map[string]interface{})["denom"].(string) + + response, err = nodeA.QueryWasmSmart(contractAddr, `{"get_count": {}}`) + if err != nil { + return false + } + count, err := strconv.ParseInt(response.(string), 10, 64) + if err != nil { + return false + } + // check if denom is uluna token ibc + return sdk.NewInt(amount).Equal(transferAmount) && denom == initialization.TerraIBCDenom && count == 1 + }, + 10*time.Second, + 10*time.Millisecond, + ) +} + +func (s *IntegrationTestSuite) TestPacketForwardMiddleware() { + if s.skipIBC { + s.T().Skip("Skipping Packet Forward Middleware tests") + } + chainA := s.configurer.GetChainConfig(0) + chainB := s.configurer.GetChainConfig(1) + chainC := s.configurer.GetChainConfig(2) + + nodeA, err := chainA.GetDefaultNode() + s.NoError(err) + nodeB, err := chainB.GetDefaultNode() + s.NoError(err) + nodeC, err := chainC.GetDefaultNode() + s.NoError(err) + + transferAmount := sdk.NewInt(10000000) + + validatorAddr := nodeA.GetWallet(initialization.ValidatorWalletName) + s.Require().NotEqual(validatorAddr, "") + + receiver := nodeB.GetWallet(initialization.ValidatorWalletName) + + balanceReceiverOld, err := nodeC.QuerySpecificBalance(receiver, initialization.TerraDenom) + s.Require().NoError(err) + s.Require().Equal(balanceReceiverOld, sdk.Coin{}) + + nodeA.SendIBCTransfer(validatorAddr, receiver, fmt.Sprintf("%duluna", transferAmount.Int64()), + fmt.Sprintf(`{"forward":{"receiver":"%s","port":"transfer","channel":"channel-2"}}`, receiver)) + + // wait for ibc cycle + time.Sleep(30 * time.Second) + + s.Eventually(func() bool { + balanceReceiver, err := nodeC.QueryBalances(receiver) + s.Require().NoError(err) + if len(balanceReceiver) == 0 { + return false + } + return balanceReceiver[0].Amount.Equal(transferAmount) + }, + 15*time.Second, + 10*time.Millisecond, + ) +} + +func (s *IntegrationTestSuite) TestAddBurnTaxExemptionAddress() { + chain := s.configurer.GetChainConfig(0) + node, err := chain.GetDefaultNode() + s.Require().NoError(err) + + whitelistAddr1 := node.CreateWallet("whitelist1") + whitelistAddr2 := node.CreateWallet("whitelist2") + + chain.AddBurnTaxExemptionAddressProposal(node, whitelistAddr1, whitelistAddr2) + + whitelistedAddresses, err := node.QueryBurnTaxExemptionList() + s.Require().NoError(err) + s.Require().Len(whitelistedAddresses, 2) + s.Require().Contains(whitelistedAddresses, whitelistAddr1) + s.Require().Contains(whitelistedAddresses, whitelistAddr2) +} + +func (s *IntegrationTestSuite) TestFeeTax() { + chain := s.configurer.GetChainConfig(0) + node, err := chain.GetDefaultNode() + s.Require().NoError(err) + + transferAmount1 := sdkmath.NewInt(20000000) + transferCoin1 := sdk.NewCoin(initialization.TerraDenom, transferAmount1) + + validatorAddr := node.GetWallet(initialization.ValidatorWalletName) + s.Require().NotEqual(validatorAddr, "") + + validatorBalance, err := node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) + s.Require().NoError(err) + + test1Addr := node.CreateWallet("test1") + + // Test 1: banktypes.MsgSend + // burn tax with bank send + node.BankSend(transferCoin1.String(), validatorAddr, test1Addr) + + subAmount := transferAmount1.Add(initialization.TaxRate.MulInt(transferAmount1).TruncateInt()) + + decremented := validatorBalance.Sub(sdk.NewCoin(initialization.TerraDenom, subAmount)) + newValidatorBalance, err := node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) + s.Require().NoError(err) + + balanceTest1, err := node.QuerySpecificBalance(test1Addr, initialization.TerraDenom) + s.Require().NoError(err) + + s.Require().Equal(balanceTest1.Amount, transferAmount1) + s.Require().Equal(newValidatorBalance, decremented) + + // Test 2: try bank send with grant + test2Addr := node.CreateWallet("test2") + transferAmount2 := sdkmath.NewInt(10000000) + transferCoin2 := sdk.NewCoin(initialization.TerraDenom, transferAmount2) + + node.BankSend(transferCoin2.String(), validatorAddr, test2Addr) + node.GrantAddress(test2Addr, test1Addr, transferCoin2.String(), "test2") + + validatorBalance, err = node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) + s.Require().NoError(err) + + node.BankSendFeeGrantWithWallet(transferCoin2.String(), test1Addr, validatorAddr, test2Addr, "test1") + + newValidatorBalance, err = node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) + s.Require().NoError(err) + + balanceTest1, err = node.QuerySpecificBalance(test1Addr, initialization.TerraDenom) + s.Require().NoError(err) + + balanceTest2, err := node.QuerySpecificBalance(test2Addr, initialization.TerraDenom) + s.Require().NoError(err) + + s.Require().Equal(balanceTest1.Amount, transferAmount1.Sub(transferAmount2)) + s.Require().Equal(newValidatorBalance, validatorBalance.Add(transferCoin2)) + s.Require().Equal(balanceTest2.Amount, transferAmount2.Sub(initialization.TaxRate.MulInt(transferAmount2).TruncateInt())) + + // Test 3: banktypes.MsgMultiSend + validatorBalance, err = node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) + s.Require().NoError(err) + + node.BankMultiSend(transferCoin1.String(), false, validatorAddr, test1Addr, test2Addr) + + newValidatorBalance, err = node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) + s.Require().NoError(err) + + totalTransferAmount := transferAmount1.Mul(sdk.NewInt(2)) + subAmount = totalTransferAmount.Add(initialization.TaxRate.MulInt(totalTransferAmount).TruncateInt()) + s.Require().Equal(newValidatorBalance, validatorBalance.Sub(sdk.NewCoin(initialization.TerraDenom, subAmount))) +} + +func (s *IntegrationTestSuite) TestFeeTaxWasm() { + chain := s.configurer.GetChainConfig(0) + node, err := chain.GetDefaultNode() + s.Require().NoError(err) + + testAddr := node.CreateWallet("test") + transferAmount := sdkmath.NewInt(100000000) + transferCoin := sdk.NewCoin(initialization.TerraDenom, transferAmount) + node.BankSend(fmt.Sprintf("%suluna", transferAmount.Mul(sdk.NewInt(4))), initialization.ValidatorWalletName, testAddr) + + node.StoreWasmCode("counter.wasm", initialization.ValidatorWalletName) + chain.LatestCodeID = int(node.QueryLatestWasmCodeID()) + // instantiate contract and transfer 100000000uluna + node.InstantiateWasmContract( + strconv.Itoa(chain.LatestCodeID), + `{"count": "0"}`, transferCoin.String(), + "test") + + contracts, err := node.QueryContractsFromID(chain.LatestCodeID) + s.Require().NoError(err) + s.Require().Len(contracts, 1, "Wrong number of contracts for the counter") + + balance1, err := node.QuerySpecificBalance(testAddr, initialization.TerraDenom) + s.Require().NoError(err) + // 400000000 - 100000000 - 100000000 * TaxRate = 300000000 - 10000000 * TaxRate + taxAmount := initialization.TaxRate.MulInt(transferAmount).TruncateInt() + s.Require().Equal(balance1.Amount, transferAmount.Mul(sdk.NewInt(3)).Sub(taxAmount)) + + stabilityFee := sdk.NewDecWithPrec(2, 2).MulInt(transferAmount) + + node.Instantiate2WasmContract( + strconv.Itoa(chain.LatestCodeID), + `{"count": "0"}`, "salt", + transferCoin.String(), + fmt.Sprintf("%duluna", stabilityFee), "test") + + contracts, err = node.QueryContractsFromID(chain.LatestCodeID) + s.Require().NoError(err) + s.Require().Len(contracts, 2, "Wrong number of contracts for the counter") + + balance2, err := node.QuerySpecificBalance(testAddr, initialization.TerraDenom) + s.Require().NoError(err) + // balance1 - 100000000 - 100000000 * TaxRate + taxAmount = initialization.TaxRate.MulInt(transferAmount).TruncateInt() + s.Require().Equal(balance2.Amount, balance1.Amount.Sub(transferAmount).Sub(taxAmount)) + + contractAddr := contracts[0] + node.WasmExecute(contractAddr, `{"donate": {}}`, transferCoin.String(), fmt.Sprintf("%duluna", stabilityFee), "test") + + balance3, err := node.QuerySpecificBalance(testAddr, initialization.TerraDenom) + s.Require().NoError(err) + // balance2 - 100000000 - 100000000 * TaxRate + taxAmount = initialization.TaxRate.MulInt(transferAmount).TruncateInt() + s.Require().Equal(balance3.Amount, balance2.Amount.Sub(transferAmount).Sub(taxAmount)) +} diff --git a/tests/e2e/initialization/chain.go b/tests/e2e/initialization/chain.go new file mode 100644 index 000000000..48657683e --- /dev/null +++ b/tests/e2e/initialization/chain.go @@ -0,0 +1,35 @@ +package initialization + +const ( + keyringPassphrase = "testpassphrase" + keyringAppName = "testnet" +) + +// internalChain contains the same info as chain, but with the validator structs instead using the internal validator +// representation, with more derived data +type internalChain struct { + chainMeta ChainMeta + nodes []*internalNode +} + +func new(id, dataDir string) (*internalChain, error) { + chainMeta := ChainMeta{ + ID: id, + DataDir: dataDir, + } + return &internalChain{ + chainMeta: chainMeta, + }, nil +} + +func (c *internalChain) export() *Chain { + exportNodes := make([]*Node, 0, len(c.nodes)) + for _, v := range c.nodes { + exportNodes = append(exportNodes, v.export()) + } + + return &Chain{ + ChainMeta: c.chainMeta, + Nodes: exportNodes, + } +} diff --git a/tests/e2e/initialization/chain/main.go b/tests/e2e/initialization/chain/main.go new file mode 100644 index 000000000..ef9e226eb --- /dev/null +++ b/tests/e2e/initialization/chain/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + + "github.com/classic-terra/core/v2/tests/e2e/initialization" +) + +func main() { + var ( + valConfig []*initialization.NodeConfig + dataDir string + chainID string + config string + forkHeight int + ) + + flag.StringVar(&dataDir, "data-dir", "", "chain data directory") + flag.StringVar(&chainID, "chain-id", "", "chain ID") + flag.StringVar(&config, "config", "", "serialized config") + flag.IntVar(&forkHeight, "fork-height", 0, "fork height") + + flag.Parse() + + err := json.Unmarshal([]byte(config), &valConfig) + if err != nil { + panic(err) + } + + if len(dataDir) == 0 { + panic("data-dir is required") + } + + if err := os.MkdirAll(dataDir, 0o755); err != nil { + panic(err) + } + + createdChain, err := initialization.InitChain(chainID, dataDir, valConfig, forkHeight) + if err != nil { + panic(err) + } + + b, _ := json.Marshal(createdChain) + fileName := fmt.Sprintf("%v/%v-encode", dataDir, chainID) + if err = os.WriteFile(fileName, b, 0o777); err != nil { //nolint + panic(err) + } +} diff --git a/tests/e2e/initialization/config.go b/tests/e2e/initialization/config.go new file mode 100644 index 000000000..d93346f96 --- /dev/null +++ b/tests/e2e/initialization/config.go @@ -0,0 +1,368 @@ +package initialization + +import ( + "encoding/json" + "fmt" + "path/filepath" + "time" + + "github.com/cosmos/cosmos-sdk/server" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + staketypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/gogo/protobuf/proto" + tmjson "github.com/tendermint/tendermint/libs/json" + + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + + "github.com/classic-terra/core/v2/tests/e2e/util" + treasurytypes "github.com/classic-terra/core/v2/x/treasury/types" +) + +// NodeConfig is a confiuration for the node supplied from the test runner +// to initialization scripts. It should be backwards compatible with earlier +// versions. If this struct is updated, the change must be backported to earlier +// branches that might be used for upgrade testing. +type NodeConfig struct { + Name string // name of the config that will also be assigned to Docke container. + Pruning string // default, nothing, everything, or custom + PruningKeepRecent string // keep all of the last N states (only used with custom pruning) + PruningInterval string // delete old states from every Nth block (only used with custom pruning) + SnapshotInterval uint64 // statesync snapshot every Nth block (0 to disable) + SnapshotKeepRecent uint32 // number of recent snapshots to keep and serve (0 to keep all) + IsValidator bool // flag indicating whether a node should be a validator +} + +const ( + // common + TerraDenom = "uluna" + AtomDenom = "uatom" + TerraIBCDenom = "ibc/4627AD2524E3E0523047E35BB76CC90E37D9D57ACF14F0FCBCEB2480705F3CB8" + MinGasPrice = "0.000" + IbcSendAmount = 3300000000 + ValidatorWalletName = "val" + // chainA + ChainAID = "terra-test-a" + TerraBalanceA = 20000000000000 + StakeBalanceA = 110000000000 + StakeAmountA = 100000000000 + // chainB + ChainBID = "terra-test-b" + TerraBalanceB = 500000000000 + StakeBalanceB = 440000000000 + StakeAmountB = 400000000000 + GenesisFeeBalance = 100000000000 + WalletFeeBalance = 100000000 + // chainC + ChainCID = "terra-test-c" + TerraBalanceC = 500000000000 + StakeBalanceC = 440000000000 + StakeAmountC = 400000000000 +) + +var ( + StakeAmountIntA = sdk.NewInt(StakeAmountA) + StakeAmountCoinA = sdk.NewCoin(TerraDenom, StakeAmountIntA) + StakeAmountIntB = sdk.NewInt(StakeAmountB) + StakeAmountCoinB = sdk.NewCoin(TerraDenom, StakeAmountIntB) + + InitBalanceStrA = fmt.Sprintf("%d%s", TerraBalanceA, TerraDenom) + InitBalanceStrB = fmt.Sprintf("%d%s", TerraBalanceB, TerraDenom) + InitBalanceStrC = fmt.Sprintf("%d%s", TerraBalanceC, TerraDenom) + LunaToken = sdk.NewInt64Coin(TerraDenom, IbcSendAmount) // 3,300luna + tenTerra = sdk.Coins{sdk.NewInt64Coin(TerraDenom, 10_000_000)} + + OneMin = time.Minute // nolint + TwoMin = 2 * time.Minute // nolint + FiveMin = 5 * time.Minute // nolint + TaxRate = sdk.NewDecWithPrec(2, 2) // 0.02 +) + +func addAccount(path, moniker, amountStr string, accAddr sdk.AccAddress, forkHeight int) error { + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + + config.SetRoot(path) + config.Moniker = moniker + + coins, err := sdk.ParseCoinsNormalized(amountStr) + if err != nil { + return fmt.Errorf("failed to parse coins: %w", err) + } + coins = coins.Sort() + + balances := banktypes.Balance{Address: accAddr.String(), Coins: coins.Sort()} + genAccount := authtypes.NewBaseAccount(accAddr, nil, 0, 0) + // TODO: Make the SDK make it far cleaner to add an account to GenesisState + genFile := config.GenesisFile() + appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile) + if err != nil { + return fmt.Errorf("failed to unmarshal genesis state: %w", err) + } + + genDoc.InitialHeight = int64(forkHeight) + + authGenState := authtypes.GetGenesisStateFromAppState(util.Cdc, appState) + + accs, err := authtypes.UnpackAccounts(authGenState.Accounts) + if err != nil { + return fmt.Errorf("failed to get accounts from any: %w", err) + } + + if accs.Contains(accAddr) { + return fmt.Errorf("failed to add account to genesis state; account already exists: %s", accAddr) + } + + // Add the new account to the set of genesis accounts and sanitize the + // accounts afterwards. + accs = append(accs, genAccount) + accs = authtypes.SanitizeGenesisAccounts(accs) + + genAccs, err := authtypes.PackAccounts(accs) + if err != nil { + return fmt.Errorf("failed to convert accounts into any's: %w", err) + } + + authGenState.Accounts = genAccs + + authGenStateBz, err := util.Cdc.MarshalJSON(&authGenState) + if err != nil { + return fmt.Errorf("failed to marshal auth genesis state: %w", err) + } + + appState[authtypes.ModuleName] = authGenStateBz + + bankGenState := banktypes.GetGenesisStateFromAppState(util.Cdc, appState) + bankGenState.Balances = append(bankGenState.Balances, balances) + bankGenState.Balances = banktypes.SanitizeGenesisBalances(bankGenState.Balances) + + bankGenStateBz, err := util.Cdc.MarshalJSON(bankGenState) + if err != nil { + return fmt.Errorf("failed to marshal bank genesis state: %w", err) + } + + appState[banktypes.ModuleName] = bankGenStateBz + + appStateJSON, err := json.Marshal(appState) + if err != nil { + return fmt.Errorf("failed to marshal application genesis state: %w", err) + } + + genDoc.AppState = appStateJSON + return genutil.ExportGenesisFile(genDoc, genFile) +} + +func updateModuleGenesis[V proto.Message](appGenState map[string]json.RawMessage, moduleName string, protoVal V, updateGenesis func(V)) error { + if err := util.Cdc.UnmarshalJSON(appGenState[moduleName], protoVal); err != nil { + return err + } + updateGenesis(protoVal) + newGenState := protoVal + + bz, err := util.Cdc.MarshalJSON(newGenState) + if err != nil { + return err + } + appGenState[moduleName] = bz + return nil +} + +func initGenesis(chain *internalChain, forkHeight int) error { + // initialize a genesis file + configDir := chain.nodes[0].configDir() + for _, val := range chain.nodes { + accAdd, err := val.keyInfo.GetAddress() + if err != nil { + return err + } + + switch chain.chainMeta.ID { + case ChainAID: + if err := addAccount(configDir, "", InitBalanceStrA, accAdd, forkHeight); err != nil { + return err + } + case ChainBID: + if err := addAccount(configDir, "", InitBalanceStrB, accAdd, forkHeight); err != nil { + return err + } + case ChainCID: + if err := addAccount(configDir, "", InitBalanceStrC, accAdd, forkHeight); err != nil { + return err + } + } + } + + // copy the genesis file to the remaining validators + for _, val := range chain.nodes[1:] { + _, err := util.CopyFile( + filepath.Join(configDir, "config", "genesis.json"), + filepath.Join(val.configDir(), "config", "genesis.json"), + ) + if err != nil { + return err + } + } + + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + + config.SetRoot(chain.nodes[0].configDir()) + config.Moniker = chain.nodes[0].moniker + + genFilePath := config.GenesisFile() + appGenState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFilePath) + if err != nil { + return err + } + + err = updateModuleGenesis(appGenState, staketypes.ModuleName, &staketypes.GenesisState{}, updateStakeGenesis) + if err != nil { + return err + } + + err = updateModuleGenesis(appGenState, minttypes.ModuleName, &minttypes.GenesisState{}, updateMintGenesis) + if err != nil { + return err + } + + err = updateModuleGenesis(appGenState, banktypes.ModuleName, &banktypes.GenesisState{}, updateBankGenesis) + if err != nil { + return err + } + + err = updateModuleGenesis(appGenState, crisistypes.ModuleName, &crisistypes.GenesisState{}, updateCrisisGenesis) + if err != nil { + return err + } + + err = updateModuleGenesis(appGenState, treasurytypes.ModuleName, &treasurytypes.GenesisState{}, updateTreasuryGenesis) + if err != nil { + return err + } + + err = updateModuleGenesis(appGenState, govtypes.ModuleName, &govv1.GenesisState{}, updateGovGenesis) + if err != nil { + return err + } + + err = updateModuleGenesis(appGenState, genutiltypes.ModuleName, &genutiltypes.GenesisState{}, updateGenUtilGenesis(chain)) + if err != nil { + return err + } + + bz, err := json.MarshalIndent(appGenState, "", " ") + if err != nil { + return err + } + + genDoc.AppState = bz + + genesisJSON, err := tmjson.MarshalIndent(genDoc, "", " ") + if err != nil { + return err + } + + // write the updated genesis file to each validator + for _, val := range chain.nodes { + if err := util.WritePublicFile(filepath.Join(val.configDir(), "config", "genesis.json"), genesisJSON); err != nil { + return err + } + } + return nil +} + +func updateMintGenesis(mintGenState *minttypes.GenesisState) { + mintGenState.Params.MintDenom = TerraDenom +} + +func updateBankGenesis(bankGenState *banktypes.GenesisState) { + denomsToRegister := []string{TerraDenom, AtomDenom} + for _, denom := range denomsToRegister { + setDenomMetadata(bankGenState, denom) + } +} + +func updateStakeGenesis(stakeGenState *staketypes.GenesisState) { + stakeGenState.Params = staketypes.Params{ + BondDenom: TerraDenom, + MaxValidators: 100, + MaxEntries: 7, + HistoricalEntries: 10000, + UnbondingTime: 240000000000, + MinCommissionRate: sdk.ZeroDec(), + } +} + +func updateCrisisGenesis(crisisGenState *crisistypes.GenesisState) { + crisisGenState.ConstantFee.Denom = TerraDenom +} + +func updateTreasuryGenesis(treasuryGenState *treasurytypes.GenesisState) { + treasuryGenState.TaxRate = TaxRate + treasuryGenState.Params.TaxPolicy = treasurytypes.PolicyConstraints{ + RateMin: TaxRate, // 0.02 (fixed) + RateMax: TaxRate, // 0.02 (fixed) + Cap: sdk.NewCoin(TerraDenom, sdk.NewInt(100000000000000)), + } +} + +func updateGovGenesis(govGenState *govv1.GenesisState) { + govGenState.VotingParams.VotingPeriod = &OneMin + govGenState.TallyParams.Quorum = sdk.NewDecWithPrec(2, 1).String() + govGenState.DepositParams.MinDeposit = tenTerra +} + +func updateGenUtilGenesis(c *internalChain) func(*genutiltypes.GenesisState) { + return func(genUtilGenState *genutiltypes.GenesisState) { + // generate genesis txs + genTxs := make([]json.RawMessage, 0, len(c.nodes)) + for _, node := range c.nodes { + if !node.isValidator { + continue + } + + stakeAmountCoin := StakeAmountCoinA + if c.chainMeta.ID != ChainAID { + stakeAmountCoin = StakeAmountCoinB + } + createValmsg, err := node.buildCreateValidatorMsg(stakeAmountCoin) + if err != nil { + panic("genutil genesis setup failed: " + err.Error()) + } + + signedTx, err := node.signMsg(createValmsg) + if err != nil { + panic("genutil genesis setup failed: " + err.Error()) + } + + txRaw, err := util.Cdc.MarshalJSON(signedTx) + if err != nil { + panic("genutil genesis setup failed: " + err.Error()) + } + genTxs = append(genTxs, txRaw) + } + genUtilGenState.GenTxs = genTxs + } +} + +func setDenomMetadata(genState *banktypes.GenesisState, denom string) { + genState.DenomMetadata = append(genState.DenomMetadata, banktypes.Metadata{ + Description: fmt.Sprintf("Registered denom %s for e2e testing", denom), + Display: denom, + Base: denom, + Symbol: denom, + Name: denom, + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: denom, + Exponent: 0, + }, + }, + }) +} diff --git a/tests/e2e/initialization/export.go b/tests/e2e/initialization/export.go new file mode 100644 index 000000000..906a1b4c2 --- /dev/null +++ b/tests/e2e/initialization/export.go @@ -0,0 +1,27 @@ +package initialization + +import "fmt" + +type ChainMeta struct { + DataDir string `json:"dataDir"` + ID string `json:"id"` +} + +type Node struct { + Name string `json:"name"` + ConfigDir string `json:"configDir"` + Mnemonic string `json:"mnemonic"` + PublicAddress string `json:"publicAddress"` + PublicKey string `json:"publicKey"` + PeerID string `json:"peerId"` + IsValidator bool `json:"isValidator"` +} + +type Chain struct { + ChainMeta ChainMeta `json:"chainMeta"` + Nodes []*Node `json:"validators"` +} + +func (c *ChainMeta) configDir() string { + return fmt.Sprintf("%s/%s", c.DataDir, c.ID) +} diff --git a/tests/e2e/initialization/init.go b/tests/e2e/initialization/init.go new file mode 100644 index 000000000..90bf6b814 --- /dev/null +++ b/tests/e2e/initialization/init.go @@ -0,0 +1,114 @@ +package initialization + +import ( + "errors" + "fmt" + "path/filepath" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/classic-terra/core/v2/tests/e2e/util" + coreutil "github.com/classic-terra/core/v2/types/util" +) + +func init() { + SetAddressPrefixes() +} + +// SetAddressPrefixes builds the Config with Bech32 addressPrefix and publKeyPrefix for accounts, validators, and consensus nodes and verifies that addreeses have correct format. +func SetAddressPrefixes() { + config := sdk.GetConfig() + config.SetBech32PrefixForAccount(coreutil.Bech32PrefixAccAddr, coreutil.Bech32PrefixAccPub) + config.SetBech32PrefixForValidator(coreutil.Bech32PrefixValAddr, coreutil.Bech32PrefixValPub) + config.SetBech32PrefixForConsensusNode(coreutil.Bech32PrefixConsAddr, coreutil.Bech32PrefixConsPub) + + // This is copied from the cosmos sdk v0.43.0-beta1 + // source: https://github.com/cosmos/cosmos-sdk/blob/v0.43.0-beta1/types/address.go#L141 + config.SetAddressVerifier(func(bytes []byte) error { + if len(bytes) == 0 { + return sdkerrors.Wrap(sdkerrors.ErrUnknownAddress, "addresses cannot be empty") + } + + if len(bytes) > address.MaxAddrLen { + return sdkerrors.Wrapf(sdkerrors.ErrUnknownAddress, "address max length is %d, got %d", address.MaxAddrLen, len(bytes)) + } + + // TODO: Do we want to allow addresses of lengths other than 20 and 32 bytes? + if len(bytes) != 20 && len(bytes) != 32 { + return sdkerrors.Wrapf(sdkerrors.ErrUnknownAddress, "address length must be 20 or 32 bytes, got %d", len(bytes)) + } + + return nil + }) +} + +func InitChain(id, dataDir string, nodeConfigs []*NodeConfig, forkHeight int) (*Chain, error) { + chain, err := new(id, dataDir) + if err != nil { + return nil, err + } + + for _, nodeConfig := range nodeConfigs { + newNode, err := newNode(chain, nodeConfig) + if err != nil { + return nil, err + } + chain.nodes = append(chain.nodes, newNode) + } + + if err := initGenesis(chain, forkHeight); err != nil { + return nil, err + } + + var peers []string + for _, peer := range chain.nodes { + peerID := fmt.Sprintf("%s@%s:26656", peer.getNodeKey().ID(), peer.moniker) + peer.peerID = peerID + peers = append(peers, peerID) + } + + for _, node := range chain.nodes { + if node.isValidator { + if err := node.initNodeConfigs(peers); err != nil { + return nil, err + } + } + } + return chain.export(), nil +} + +func InitSingleNode(chainID, dataDir string, existingGenesisDir string, nodeConfig *NodeConfig, trustHeight int64, trustHash string, stateSyncRPCServers []string, persistentPeers []string) (*Node, error) { + if nodeConfig.IsValidator { + return nil, errors.New("creating individual validator nodes after starting up chain is not currently supported") + } + + chain, err := new(chainID, dataDir) + if err != nil { + return nil, err + } + + newNode, err := newNode(chain, nodeConfig) + if err != nil { + return nil, err + } + + _, err = util.CopyFile( + existingGenesisDir, + filepath.Join(newNode.configDir(), "config", "genesis.json"), + ) + if err != nil { + return nil, err + } + + if err := newNode.initNodeConfigs(persistentPeers); err != nil { + return nil, err + } + + if err := newNode.initStateSyncConfig(trustHeight, trustHash, stateSyncRPCServers); err != nil { + return nil, err + } + + return newNode.export(), nil +} diff --git a/tests/e2e/initialization/init_test.go b/tests/e2e/initialization/init_test.go new file mode 100644 index 000000000..4796de6af --- /dev/null +++ b/tests/e2e/initialization/init_test.go @@ -0,0 +1,140 @@ +package initialization_test + +import ( + "fmt" + "os" + "path" + "path/filepath" + "testing" + + "github.com/classic-terra/core/v2/tests/e2e/initialization" + "github.com/stretchr/testify/require" +) + +const forkHeight = 10 + +var expectedConfigFiles = []string{ + "app.toml", "config.toml", "genesis.json", "node_key.json", "priv_validator_key.json", +} + +// TestChainInit tests that chain initialization correctly initializes a full chain +// and produces the desired output with genesis, chain and validator configs. +func TestChainInit(t *testing.T) { + const id = initialization.ChainAID + + var ( + nodeConfigs = []*initialization.NodeConfig{ + { + Name: "0", + Pruning: "default", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, + }, + { + Name: "1", + Pruning: "nothing", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 100, + SnapshotKeepRecent: 1, + IsValidator: false, + }, + } + dataDir, err = os.MkdirTemp("", "terra-e2e-testnet-test") + ) + require.NoError(t, err) + + chain, err := initialization.InitChain(id, dataDir, nodeConfigs, forkHeight) + require.NoError(t, err) + + require.Equal(t, chain.ChainMeta.DataDir, dataDir) + require.Equal(t, chain.ChainMeta.ID, id) + + require.Equal(t, len(nodeConfigs), len(chain.Nodes)) + + actualNodes := chain.Nodes + + for i, expectedConfig := range nodeConfigs { + actualNode := actualNodes[i] + + validateNode(t, id, dataDir, expectedConfig, actualNode) + } +} + +// TestSingleNodeInit tests that node initialization correctly initializes a single node +// and produces the desired output with genesis, chain and validator config. +func TestSingleNodeInit(t *testing.T) { + const ( + id = initialization.ChainAID + ) + + var ( + existingChainNodeConfigs = []*initialization.NodeConfig{ + { + Name: "0", + Pruning: "default", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, + }, + { + Name: "1", + Pruning: "nothing", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 100, + SnapshotKeepRecent: 1, + IsValidator: true, + }, + } + expectedConfig = &initialization.NodeConfig{ + Name: "2", + Pruning: "everything", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 100, + SnapshotKeepRecent: 1, + IsValidator: false, + } + dataDir, err = os.MkdirTemp("", "terra-e2e-testnet-test") + ) + require.NoError(t, err) + + // Setup + existingChain, err := initialization.InitChain(id, dataDir, existingChainNodeConfigs, forkHeight) + require.NoError(t, err) + + actualNode, err := initialization.InitSingleNode(existingChain.ChainMeta.ID, dataDir, filepath.Join(existingChain.Nodes[0].ConfigDir, "config", "genesis.json"), expectedConfig, 3, "testHash", []string{"some server"}, []string{"some server"}) + require.NoError(t, err) + + validateNode(t, id, dataDir, expectedConfig, actualNode) +} + +func validateNode(t *testing.T, chainID string, dataDir string, expectedConfig *initialization.NodeConfig, actualNode *initialization.Node) { + require.Equal(t, fmt.Sprintf("%s-node-%s", chainID, expectedConfig.Name), actualNode.Name) + require.Equal(t, expectedConfig.IsValidator, actualNode.IsValidator) + + expectedPath := fmt.Sprintf("%s/%s/%s-node-%s", dataDir, chainID, chainID, expectedConfig.Name) + + require.Equal(t, expectedPath, actualNode.ConfigDir) + + require.NotEmpty(t, actualNode.Mnemonic) + require.NotEmpty(t, actualNode.PublicAddress) + + if expectedConfig.IsValidator { + require.NotEmpty(t, actualNode.PeerID) + } + + for _, expectedFileName := range expectedConfigFiles { + expectedFilePath := path.Join(expectedPath, "config", expectedFileName) + _, err := os.Stat(expectedFilePath) + require.NoError(t, err) + } + _, err := os.Stat(path.Join(expectedPath, "keyring-test")) + require.NoError(t, err) +} diff --git a/tests/e2e/initialization/node.go b/tests/e2e/initialization/node.go new file mode 100644 index 000000000..b5c41693a --- /dev/null +++ b/tests/e2e/initialization/node.go @@ -0,0 +1,429 @@ +package initialization + +import ( + "encoding/json" + "fmt" + "os" + "path" + "path/filepath" + "strings" + + sdkcrypto "github.com/cosmos/cosmos-sdk/crypto" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/server" + srvconfig "github.com/cosmos/cosmos-sdk/server/config" + sdk "github.com/cosmos/cosmos-sdk/types" + sdktx "github.com/cosmos/cosmos-sdk/types/tx" + txsigning "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/cosmos/cosmos-sdk/x/genutil" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/cosmos/go-bip39" + "github.com/spf13/viper" + tmconfig "github.com/tendermint/tendermint/config" + tmos "github.com/tendermint/tendermint/libs/os" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/privval" + tmtypes "github.com/tendermint/tendermint/types" + + terraApp "github.com/classic-terra/core/v2/app" + "github.com/classic-terra/core/v2/tests/e2e/util" +) + +type internalNode struct { + chain *internalChain + moniker string + mnemonic string + keyInfo keyring.Record + privateKey cryptotypes.PrivKey + consensusKey privval.FilePVKey + nodeKey p2p.NodeKey + peerID string + isValidator bool +} + +func newNode(chain *internalChain, nodeConfig *NodeConfig) (*internalNode, error) { + node := &internalNode{ + chain: chain, + moniker: fmt.Sprintf("%s-node-%s", chain.chainMeta.ID, nodeConfig.Name), + isValidator: nodeConfig.IsValidator, + } + // generate genesis files + if err := node.init(); err != nil { + return nil, err + } + // create keys + if err := node.createKey(ValidatorWalletName); err != nil { + return nil, err + } + + if err := node.createNodeKey(); err != nil { + return nil, err + } + if err := node.createConsensusKey(); err != nil { + return nil, err + } + node.createAppConfig(nodeConfig) + return node, nil +} + +func (n *internalNode) configDir() string { + return fmt.Sprintf("%s/%s", n.chain.chainMeta.configDir(), n.moniker) +} + +func (n *internalNode) buildCreateValidatorMsg(amount sdk.Coin) (sdk.Msg, error) { + description := stakingtypes.NewDescription(n.moniker, "", "", "", "") + commissionRates := stakingtypes.CommissionRates{ + Rate: sdk.MustNewDecFromStr("0.1"), + MaxRate: sdk.MustNewDecFromStr("0.2"), + MaxChangeRate: sdk.MustNewDecFromStr("0.01"), + } + + // get the initial validator min self delegation + minSelfDelegation, _ := sdk.NewIntFromString("1") + + valPubKey, err := cryptocodec.FromTmPubKeyInterface(n.consensusKey.PubKey) + if err != nil { + return nil, err + } + accAdd, err := n.keyInfo.GetAddress() + if err != nil { + return nil, err + } + return stakingtypes.NewMsgCreateValidator( + sdk.ValAddress(accAdd), + valPubKey, + amount, + description, + commissionRates, + minSelfDelegation, + ) +} + +func (n *internalNode) createConfig() error { + p := path.Join(n.configDir(), "config") + return os.MkdirAll(p, 0o755) +} + +func (n *internalNode) createAppConfig(nodeConfig *NodeConfig) { + // set application configuration + appCfgPath := filepath.Join(n.configDir(), "config", "app.toml") + + appConfig := srvconfig.DefaultConfig() + appConfig.BaseConfig.Pruning = nodeConfig.Pruning + appConfig.BaseConfig.PruningKeepRecent = nodeConfig.PruningKeepRecent + appConfig.BaseConfig.PruningInterval = nodeConfig.PruningInterval + appConfig.API.Enable = true + appConfig.MinGasPrices = fmt.Sprintf("%s%s", MinGasPrice, TerraDenom) + appConfig.StateSync.SnapshotInterval = nodeConfig.SnapshotInterval + appConfig.StateSync.SnapshotKeepRecent = nodeConfig.SnapshotKeepRecent + + srvconfig.WriteConfigFile(appCfgPath, appConfig) +} + +func (n *internalNode) createNodeKey() error { + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + + config.SetRoot(n.configDir()) + config.Moniker = n.moniker + + nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + if err != nil { + return err + } + + n.nodeKey = *nodeKey + return nil +} + +func (n *internalNode) createConsensusKey() error { + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + + config.SetRoot(n.configDir()) + config.Moniker = n.moniker + + pvKeyFile := config.PrivValidatorKeyFile() + if err := tmos.EnsureDir(filepath.Dir(pvKeyFile), 0o777); err != nil { + return err + } + + pvStateFile := config.PrivValidatorStateFile() + if err := tmos.EnsureDir(filepath.Dir(pvStateFile), 0o777); err != nil { + return err + } + + filePV := privval.LoadOrGenFilePV(pvKeyFile, pvStateFile) + n.consensusKey = filePV.Key + + return nil +} + +func (n *internalNode) createKeyFromMnemonic(name, mnemonic string) error { + kb, err := keyring.New(keyringAppName, keyring.BackendTest, n.configDir(), nil, util.Cdc) + if err != nil { + return err + } + + keyringAlgos, _ := kb.SupportedAlgorithms() + algo, err := keyring.NewSigningAlgoFromString(string(hd.Secp256k1Type), keyringAlgos) + if err != nil { + return err + } + + info, err := kb.NewAccount(name, mnemonic, "", "44'/330'/0'/0/0", algo) + if err != nil { + return err + } + + if err != nil { + return err + } + + privKeyArmor, err := kb.ExportPrivKeyArmor(name, keyringPassphrase) + if err != nil { + return err + } + + privKey, _, err := sdkcrypto.UnarmorDecryptPrivKey(privKeyArmor, keyringPassphrase) + if err != nil { + return err + } + + n.keyInfo = *info + n.mnemonic = mnemonic + n.privateKey = privKey + + return nil +} + +func (n *internalNode) createKey(name string) error { + mnemonic, err := n.createMnemonic() + if err != nil { + return err + } + + return n.createKeyFromMnemonic(name, mnemonic) +} + +func (n *internalNode) export() *Node { + accadd, _ := n.keyInfo.GetAddress() + pubKey, _ := n.keyInfo.GetPubKey() + return &Node{ + Name: n.moniker, + ConfigDir: n.configDir(), + Mnemonic: n.mnemonic, + PublicAddress: accadd.String(), + PublicKey: pubKey.Address().String(), + PeerID: n.peerID, + IsValidator: n.isValidator, + } +} + +func (n *internalNode) getNodeKey() *p2p.NodeKey { + return &n.nodeKey +} + +func (n *internalNode) getGenesisDoc() (*tmtypes.GenesisDoc, error) { + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + config.SetRoot(n.configDir()) + + genFile := config.GenesisFile() + doc := &tmtypes.GenesisDoc{} + + if _, err := os.Stat(genFile); err != nil { + if !os.IsNotExist(err) { + return nil, err + } + } else { + var err error + + doc, err = tmtypes.GenesisDocFromFile(genFile) + if err != nil { + return nil, fmt.Errorf("failed to read genesis doc from file: %w", err) + } + } + + return doc, nil +} + +func (n *internalNode) init() error { + if err := n.createConfig(); err != nil { + return err + } + + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + + config.SetRoot(n.configDir()) + config.Moniker = n.moniker + + genDoc, err := n.getGenesisDoc() + if err != nil { + return err + } + + appState, err := json.MarshalIndent(terraApp.ModuleBasics.DefaultGenesis(util.Cdc), "", " ") + if err != nil { + return fmt.Errorf("failed to JSON encode app genesis state: %w", err) + } + + genDoc.ChainID = n.chain.chainMeta.ID + genDoc.Validators = nil + genDoc.AppState = appState + + if err = genutil.ExportGenesisFile(genDoc, config.GenesisFile()); err != nil { + return fmt.Errorf("failed to export app genesis state: %w", err) + } + + tmconfig.WriteConfigFile(filepath.Join(config.RootDir, "config", "config.toml"), config) + return nil +} + +func (n *internalNode) createMnemonic() (string, error) { + entropySeed, err := bip39.NewEntropy(256) + if err != nil { + return "", err + } + + mnemonic, err := bip39.NewMnemonic(entropySeed) + if err != nil { + return "", err + } + + return mnemonic, nil +} + +func (n *internalNode) initNodeConfigs(persistentPeers []string) error { + tmCfgPath := filepath.Join(n.configDir(), "config", "config.toml") + + vpr := viper.New() + vpr.SetConfigFile(tmCfgPath) + if err := vpr.ReadInConfig(); err != nil { + return err + } + + valConfig := tmconfig.DefaultConfig() + if err := vpr.Unmarshal(valConfig); err != nil { + return err + } + + valConfig.P2P.ListenAddress = "tcp://0.0.0.0:26656" + valConfig.P2P.AddrBookStrict = false + valConfig.P2P.ExternalAddress = fmt.Sprintf("%s:%d", n.moniker, 26656) + valConfig.RPC.ListenAddress = "tcp://0.0.0.0:26657" + valConfig.StateSync.Enable = false + valConfig.LogLevel = "info" + valConfig.P2P.PersistentPeers = strings.Join(persistentPeers, ",") + valConfig.Storage.DiscardABCIResponses = true + + tmconfig.WriteConfigFile(tmCfgPath, valConfig) + return nil +} + +func (n *internalNode) initStateSyncConfig(trustHeight int64, trustHash string, stateSyncRPCServers []string) error { + tmCfgPath := filepath.Join(n.configDir(), "config", "config.toml") + + vpr := viper.New() + vpr.SetConfigFile(tmCfgPath) + if err := vpr.ReadInConfig(); err != nil { + return err + } + + valConfig := tmconfig.DefaultConfig() + if err := vpr.Unmarshal(valConfig); err != nil { + return err + } + + valConfig.StateSync = tmconfig.DefaultStateSyncConfig() + valConfig.StateSync.Enable = true + valConfig.StateSync.TrustHeight = trustHeight + valConfig.StateSync.TrustHash = trustHash + valConfig.StateSync.RPCServers = stateSyncRPCServers + + tmconfig.WriteConfigFile(tmCfgPath, valConfig) + return nil +} + +// signMsg returns a signed tx of the provided messages, +// signed by the validator, using 0 fees, a high gas limit, and a common memo. +func (n *internalNode) signMsg(msgs ...sdk.Msg) (*sdktx.Tx, error) { + txBuilder := util.EncodingConfig.TxConfig.NewTxBuilder() + + if err := txBuilder.SetMsgs(msgs...); err != nil { + return nil, err + } + + txBuilder.SetMemo(fmt.Sprintf("%s@%s:26656", n.nodeKey.ID(), n.moniker)) + txBuilder.SetFeeAmount(sdk.NewCoins()) + txBuilder.SetGasLimit(uint64(200000 * len(msgs))) + + // TODO: Find a better way to sign this tx with less code. + signerData := authsigning.SignerData{ + ChainID: n.chain.chainMeta.ID, + AccountNumber: 0, + Sequence: 0, + } + + // For SIGN_MODE_DIRECT, calling SetSignatures calls setSignerInfos on + // TxBuilder under the hood, and SignerInfos is needed to generate the sign + // bytes. This is the reason for setting SetSignatures here, with a nil + // signature. + // + // Note: This line is not needed for SIGN_MODE_LEGACY_AMINO, but putting it + // also doesn't affect its generated sign bytes, so for code's simplicity + // sake, we put it here. + pubKey, _ := n.keyInfo.GetPubKey() + sig := txsigning.SignatureV2{ + PubKey: pubKey, + Data: &txsigning.SingleSignatureData{ + SignMode: txsigning.SignMode_SIGN_MODE_DIRECT, + Signature: nil, + }, + Sequence: 0, + } + + if err := txBuilder.SetSignatures(sig); err != nil { + return nil, err + } + + bytesToSign, err := util.EncodingConfig.TxConfig.SignModeHandler().GetSignBytes( + txsigning.SignMode_SIGN_MODE_DIRECT, + signerData, + txBuilder.GetTx(), + ) + if err != nil { + return nil, err + } + + sigBytes, err := n.privateKey.Sign(bytesToSign) + if err != nil { + return nil, err + } + + pubKey, _ = n.keyInfo.GetPubKey() + sig = txsigning.SignatureV2{ + PubKey: pubKey, + Data: &txsigning.SingleSignatureData{ + SignMode: txsigning.SignMode_SIGN_MODE_DIRECT, + Signature: sigBytes, + }, + Sequence: 0, + } + if err := txBuilder.SetSignatures(sig); err != nil { + return nil, err + } + + signedTx := txBuilder.GetTx() + bz, err := util.EncodingConfig.TxConfig.TxEncoder()(signedTx) + if err != nil { + return nil, err + } + + return decodeTx(bz) +} diff --git a/tests/e2e/initialization/node/main.go b/tests/e2e/initialization/node/main.go new file mode 100644 index 000000000..26431022c --- /dev/null +++ b/tests/e2e/initialization/node/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "encoding/json" + "flag" + "os" + "strings" + + "github.com/classic-terra/core/v2/tests/e2e/initialization" +) + +func main() { + var ( + nodeConfigStr string + + dataDir string + + existingGenesisDir string + + chainID string + + stateSyncRPCServersStr string + + persistentPeersStr string + + trustHeight int64 + + trustHash string + ) + + flag.StringVar(&dataDir, "data-dir", "", "chain data directory") + flag.StringVar(&existingGenesisDir, "genesis-dir", "", "pre-existing genesis location") + flag.StringVar(&chainID, "chain-id", "", "chain ID") + flag.StringVar(&nodeConfigStr, "node-config", "", "serialized node config") + flag.StringVar(&stateSyncRPCServersStr, "rpc-servers", "", "state sync RPC servers") + flag.StringVar(&persistentPeersStr, "peers", "", "state sync RPC servers") + flag.Int64Var(&trustHeight, "trust-height", 0, "trust Height") + flag.StringVar(&trustHash, "trust-hash", "", "trust hash") + + flag.Parse() + + if len(dataDir) == 0 { + panic("data-dir is required") + } + + var nodeConfig initialization.NodeConfig + err := json.Unmarshal([]byte(nodeConfigStr), &nodeConfig) + if err != nil { + panic(err) + } + + stateSyncRPCServers := strings.Split(stateSyncRPCServersStr, ",") + if len(stateSyncRPCServers) == 0 { + panic("rpc-servers is required, separated by commas") + } + + persistenrPeers := strings.Split(persistentPeersStr, ",") + if len(persistenrPeers) == 0 { + panic("persistent peers are required, separated by commas") + } + + if err := os.MkdirAll(dataDir, 0o755); err != nil { + panic(err) + } + + _, err = initialization.InitSingleNode(chainID, dataDir, existingGenesisDir, &nodeConfig, trustHeight, trustHash, stateSyncRPCServers, persistenrPeers) + if err != nil { + panic(err) + } +} diff --git a/tests/e2e/initialization/util.go b/tests/e2e/initialization/util.go new file mode 100644 index 000000000..d723855a8 --- /dev/null +++ b/tests/e2e/initialization/util.go @@ -0,0 +1,47 @@ +package initialization + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec/unknownproto" + sdktx "github.com/cosmos/cosmos-sdk/types/tx" + + "github.com/classic-terra/core/v2/tests/e2e/util" +) + +func decodeTx(txBytes []byte) (*sdktx.Tx, error) { + var raw sdktx.TxRaw + + // reject all unknown proto fields in the root TxRaw + err := unknownproto.RejectUnknownFieldsStrict(txBytes, &raw, util.EncodingConfig.InterfaceRegistry) + if err != nil { + return nil, fmt.Errorf("failed to reject unknown fields: %w", err) + } + + if err := util.Cdc.Unmarshal(txBytes, &raw); err != nil { + return nil, err + } + + var body sdktx.TxBody + if err := util.Cdc.Unmarshal(raw.BodyBytes, &body); err != nil { + return nil, fmt.Errorf("failed to decode tx: %w", err) + } + + var authInfo sdktx.AuthInfo + + // reject all unknown proto fields in AuthInfo + err = unknownproto.RejectUnknownFieldsStrict(raw.AuthInfoBytes, &authInfo, util.EncodingConfig.InterfaceRegistry) + if err != nil { + return nil, fmt.Errorf("failed to reject unknown fields: %w", err) + } + + if err := util.Cdc.Unmarshal(raw.AuthInfoBytes, &authInfo); err != nil { + return nil, fmt.Errorf("failed to decode auth info: %w", err) + } + + return &sdktx.Tx{ + Body: &body, + AuthInfo: &authInfo, + Signatures: raw.Signatures, + }, nil +} diff --git a/tests/e2e/scripts/add_burn_tax_exemption_address_proposal.json b/tests/e2e/scripts/add_burn_tax_exemption_address_proposal.json new file mode 100644 index 000000000..c32934964 --- /dev/null +++ b/tests/e2e/scripts/add_burn_tax_exemption_address_proposal.json @@ -0,0 +1 @@ +{"title":"Add Burn Tax Exemption Address","description":"Add terra1r6xhjf82szvu476gjqu0c5lsz4n3gtc4cuqszw,terra1sju803llkgg9xpkwr438s2x375dzeknf4sa4gr to the burn tax exemption address list","addresses":["terra1r6xhjf82szvu476gjqu0c5lsz4n3gtc4cuqszw","terra1sju803llkgg9xpkwr438s2x375dzeknf4sa4gr"]} \ No newline at end of file diff --git a/tests/e2e/scripts/counter.wasm b/tests/e2e/scripts/counter.wasm new file mode 100644 index 000000000..5afd3e0cc Binary files /dev/null and b/tests/e2e/scripts/counter.wasm differ diff --git a/tests/e2e/scripts/hermes_bootstrap.sh b/tests/e2e/scripts/hermes_bootstrap.sh new file mode 100644 index 000000000..0402f93ea --- /dev/null +++ b/tests/e2e/scripts/hermes_bootstrap.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +set -ex + +# initialize Hermes relayer configuration +mkdir -p /root/.hermes/ +touch /root/.hermes/config.toml + +# setup Hermes relayer configuration +tee /root/.hermes/config.toml < /dev/null)" != "" ]]; then + return 1 + fi + return 0 +} + +# check_if_exists returns 1 if an "terra" image is built from the same commit SHA +# as the current commit, 0 otherwise. +# It assummes that the "terra" image was specifically tagged with Git SHA at build +# time. Please see "docker-build-debug" Makefile step for details. +check_if_up_to_date() { + sha_from_image=$LIST_DOCKER_IMAGE_HASHES + local_git_sha=$(git rev-parse HEAD) + echo "Local Git Commit SHA: $local_git_sha" + for cur_image_sha in $sha_from_image; do + echo "Found Docker Tag Git SHA : $cur_image_sha" + if [[ "$cur_image_sha" == "$local_git_sha" ]]; then + return 1 + fi + done + return 0 +} + +check_if_exists +exists=$? + +if [[ "$exists" -eq 1 ]]; then + echo "terra:debug image found" + + check_if_up_to_date + up_to_date=$? + + if [[ "$up_to_date" -eq 1 ]]; then + echo "terra:debug image is up to date; nothing is done" + exit 0 + else + echo "terra:debug image is not up to date; rebuilding" + fi +else + echo "terra:debug image not found; building" +fi + +# Rebuild the image +make docker-build-debug + +check_if_up_to_date diff --git a/tests/e2e/scripts/run/common.sh b/tests/e2e/scripts/run/common.sh new file mode 100755 index 000000000..e1dcd3c70 --- /dev/null +++ b/tests/e2e/scripts/run/common.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# N.B.: We match all tags but "Debug" and semantic version tags such as "V10". These are the only +# tags we support. As a result, the only remaining tag is the Git SHA tag. +LIST_DOCKER_IMAGE_HASHES=$(docker images terra --format "{{ title .Tag }}" | awk '!/Debug/ && !/V[0-9-]+/' | awk '{print tolower($0)}') diff --git a/tests/e2e/scripts/run/remove_stale_resources.sh b/tests/e2e/scripts/run/remove_stale_resources.sh new file mode 100755 index 000000000..90938d1b0 --- /dev/null +++ b/tests/e2e/scripts/run/remove_stale_resources.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +source $(dirname $0)/common.sh + +# Filtering by containers belonging to the "terra-testnet" network +LIST_CONTAINERS_CMD=$(docker ps -a --filter network=terra-testnet --format {{.ID}}) +LIST_NETWORKS_CMD=$(docker network ls --filter name=terra-testnet --format {{.ID}}) + +if [[ "$LIST_CONTAINERS_CMD" != "" ]]; then + echo "Removing stale e2e containers" + docker container rm -f $LIST_CONTAINERS_CMD +else + echo "No stale e2e containers found" +fi + +if [[ "$LIST_NETWORKS_CMD" != "" ]]; then + echo "Removing stale e2e networks" + docker network rm $LIST_NETWORKS_CMD +else + echo "No stale e2e networks found" +fi + +local_git_sha=$(git rev-parse HEAD) +for cur_image_sha in $LIST_DOCKER_IMAGE_HASHES; do + if [[ "$cur_image_sha" != "$local_git_sha" ]]; then + echo "Removing stale e2e image with SHA $cur_image_sha" + docker rmi -f $(docker images --filter=reference="terra:$cur_image_sha" -q) + fi +done diff --git a/tests/e2e/util/codec.go b/tests/e2e/util/codec.go new file mode 100644 index 000000000..6307a7163 --- /dev/null +++ b/tests/e2e/util/codec.go @@ -0,0 +1,40 @@ +package util + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + terraApp "github.com/classic-terra/core/v2/app" + "github.com/classic-terra/core/v2/app/params" +) + +var ( + EncodingConfig params.EncodingConfig + Cdc codec.Codec +) + +func init() { + EncodingConfig, Cdc = initEncodingConfigAndCdc() +} + +func initEncodingConfigAndCdc() (params.EncodingConfig, codec.Codec) { + encodingConfig := terraApp.MakeEncodingConfig() + + encodingConfig.InterfaceRegistry.RegisterImplementations( + (*sdk.Msg)(nil), + &stakingtypes.MsgCreateValidator{}, + ) + encodingConfig.InterfaceRegistry.RegisterImplementations( + (*cryptotypes.PubKey)(nil), + &secp256k1.PubKey{}, + &ed25519.PubKey{}, + ) + + cdc := encodingConfig.Marshaler + + return encodingConfig, cdc +} diff --git a/tests/e2e/util/io.go b/tests/e2e/util/io.go new file mode 100644 index 000000000..84b8387b2 --- /dev/null +++ b/tests/e2e/util/io.go @@ -0,0 +1,42 @@ +package util + +import ( + "fmt" + "io" + "os" +) + +func CopyFile(src, dst string) (int64, error) { + sourceFileStat, err := os.Stat(src) + if err != nil { + return 0, err + } + + if !sourceFileStat.Mode().IsRegular() { + return 0, fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return 0, err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return 0, err + } + defer destination.Close() + + nBytes, err := io.Copy(destination, source) + return nBytes, err +} + +func WritePublicFile(path string, body []byte) error { + _, err := os.Create(path) + if err != nil { + return err + } + + return os.WriteFile(path, body, 0o600) +}