diff --git a/.github/workflows/sim.yml b/.github/workflows/sim.yml index 04616183fa8..cf2b59c99dd 100644 --- a/.github/workflows/sim.yml +++ b/.github/workflows/sim.yml @@ -17,4 +17,5 @@ jobs: - name: Display go version run: go version - name: Run simulation - run: go test ./simapp + run: | + make test-sim diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ad4abcc0f1..15d47fc63ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,30 @@ jobs: - name: Display go version run: go version - name: Run all tests - run: go test -mod=readonly -timeout 30m -coverprofile=coverage.txt -tags='norace' -covermode=atomic `go list ./... | grep -v simapp` + run: | + make test-cover - name: Codecov uses: codecov/codecov-action@v1.5.2 + test-e2e: + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/setup-go@v2.2.0 + with: + go-version: 1.17 + - uses: actions/checkout@v2 + - uses: technote-space/get-diff-action@v6.0.1 + with: + PATTERNS: | + **/**.go + go.mod + go.sum + - name: Build Docker Image + run: | + make docker-build-debug + make docker-build-e2e-chain-init + if: env.GIT_DIFF + - name: Test E2E + run: | + make test-e2e + if: env.GIT_DIFF diff --git a/CHANGELOG.md b/CHANGELOG.md index 09afc73a9e6..8d72fd59533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Minor improvements & Bug Fixes * [#1177](https://github.com/osmosis-labs/osmosis/pull/1177) upgrade to go 1.18 +* [#1193](https://github.com/osmosis-labs/osmosis/pull/1193) Setup e2e tests on a single chain; add balances query test * [#1061](https://github.com/osmosis-labs/osmosis/pull/1061) upgrade iavl to v0.17.3-osmo-v5 with concurrent map write fix * [#1071](https://github.com/osmosis-labs/osmosis/pull/1071) improve Dockerfile diff --git a/Dockerfile b/Dockerfile index 022e5105bef..2a01c6639ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ # syntax=docker/dockerfile:1 +ARG BASE_IMG_TAG=nonroot + ## Build Image FROM golang:1.18-bullseye as build @@ -13,7 +15,7 @@ RUN sha256sum /lib/libwasmvm_muslc.a | grep d0152067a5609bfdfb3f0d5d6c0f2760f79d RUN BUILD_TAGS=muslc make build ## Deploy image -FROM gcr.io/distroless/base-debian11:nonroot +FROM gcr.io/distroless/base-debian11:${BASE_IMG_TAG} COPY --from=build /osmosis/build/osmosisd /bin/osmosisd @@ -22,6 +24,7 @@ WORKDIR $HOME EXPOSE 26656 EXPOSE 26657 -EXPOSE 1317 +EXPOSE 1317 ENTRYPOINT ["osmosisd"] +CMD [ "start" ] diff --git a/Makefile b/Makefile index 3afbefc9987..622be1491c3 100644 --- a/Makefile +++ b/Makefile @@ -115,6 +115,10 @@ build-contract-tests-hooks: mkdir -p $(BUILDDIR) go build -mod=readonly $(BUILD_FLAGS) -o $(BUILDDIR)/ ./cmd/contract_tests +build-e2e-chain-init: + mkdir -p $(BUILDDIR) + go build -mod=readonly $(BUILD_FLAGS) -o $(BUILDDIR)/ ./tests/e2e/chain_init + go-mod-cache: go.sum @echo "--> Download go modules to local cache" @go mod download @@ -210,6 +214,11 @@ sync-docs: ### Tests & Simulation ### ############################################################################### +PACKAGES_UNIT=$(shell go list ./... | grep -E -v 'simapp|e2e') +PACKAGES_E2E=$(shell go list ./... | grep '/e2e') +PACKAGES_SIM=$(shell go list ./... | grep '/simapp') +TEST_PACKAGES=./... + include sims.mk test: test-unit test-build @@ -217,17 +226,28 @@ test: test-unit test-build test-all: check test-race test-cover test-unit: - @VERSION=$(VERSION) go test -mod=readonly -tags='ledger test_ledger_mock norace' ./... + @VERSION=$(VERSION) go test -mod=readonly -tags='ledger test_ledger_mock norace' $(PACKAGES_UNIT) test-race: - @VERSION=$(VERSION) go test -mod=readonly -race -tags='ledger test_ledger_mock' ./... + @VERSION=$(VERSION) go test -mod=readonly -race -tags='ledger test_ledger_mock' $(PACKAGES_UNIT) test-cover: - @go test -mod=readonly -timeout 30m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock' ./... + @VERSION=$(VERSION) go test -mod=readonly -timeout 30m -coverprofile=coverage.txt -tags='norace' -covermode=atomic $(PACKAGES_UNIT) + +test-sim: + @VERSION=$(VERSION) go test -mod=readonly $(PACKAGES_SIM) + +test-e2e: + @VERSION=$(VERSION) go test -mod=readonly -timeout=25m -v $(PACKAGES_E2E) benchmark: - @go test -mod=readonly -bench=. ./... + @go test -mod=readonly -bench=. $(PACKAGES_UNIT) + +docker-build-debug: + @docker build -t osmosis:debug --build-arg BASE_IMG_TAG=debug -f Dockerfile . +docker-build-e2e-chain-init: + @docker build -t osmosis-e2e-chain-init:debug -f tests/e2e/chain_init/chain-init.Dockerfile . ############################################################################### ### Linting ### diff --git a/go.mod b/go.mod index d3107b68fb0..9a5085c45c6 100644 --- a/go.mod +++ b/go.mod @@ -36,12 +36,15 @@ require ( github.com/99designs/keyring v1.1.6 // indirect github.com/Antonboom/errname v0.1.5 // indirect github.com/Antonboom/nilnil v0.1.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/BurntSushi/toml v1.1.0 // indirect github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect github.com/CosmWasm/wasmvm v1.0.0-beta5 // indirect github.com/DataDog/zstd v1.4.5 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect github.com/Masterminds/semver v1.5.0 // indirect + github.com/Microsoft/go-winio v0.5.1 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/OpenPeeDeeP/depguard v1.1.0 // indirect github.com/Workiva/go-datastructures v1.0.53 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect @@ -57,12 +60,14 @@ require ( github.com/breml/errchkjson v0.2.3 // indirect github.com/btcsuite/btcd v0.22.0-beta // indirect github.com/butuzov/ireturn v0.1.1 // indirect + github.com/cenkalti/backoff/v4 v4.1.2 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/charithe/durationcheck v0.0.9 // indirect github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect github.com/coinbase/rosetta-sdk-go v0.7.0 // indirect github.com/confio/ics23/go v0.6.6 // indirect + github.com/containerd/continuity v0.2.1 // indirect github.com/cosmos/btcutil v1.0.4 // indirect github.com/cosmos/ledger-cosmos-go v0.11.1 // indirect github.com/cosmos/ledger-go v0.9.2 // indirect @@ -74,6 +79,10 @@ require ( github.com/dgraph-io/badger/v2 v2.2007.3 // indirect github.com/dgraph-io/ristretto v0.0.3 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/docker/cli v20.10.11+incompatible // indirect + github.com/docker/docker v20.10.7+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b // indirect github.com/esimonov/ifshort v1.0.4 // indirect @@ -114,6 +123,7 @@ require ( github.com/google/go-cmp v0.5.7 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/orderedcode v0.0.1 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect @@ -133,6 +143,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/hdevalence/ed25519consensus v0.0.0-20210204194344-59a8610d2b87 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/imdario/mergo v0.3.12 // indirect github.com/improbable-eng/grpc-web v0.14.1 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jgautheron/goconst v1.5.1 // indirect @@ -165,6 +176,7 @@ require ( github.com/minio/highwayhash v1.0.2 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect github.com/moricho/tparallel v0.2.1 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/nakabonne/nestif v0.3.1 // indirect @@ -172,6 +184,10 @@ require ( github.com/nishanths/exhaustive v0.7.11 // indirect github.com/nishanths/predeclared v0.2.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.0.3 // indirect + github.com/ory/dockertest/v3 v3.8.1 github.com/pelletier/go-toml v1.9.4 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect @@ -200,7 +216,7 @@ require ( github.com/sourcegraph/go-diff v0.6.1 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/viper v1.10.1 // indirect + github.com/spf13/viper v1.10.1 github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stretchr/objx v0.3.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect @@ -217,6 +233,9 @@ require ( github.com/ultraware/funlen v0.0.3 // indirect github.com/ultraware/whitespace v0.0.5 // indirect github.com/uudashr/gocognit v1.0.5 // 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 github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.1.1-0.20210918184747-d757024714a1 // indirect github.com/zondax/hid v0.9.0 // indirect diff --git a/go.sum b/go.sum index 9124a1a0bc3..5e8c1150c48 100644 --- a/go.sum +++ b/go.sum @@ -218,6 +218,8 @@ github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRt github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= +github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= @@ -284,6 +286,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/daixiang0/gci v0.3.3 h1:55xJKH7Gl9Vk6oQ1cMkwrDWjAkT1D+D1G9kNmRcAIY4= github.com/daixiang0/gci v0.3.3/go.mod h1:1Xr2bxnQbDxCqqulUOv8qpGqkgRw9RSCGGjEC2LjF8o= @@ -314,7 +318,11 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUn github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/cli v20.10.11+incompatible h1:tXU1ezXcruZQRrMP8RN2z9N91h+6egZTS1gsPsKantc= +github.com/docker/cli v20.10.11+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ= +github.com/docker/docker v20.10.7+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 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= @@ -565,6 +573,8 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +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/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -702,6 +712,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/improbable-eng/grpc-web v0.14.1 h1:NrN4PY71A6tAz2sKDvC5JCauENWp0ykG8Oq1H3cpFvw= github.com/improbable-eng/grpc-web v0.14.1/go.mod h1:zEjGHa8DAlkoOXmswrNvhUGEYQA9UI7DhrGeHR1DMGU= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -795,6 +807,7 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/leonklingele/grouper v1.1.0 h1:tC2y/ygPbMFSBOs3DcyaEMKnnwH7eYKzohOtRrf0SAg= github.com/leonklingele/grouper v1.1.0/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -884,6 +897,8 @@ github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= 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= @@ -989,6 +1004,8 @@ github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= +github.com/ory/dockertest/v3 v3.8.1 h1:vU/8d1We4qIad2YM0kOwRVtnyue7ExvacPiw1yDm17g= +github.com/ory/dockertest/v3 v3.8.1/go.mod h1:wSRQ3wmkz+uSARYMk7kVJFDBGm8x5gSxIhI7NDc+BAQ= github.com/osmosis-labs/bech32-ibc v0.2.0-rc2 h1:7xy1pLtNiF2KaRSkolayZf4z3OfCJsO3eqBtEAXg2VA= github.com/osmosis-labs/bech32-ibc v0.2.0-rc2/go.mod h1:0JCaioRNOVUiw7c3MngmKACnumaQ2sjPenXCnwxCttI= github.com/osmosis-labs/cosmos-sdk v0.45.1-0.20220427011259-3fa7fa458e54 h1:leVUW0Std8ZIYPPkJEMsZcRxgvFcaBBMNgX4Epn8XsI= @@ -1300,6 +1317,12 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17 github.com/vmihailenco/msgpack/v5 v5.1.4/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI= github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees= +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/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= @@ -1598,6 +1621,7 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1681,6 +1705,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-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1950,7 +1975,11 @@ 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 h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 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= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 00000000000..a3de35e60dd --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,71 @@ +# 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 Osmosis codebase. +It initializes the chains for testing via Docker files. As a result, the test suite may +provide the desired Osmosis version to Docker containers during the initialization. +This design allows for the opportunity of testing chain upgrades in the future by providing +an older Osmosis version to the container, performing the chain upgrade, and running the latest test suite. + +The file e2e_suite_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 is a single test in `e2e_test.go` to query the balances of a validator. + +## `chain` Package + +The `chain` 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 Osmosis codebase. + +## `upgrade` Package + +The `upgrade` package starts chain initialization. In addition, there is a Dockerfile `init-e2e.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 osmosis container to start the `osmosisd` service. + +The decoupling between chain initialization and start-up allows to minimize the differences between our test suite and the production environment. + +# Running Locally + +##### To build the binary that initializes the chain: + +``` +make build-e2e-chain-init +``` +- The produced binary is an entrypoint to the `osmosis-e2e-chain-init:debug` image. + +##### To build the image for initializing the chain (`osmosis-e2e-chain-init:debug`): + +``` +make docker-build-e2e-chain-init +``` + +##### To run the chain initialization container locally: + +``` +mkdir < path > +docker run -v < path >:/tmp/osmo-test osmosis-e2e-chain-init:debug --data-dir=/tmp/osmo-test +sudo rm -r < path > # must be root to clean up +``` +- runs a container with a volume mounted at < path > where all chain initialization files are placed. +- < path > must be absolute. +- `--data-dir` flag is needed for outputting the files into a directory inside the container + +Example: +``` +docker run -v /home/roman/cosmos/osmosis/tmp:/tmp/osmo-test osmosis-e2e-chain-init:debug --data-dir=/tmp/osmo-test +``` + +##### To build the debug Osmosis image: + +``` +make docker-build-e2e-debug +``` diff --git a/tests/e2e/chain/chain.go b/tests/e2e/chain/chain.go new file mode 100644 index 00000000000..a746bcc7440 --- /dev/null +++ b/tests/e2e/chain/chain.go @@ -0,0 +1,100 @@ +package chain + +import ( + "fmt" +) + +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 + validators []*internalValidator +} + +func new(id, dataDir string) (*internalChain, error) { + chainMeta := ChainMeta{ + Id: id, + DataDir: dataDir, + } + return &internalChain{ + chainMeta: chainMeta, + }, nil +} + +func (c *internalChain) createAndInitValidators(count int) error { + for i := 0; i < count; i++ { + node := c.createValidator(i) + + // generate genesis files + if err := node.init(); err != nil { + return err + } + + c.validators = append(c.validators, node) + + // create keys + if err := node.createKey("val"); err != nil { + return err + } + if err := node.createNodeKey(); err != nil { + return err + } + if err := node.createConsensusKey(); err != nil { + return err + } + } + + return nil +} + +func (c *internalChain) createAndInitValidatorsWithMnemonics(count int, mnemonics []string) error { + for i := 0; i < count; i++ { + // create node + node := c.createValidator(i) + + // generate genesis files + if err := node.init(); err != nil { + return err + } + + c.validators = append(c.validators, node) + + // create keys + if err := node.createKeyFromMnemonic("val", mnemonics[i]); err != nil { + return err + } + if err := node.createNodeKey(); err != nil { + return err + } + if err := node.createConsensusKey(); err != nil { + return err + } + } + + return nil +} + +func (c *internalChain) createValidator(index int) *internalValidator { + return &internalValidator{ + chain: c, + index: index, + moniker: fmt.Sprintf("%s-osmosis-%d", c.chainMeta.Id, index), + } +} + +func (c *internalChain) export() *Chain { + exportValidators := make([]*Validator, 0, len(c.validators)) + for _, v := range c.validators { + exportValidators = append(exportValidators, v.export()) + } + + return &Chain{ + ChainMeta: c.chainMeta, + Validators: exportValidators, + } +} diff --git a/tests/e2e/chain/chain_util.go b/tests/e2e/chain/chain_util.go new file mode 100644 index 00000000000..f780976e312 --- /dev/null +++ b/tests/e2e/chain/chain_util.go @@ -0,0 +1,47 @@ +package chain + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec/unknownproto" + sdktx "github.com/cosmos/cosmos-sdk/types/tx" + + "github.com/osmosis-labs/osmosis/v7/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/chain/config.go b/tests/e2e/chain/config.go new file mode 100644 index 00000000000..28bc3ba3016 --- /dev/null +++ b/tests/e2e/chain/config.go @@ -0,0 +1,300 @@ +package chain + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strings" + + "github.com/cosmos/cosmos-sdk/server" + srvconfig "github.com/cosmos/cosmos-sdk/server/config" + 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" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/spf13/viper" + tmconfig "github.com/tendermint/tendermint/config" + tmjson "github.com/tendermint/tendermint/libs/json" + + "github.com/osmosis-labs/osmosis/v7/tests/e2e/util" +) + +const ( + // common + OsmoDenom = "uosmo" + StakeDenom = "stake" + IbcDenom = "ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518" + MinGasPrice = "0.000" + IbcSendAmount = 3300000000 + // chainA + ChainAID = "osmo-test-a" + OsmoBalanceA = 200000000000 + StakeBalanceA = 110000000000 + StakeAmountA = 100000000000 + // chainB + ChainBID = "osmo-test-b" + OsmoBalanceB = 500000000000 + StakeBalanceB = 440000000000 + StakeAmountB = 400000000000 +) + +var ( + StakeAmountIntA = sdk.NewInt(StakeAmountA) + StakeAmountCoinA = sdk.NewCoin(StakeDenom, StakeAmountIntA) + StakeAmountIntB = sdk.NewInt(StakeAmountB) + StakeAmountCoinB = sdk.NewCoin(StakeDenom, StakeAmountIntB) + + InitBalanceStrA = fmt.Sprintf("%d%s,%d%s", OsmoBalanceA, OsmoDenom, StakeBalanceA, StakeDenom) + InitBalanceStrB = fmt.Sprintf("%d%s,%d%s", OsmoBalanceB, OsmoDenom, StakeBalanceB, StakeDenom) +) + +func addAccount(path, moniker, amountStr string, accAddr sdk.AccAddress) 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) + } + + balances := banktypes.Balance{Address: accAddr.String(), Coins: coins.Sort()} + genAccount := authtypes.NewBaseAccount(accAddr, nil, 0, 0) + + genFile := config.GenesisFile() + appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile) + if err != nil { + return fmt.Errorf("failed to unmarshal genesis state: %w", err) + } + + 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 initGenesis(c *internalChain) error { + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + + config.SetRoot(c.validators[0].configDir()) + config.Moniker = c.validators[0].getMoniker() + + genFilePath := config.GenesisFile() + appGenState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFilePath) + if err != nil { + return err + } + + var bankGenState banktypes.GenesisState + if err := util.Cdc.UnmarshalJSON(appGenState[banktypes.ModuleName], &bankGenState); err != nil { + return err + } + + bankGenState.DenomMetadata = append(bankGenState.DenomMetadata, banktypes.Metadata{ + Description: "An example stable token", + Display: OsmoDenom, + Base: OsmoDenom, + Symbol: OsmoDenom, + Name: OsmoDenom, + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: OsmoDenom, + Exponent: 0, + }, + }, + }) + + bz, err := util.Cdc.MarshalJSON(&bankGenState) + if err != nil { + return err + } + appGenState[banktypes.ModuleName] = bz + + var genUtilGenState genutiltypes.GenesisState + if err := util.Cdc.UnmarshalJSON(appGenState[genutiltypes.ModuleName], &genUtilGenState); err != nil { + return err + } + + // generate genesis txs + genTxs := make([]json.RawMessage, len(c.validators)) + for i, val := range c.validators { + stakeAmountCoin := StakeAmountCoinA + if c.chainMeta.Id != ChainAID { + stakeAmountCoin = StakeAmountCoinB + } + createValmsg, err := val.buildCreateValidatorMsg(stakeAmountCoin) + if err != nil { + return err + } + + signedTx, err := val.signMsg(createValmsg) + if err != nil { + return err + } + + txRaw, err := util.Cdc.MarshalJSON(signedTx) + if err != nil { + return err + } + + genTxs[i] = txRaw + } + + genUtilGenState.GenTxs = genTxs + + bz, err = util.Cdc.MarshalJSON(&genUtilGenState) + if err != nil { + return err + } + appGenState[genutiltypes.ModuleName] = bz + + bz, err = json.MarshalIndent(appGenState, "", " ") + if err != nil { + return err + } + + genDoc.AppState = bz + + bz, err = tmjson.MarshalIndent(genDoc, "", " ") + if err != nil { + return err + } + + // write the updated genesis file to each validator + for _, val := range c.validators { + if err := util.WriteFile(filepath.Join(val.configDir(), "config", "genesis.json"), bz); err != nil { + return err + } + } + return nil +} + +func initNodes(c *internalChain) error { + if err := c.createAndInitValidators(2); err != nil { + return err + } + + // initialize a genesis file for the first validator + val0ConfigDir := c.validators[0].configDir() + for _, val := range c.validators { + if c.chainMeta.Id == ChainAID { + if err := addAccount(val0ConfigDir, "", InitBalanceStrA, val.getKeyInfo().GetAddress()); err != nil { + return err + } + } else if c.chainMeta.Id == ChainBID { + if err := addAccount(val0ConfigDir, "", InitBalanceStrB, val.getKeyInfo().GetAddress()); err != nil { + return err + } + } + } + + // copy the genesis file to the remaining validators + for _, val := range c.validators[1:] { + _, err := util.CopyFile( + filepath.Join(val0ConfigDir, "config", "genesis.json"), + filepath.Join(val.configDir(), "config", "genesis.json"), + ) + if err != nil { + return err + } + } + return nil +} + +func initValidatorConfigs(c *internalChain) error { + for i, val := range c.validators { + tmCfgPath := filepath.Join(val.configDir(), "config", "config.toml") + + vpr := viper.New() + vpr.SetConfigFile(tmCfgPath) + if err := vpr.ReadInConfig(); err != nil { + return err + } + + valConfig := &tmconfig.Config{} + 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", val.instanceName(), 26656) + valConfig.RPC.ListenAddress = "tcp://0.0.0.0:26657" + valConfig.StateSync.Enable = false + valConfig.LogLevel = "info" + + var peers []string + + for j := 0; j < len(c.validators); j++ { + if i == j { + continue + } + + peer := c.validators[j] + peerID := fmt.Sprintf("%s@%s%d:26656", peer.getNodeKey().ID(), peer.getMoniker(), j) + peers = append(peers, peerID) + } + + valConfig.P2P.PersistentPeers = strings.Join(peers, ",") + + tmconfig.WriteConfigFile(tmCfgPath, valConfig) + + // set application configuration + appCfgPath := filepath.Join(val.configDir(), "config", "app.toml") + + appConfig := srvconfig.DefaultConfig() + appConfig.API.Enable = true + appConfig.MinGasPrices = fmt.Sprintf("%s%s", MinGasPrice, OsmoDenom) + + srvconfig.WriteConfigFile(appCfgPath, appConfig) + } + return nil +} diff --git a/tests/e2e/chain/export.go b/tests/e2e/chain/export.go new file mode 100644 index 00000000000..d2b7922dbaa --- /dev/null +++ b/tests/e2e/chain/export.go @@ -0,0 +1,25 @@ +package chain + +import "fmt" + +type ChainMeta struct { + DataDir string `json:"dataDir"` + Id string `json:"id"` +} + +type Validator struct { + Name string `json:"name"` + ConfigDir string `json:"configDir"` + Index int `json:"index"` + Mnemonic string `json:"mnemonic"` + PublicAddress string `json:"publicAddress"` +} + +type Chain struct { + ChainMeta ChainMeta `json:"chainMeta"` + Validators []*Validator `json:"validators"` +} + +func (c *ChainMeta) configDir() string { + return fmt.Sprintf("%s/%s", c.DataDir, c.Id) +} diff --git a/tests/e2e/chain/main.go b/tests/e2e/chain/main.go new file mode 100644 index 00000000000..56cfc89b329 --- /dev/null +++ b/tests/e2e/chain/main.go @@ -0,0 +1,18 @@ +package chain + +func Init(id, dataDir string) (*Chain, error) { + chain, err := new(id, dataDir) + if err != nil { + return nil, err + } + if err := initNodes(chain); err != nil { + return nil, err + } + if err := initGenesis(chain); err != nil { + return nil, err + } + if err := initValidatorConfigs(chain); err != nil { + return nil, err + } + return chain.export(), nil +} diff --git a/tests/e2e/chain/validator.go b/tests/e2e/chain/validator.go new file mode 100644 index 00000000000..2cc66e85f9c --- /dev/null +++ b/tests/e2e/chain/validator.go @@ -0,0 +1,334 @@ +package chain + +import ( + "encoding/json" + "fmt" + "os" + "path" + "path/filepath" + + 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" + 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" + tmcfg "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" + + osmosisApp "github.com/osmosis-labs/osmosis/v7/app" + "github.com/osmosis-labs/osmosis/v7/tests/e2e/util" +) + +type internalValidator struct { + chain *internalChain + index int + moniker string + mnemonic string + keyInfo keyring.Info + privateKey cryptotypes.PrivKey + consensusKey privval.FilePVKey + consensusPrivKey cryptotypes.PrivKey + nodeKey p2p.NodeKey +} + +func (v *internalValidator) instanceName() string { + return fmt.Sprintf("%s%d", v.moniker, v.index) +} + +func (v *internalValidator) configDir() string { + return fmt.Sprintf("%s/%s", v.chain.chainMeta.configDir(), v.instanceName()) +} + +func (v *internalValidator) getKeyInfo() keyring.Info { + return v.keyInfo +} + +func (v *internalValidator) getMoniker() string { + return v.moniker +} + +func (v *internalValidator) getMnemonic() string { + return v.mnemonic +} + +func (v *internalValidator) buildCreateValidatorMsg(amount sdk.Coin) (sdk.Msg, error) { + description := stakingtypes.NewDescription(v.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(v.consensusKey.PubKey) + if err != nil { + return nil, err + } + + return stakingtypes.NewMsgCreateValidator( + sdk.ValAddress(v.keyInfo.GetAddress()), + valPubKey, + amount, + description, + commissionRates, + minSelfDelegation, + ) +} + +func (v *internalValidator) createConfig() error { + p := path.Join(v.configDir(), "config") + return os.MkdirAll(p, 0o755) +} + +func (v *internalValidator) createNodeKey() error { + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + + config.SetRoot(v.configDir()) + config.Moniker = v.moniker + + nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + if err != nil { + return err + } + + v.nodeKey = *nodeKey + return nil +} + +func (v *internalValidator) createConsensusKey() error { + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + + config.SetRoot(v.configDir()) + config.Moniker = v.moniker + + pvKeyFile := config.PrivValidatorKeyFile() + if err := tmos.EnsureDir(filepath.Dir(pvKeyFile), 0777); err != nil { + return err + } + + pvStateFile := config.PrivValidatorStateFile() + if err := tmos.EnsureDir(filepath.Dir(pvStateFile), 0777); err != nil { + return err + } + + filePV := privval.LoadOrGenFilePV(pvKeyFile, pvStateFile) + v.consensusKey = filePV.Key + + return nil +} + +func (v *internalValidator) createKeyFromMnemonic(name, mnemonic string) error { + kb, err := keyring.New(keyringAppName, keyring.BackendTest, v.configDir(), nil) + 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, "", sdk.FullFundraiserPath, algo) + 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 + } + + v.keyInfo = info + v.mnemonic = mnemonic + v.privateKey = privKey + + return nil +} + +func (v *internalValidator) createKey(name string) error { + mnemonic, err := createMnemonic() + if err != nil { + return err + } + + return v.createKeyFromMnemonic(name, mnemonic) +} + +func (v *internalValidator) export() *Validator { + return &Validator{ + Name: v.instanceName(), + ConfigDir: v.configDir(), + Index: v.index, + Mnemonic: v.mnemonic, + PublicAddress: v.keyInfo.GetAddress().String(), + } +} + +func (v *internalValidator) getNodeKey() *p2p.NodeKey { + return &v.nodeKey +} + +func (v *internalValidator) getGenesisDoc() (*tmtypes.GenesisDoc, error) { + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + config.SetRoot(v.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 (v *internalValidator) init() error { + if err := v.createConfig(); err != nil { + return err + } + + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + + config.SetRoot(v.configDir()) + config.Moniker = v.moniker + + genDoc, err := v.getGenesisDoc() + if err != nil { + return err + } + + appState, err := json.MarshalIndent(osmosisApp.ModuleBasics.DefaultGenesis(util.Cdc), "", " ") + if err != nil { + return fmt.Errorf("failed to JSON encode app genesis state: %w", err) + } + + genDoc.ChainID = v.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) + } + + tmcfg.WriteConfigFile(filepath.Join(config.RootDir, "config", "config.toml"), config) + return nil +} + +func 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 (v *internalValidator) 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", v.nodeKey.ID(), v.instanceName())) + txBuilder.SetFeeAmount(sdk.NewCoins()) + txBuilder.SetGasLimit(200000) + + signerData := authsigning.SignerData{ + ChainID: v.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. + sig := txsigning.SignatureV2{ + PubKey: v.keyInfo.GetPubKey(), + 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 := v.privateKey.Sign(bytesToSign) + if err != nil { + return nil, err + } + + sig = txsigning.SignatureV2{ + PubKey: v.keyInfo.GetPubKey(), + 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/chain_init/chain-init.Dockerfile b/tests/e2e/chain_init/chain-init.Dockerfile new file mode 100644 index 00000000000..91e5346b0df --- /dev/null +++ b/tests/e2e/chain_init/chain-init.Dockerfile @@ -0,0 +1,23 @@ +# syntax=docker/dockerfile:1 + +## Build Image +FROM golang:1.18-bullseye as build + +WORKDIR /osmosis +COPY . /osmosis + +# From https://github.com/CosmWasm/wasmd/blob/master/Dockerfile +# For more details see https://github.com/CosmWasm/wasmvm#builds-of-libwasmvm +ADD https://github.com/CosmWasm/wasmvm/releases/download/v1.0.0-beta7/libwasmvm_muslc.a /lib/libwasmvm_muslc.a +RUN sha256sum /lib/libwasmvm_muslc.a | grep d0152067a5609bfdfb3f0d5d6c0f2760f79d5f2cd7fd8513cafa9932d22eb350 +RUN BUILD_TAGS=muslc make build-e2e-chain-init + +## Deploy image +FROM ubuntu + +COPY --from=build /osmosis/build/chain_init /bin/chain_init + +ENV HOME /osmosis +WORKDIR $HOME + +ENTRYPOINT [ "chain_init" ] diff --git a/tests/e2e/chain_init/main.go b/tests/e2e/chain_init/main.go new file mode 100644 index 00000000000..12275a56baa --- /dev/null +++ b/tests/e2e/chain_init/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + + "github.com/osmosis-labs/osmosis/v7/tests/e2e/chain" +) + +func main() { + var ( + dataDir string + chainId string + ) + + flag.StringVar(&dataDir, "data-dir", "", "chain data directory") + flag.StringVar(&chainId, "chain-id", "", "chain ID") + flag.Parse() + + if len(dataDir) == 0 { + panic("data-dir is required") + } + + if err := os.MkdirAll(dataDir, 0o755); err != nil { + panic(err) + } + + createdChain, err := chain.Init(chainId, dataDir) + if err != nil { + panic(err) + } + + b, _ := json.Marshal(createdChain) + fileName := fmt.Sprintf("%v/%v-encode", dataDir, chainId) + if err = os.WriteFile(fileName, b, 0777); err != nil { + panic(err) + } +} diff --git a/tests/e2e/e2e_setup_test.go b/tests/e2e/e2e_setup_test.go new file mode 100644 index 00000000000..7e2ac86197e --- /dev/null +++ b/tests/e2e/e2e_setup_test.go @@ -0,0 +1,328 @@ +package e2e + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path" + "path/filepath" + "strconv" + "testing" + "time" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/stretchr/testify/suite" + + rpchttp "github.com/tendermint/tendermint/rpc/client/http" + + "github.com/osmosis-labs/osmosis/v7/tests/e2e/chain" + "github.com/osmosis-labs/osmosis/v7/tests/e2e/util" +) + +const maxRetries = 10 // max retries for json unmarshalling + +type IntegrationTestSuite struct { + suite.Suite + + tmpDirs []string + chains []*chain.Chain + dkrPool *dockertest.Pool + dkrNet *dockertest.Network + hermesResource *dockertest.Resource + initResource *dockertest.Resource + valResources map[string][]*dockertest.Resource +} + +func TestIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} + +func (s *IntegrationTestSuite) SetupSuite() { + s.T().Log("setting up e2e integration test suite...") + + s.chains = make([]*chain.Chain, 0, 2) + + // The e2e test flow is as follows: + // + // 1. Configure two chains - chan A and chain B. + // * For each chain, set up two validators + // * Initialize configs and genesis for all validators. + // 2. Start both networks. + // 3. Run IBC relayer betweeen the two chains. + // 4. Execute various e2e tests, including IBC. + s.configureDockerResources(chain.ChainAID, chain.ChainBID) + + s.configureChain(chain.ChainAID) + s.Require().NoError(s.dkrPool.Purge(s.initResource)) + s.configureChain(chain.ChainBID) + s.Require().NoError(s.dkrPool.Purge(s.initResource)) + + s.runValidators(s.chains[0], 0) + s.runValidators(s.chains[1], 10) + s.runIBCRelayer() +} + +func (s *IntegrationTestSuite) TearDownSuite() { + if str := os.Getenv("OSMOSIS_E2E_SKIP_CLEANUP"); len(str) > 0 { + skipCleanup, err := strconv.ParseBool(str) + s.Require().NoError(err) + + if skipCleanup { + return + } + } + + s.T().Log("tearing down e2e integration test suite...") + + s.Require().NoError(s.dkrPool.Purge(s.hermesResource)) + + for _, vr := range s.valResources { + for _, r := range vr { + s.Require().NoError(s.dkrPool.Purge(r)) + } + } + + s.Require().NoError(s.dkrPool.RemoveNetwork(s.dkrNet)) + + for _, chain := range s.chains { + os.RemoveAll(chain.ChainMeta.DataDir) + } + + for _, td := range s.tmpDirs { + os.RemoveAll(td) + } +} + +func (s *IntegrationTestSuite) runValidators(c *chain.Chain, portOffset int) { + s.T().Logf("starting Osmosis %s validator containers...", c.ChainMeta.Id) + + s.valResources[c.ChainMeta.Id] = make([]*dockertest.Resource, len(c.Validators)) + for i, val := range c.Validators { + runOpts := &dockertest.RunOptions{ + Name: val.Name, + NetworkID: s.dkrNet.Network.ID, + Mounts: []string{ + fmt.Sprintf("%s/:/osmosis/.osmosisd", val.ConfigDir), + }, + Repository: "osmosis", + Tag: "debug", + } + + // expose the first validator for debugging and communication + if val.Index == 0 { + runOpts.PortBindings = map[docker.Port][]docker.PortBinding{ + "1317/tcp": {{HostIP: "", HostPort: fmt.Sprintf("%d", 1317+portOffset)}}, + "6060/tcp": {{HostIP: "", HostPort: fmt.Sprintf("%d", 6060+portOffset)}}, + "6061/tcp": {{HostIP: "", HostPort: fmt.Sprintf("%d", 6061+portOffset)}}, + "6062/tcp": {{HostIP: "", HostPort: fmt.Sprintf("%d", 6062+portOffset)}}, + "6063/tcp": {{HostIP: "", HostPort: fmt.Sprintf("%d", 6063+portOffset)}}, + "6064/tcp": {{HostIP: "", HostPort: fmt.Sprintf("%d", 6064+portOffset)}}, + "6065/tcp": {{HostIP: "", HostPort: fmt.Sprintf("%d", 6065+portOffset)}}, + "9090/tcp": {{HostIP: "", HostPort: fmt.Sprintf("%d", 9090+portOffset)}}, + "26656/tcp": {{HostIP: "", HostPort: fmt.Sprintf("%d", 26656+portOffset)}}, + "26657/tcp": {{HostIP: "", HostPort: fmt.Sprintf("%d", 26657+portOffset)}}, + } + } + + resource, err := s.dkrPool.RunWithOptions(runOpts, noRestart) + s.Require().NoError(err) + + s.valResources[c.ChainMeta.Id][i] = resource + s.T().Logf("started Osmosis %s validator container: %s", c.ChainMeta.Id, resource.Container.ID) + } + + rpcClient, err := rpchttp.New("tcp://localhost:26657", "/websocket") + s.Require().NoError(err) + + s.Require().Eventually( + func() bool { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + status, err := rpcClient.Status(ctx) + if err != nil { + return false + } + + // let the node produce a few blocks + if status.SyncInfo.CatchingUp || status.SyncInfo.LatestBlockHeight < 3 { + return false + } + + return true + }, + 5*time.Minute, + time.Second, + "Osmosis node failed to produce blocks", + ) +} + +func (s *IntegrationTestSuite) runIBCRelayer() { + s.T().Log("starting Hermes relayer container...") + + tmpDir, err := ioutil.TempDir("", "osmosis-e2e-testnet-hermes-") + s.Require().NoError(err) + s.tmpDirs = append(s.tmpDirs, tmpDir) + + osmoAVal := s.chains[0].Validators[0] + osmoBVal := s.chains[1].Validators[0] + hermesCfgPath := path.Join(tmpDir, "hermes") + + s.Require().NoError(os.MkdirAll(hermesCfgPath, 0o755)) + _, err = util.CopyFile( + filepath.Join("./scripts/", "hermes_bootstrap.sh"), + filepath.Join(hermesCfgPath, "hermes_bootstrap.sh"), + ) + s.Require().NoError(err) + + s.hermesResource, err = s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: fmt.Sprintf("%s-%s-relayer", s.chains[0].ChainMeta.Id, s.chains[1].ChainMeta.Id), + Repository: "osmolabs/hermes", + Tag: "0.13.0", + NetworkID: s.dkrNet.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("OSMO_A_E2E_CHAIN_ID=%s", s.chains[0].ChainMeta.Id), + fmt.Sprintf("OSMO_B_E2E_CHAIN_ID=%s", s.chains[1].ChainMeta.Id), + fmt.Sprintf("OSMO_A_E2E_VAL_MNEMONIC=%s", osmoAVal.Mnemonic), + fmt.Sprintf("OSMO_B_E2E_VAL_MNEMONIC=%s", osmoBVal.Mnemonic), + fmt.Sprintf("OSMO_A_E2E_VAL_HOST=%s", s.valResources[s.chains[0].ChainMeta.Id][0].Container.Name[1:]), + fmt.Sprintf("OSMO_B_E2E_VAL_HOST=%s", s.valResources[s.chains[1].ChainMeta.Id][0].Container.Name[1:]), + }, + Entrypoint: []string{ + "sh", + "-c", + "chmod +x /root/hermes/hermes_bootstrap.sh && /root/hermes/hermes_bootstrap.sh", + }, + }, + noRestart, + ) + s.Require().NoError(err) + + endpoint := fmt.Sprintf("http://%s/state", s.hermesResource.GetHostPort("3031/tcp")) + s.Require().Eventually( + func() bool { + resp, err := http.Get(endpoint) + 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 := respBody["status"].(string) + result := respBody["result"].(map[string]interface{}) + + return status == "success" && len(result["chains"].([]interface{})) == 2 + }, + 5*time.Minute, + time.Second, + "hermes relayer not healthy", + ) + + s.T().Logf("started Hermes relayer container: %s", 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 Osmosis chains + s.connectIBCChains() +} + +func (s *IntegrationTestSuite) configureChain(chainId string) { + s.T().Logf("starting e2e infrastructure for chain-id: %s", chainId) + tmpDir, err := ioutil.TempDir("", "osmosis-e2e-testnet-") + + s.T().Logf("temp directory for chain-id %v: %v", chainId, tmpDir) + s.Require().NoError(err) + + s.initResource, err = s.dkrPool.RunWithOptions( + &dockertest.RunOptions{ + Name: fmt.Sprintf("%s", chainId), + Repository: "osmosis-e2e-chain-init", + Tag: "debug", + NetworkID: s.dkrNet.Network.ID, + Cmd: []string{ + fmt.Sprintf("--data-dir=%s", tmpDir), + fmt.Sprintf("--chain-id=%s", chainId), + }, + User: "root:root", + Mounts: []string{ + fmt.Sprintf("%s:%s", tmpDir, tmpDir), + }, + }, + noRestart, + ) + s.Require().NoError(err) + + var newChain chain.Chain + + fileName := fmt.Sprintf("%v/%v-encode", tmpDir, chainId) + s.T().Logf("serialized init file for chain-id %v: %v", chainId, fileName) + + // loop through the reading and unmarshaling of the init file a total of maxRetries or until error is nil + // without this, test attempts to unmarshal file before docker container is finished writing + for i := 0; i < maxRetries; i++ { + encJson, _ := os.ReadFile(fileName) + err = json.Unmarshal(encJson, &newChain) + if err == nil { + break + } + + if i == maxRetries-1 { + s.Require().NoError(err) + } + + if i > 0 { + time.Sleep(1 * time.Second) + } + } + s.chains = append(s.chains, &newChain) + +} + +func (s *IntegrationTestSuite) configureDockerResources(chainIDOne, chainIDTwo string) { + var err error + s.dkrPool, err = dockertest.NewPool("") + s.Require().NoError(err) + + s.dkrNet, err = s.dkrPool.CreateNetwork(fmt.Sprintf("%s-%s-testnet", chainIDOne, chainIDTwo)) + s.Require().NoError(err) + + s.valResources = make(map[string][]*dockertest.Resource) +} + +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_test.go b/tests/e2e/e2e_test.go new file mode 100644 index 00000000000..e93d8e10db7 --- /dev/null +++ b/tests/e2e/e2e_test.go @@ -0,0 +1,136 @@ +package e2e + +import ( + "fmt" + "io" + "net/http" + "strings" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/osmosis-labs/osmosis/v7/tests/e2e/chain" + "github.com/osmosis-labs/osmosis/v7/tests/e2e/util" +) + +func (s *IntegrationTestSuite) TestQueryBalances() { + var ( + expectedDenomsA = []string{chain.OsmoDenom, chain.StakeDenom} + expectedDenomsB = []string{chain.OsmoDenom, chain.StakeDenom, chain.IbcDenom} + expectedBalancesA = []uint64{chain.OsmoBalanceA - chain.IbcSendAmount, chain.StakeBalanceA - chain.StakeAmountA} + expectedBalancesB = []uint64{chain.OsmoBalanceB, chain.StakeBalanceB - chain.StakeAmountB, chain.IbcSendAmount} + ) + + chainAAPIEndpoint := fmt.Sprintf("http://%s", s.valResources[s.chains[0].ChainMeta.Id][0].GetHostPort("1317/tcp")) + balancesA, err := queryBalances(chainAAPIEndpoint, s.chains[0].Validators[0].PublicAddress) + s.Require().NoError(err) + s.Require().NotNil(balancesA) + s.Require().Equal(2, len(balancesA)) + + chainBAPIEndpoint := fmt.Sprintf("http://%s", s.valResources[s.chains[1].ChainMeta.Id][0].GetHostPort("1317/tcp")) + balancesB, err := queryBalances(chainBAPIEndpoint, s.chains[1].Validators[0].PublicAddress) + s.Require().NoError(err) + s.Require().NotNil(balancesB) + s.Require().Equal(3, len(balancesB)) + + actualDenomsA := make([]string, 0, 2) + actualBalancesA := make([]uint64, 0, 2) + actualDenomsB := make([]string, 0, 2) + actualBalancesB := make([]uint64, 0, 2) + + for _, balanceA := range balancesA { + actualDenomsA = append(actualDenomsA, balanceA.GetDenom()) + actualBalancesA = append(actualBalancesA, balanceA.Amount.Uint64()) + } + + for _, balanceB := range balancesB { + actualDenomsB = append(actualDenomsB, balanceB.GetDenom()) + actualBalancesB = append(actualBalancesB, balanceB.Amount.Uint64()) + } + + s.Require().ElementsMatch(expectedDenomsA, actualDenomsA) + s.Require().ElementsMatch(expectedBalancesA, actualBalancesA) + s.Require().ElementsMatch(expectedDenomsB, actualDenomsB) + s.Require().ElementsMatch(expectedBalancesB, actualBalancesB) + +} + +func queryBalances(endpoint, addr string) (sdk.Coins, error) { + path := fmt.Sprintf( + "%s/cosmos/bank/v1beta1/balances/%s", + endpoint, addr, + ) + var err error + var resp *http.Response + retriesLeft := 5 + for { + resp, err = http.Get(path) + + if resp.StatusCode == http.StatusServiceUnavailable { + retriesLeft-- + if retriesLeft == 0 { + return nil, fmt.Errorf("exceeded retry limit of %d with %d", retriesLeft, http.StatusServiceUnavailable) + } + time.Sleep(10 * time.Second) + } else { + break + } + } + + if err != nil { + return nil, fmt.Errorf("failed to execute HTTP request: %w", err) + } + + defer resp.Body.Close() + + bz, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var balancesResp banktypes.QueryAllBalancesResponse + if err := util.Cdc.UnmarshalJSON(bz, &balancesResp); err != nil { + return nil, err + } + + return balancesResp.GetBalances(), nil +} + +func (s *IntegrationTestSuite) TestIBCTokenTransfer() { + var ibcStakeDenom string + + s.Run("send_uosmo_to_chainB", func() { + recipient := s.chains[1].Validators[0].PublicAddress + token := sdk.NewInt64Coin(chain.OsmoDenom, chain.IbcSendAmount) // 3,300uosmo + s.sendIBC(s.chains[0].ChainMeta.Id, s.chains[1].ChainMeta.Id, recipient, token) + + chainBAPIEndpoint := fmt.Sprintf("http://%s", s.valResources[s.chains[1].ChainMeta.Id][0].GetHostPort("1317/tcp")) + + // require the recipient account receives the IBC tokens (IBC packets ACKd) + var ( + balances sdk.Coins + err error + ) + s.Require().Eventually( + func() bool { + balances, err = queryBalances(chainBAPIEndpoint, recipient) + s.Require().NoError(err) + + return balances.Len() == 3 + }, + time.Minute, + 5*time.Second, + ) + + for _, c := range balances { + if strings.Contains(c.Denom, "ibc/") { + ibcStakeDenom = c.Denom + s.Require().Equal(token.Amount.Int64(), c.Amount.Int64()) + break + } + } + + s.Require().NotEmpty(ibcStakeDenom) + }) +} diff --git a/tests/e2e/e2e_util_test.go b/tests/e2e/e2e_util_test.go new file mode 100644 index 00000000000..83976442b76 --- /dev/null +++ b/tests/e2e/e2e_util_test.go @@ -0,0 +1,108 @@ +package e2e + +import ( + "bytes" + "context" + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ory/dockertest/v3/docker" +) + +func (s *IntegrationTestSuite) connectIBCChains() { + s.T().Logf("connecting %s and %s chains via IBC", s.chains[0].ChainMeta.Id, s.chains[1].ChainMeta.Id) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ + Context: ctx, + AttachStdout: true, + AttachStderr: true, + Container: s.hermesResource.Container.ID, + User: "root", + Cmd: []string{ + "hermes", + "create", + "channel", + s.chains[0].ChainMeta.Id, + s.chains[1].ChainMeta.Id, + "--port-a=transfer", + "--port-b=transfer", + }, + }) + s.Require().NoError(err) + + var ( + outBuf bytes.Buffer + errBuf bytes.Buffer + ) + + err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{ + Context: ctx, + Detach: false, + OutputStream: &outBuf, + ErrorStream: &errBuf, + }) + s.Require().NoErrorf( + err, + "failed connect chains; stdout: %s, stderr: %s", outBuf.String(), errBuf.String(), + ) + + s.Require().Containsf( + errBuf.String(), + "successfully opened init channel", + "failed to connect chains via IBC: %s", errBuf.String(), + ) + + s.T().Logf("connected %s and %s chains via IBC", s.chains[0].ChainMeta.Id, s.chains[1].ChainMeta.Id) +} + +func (s *IntegrationTestSuite) sendIBC(srcChainID, dstChainID, recipient string, token sdk.Coin) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + s.T().Logf("sending %s from %s to %s (%s)", token, srcChainID, dstChainID, recipient) + + exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{ + Context: ctx, + AttachStdout: true, + AttachStderr: true, + Container: s.hermesResource.Container.ID, + User: "root", + Cmd: []string{ + "hermes", + "tx", + "raw", + "ft-transfer", + dstChainID, + srcChainID, + "transfer", // source chain port ID + "channel-0", // since only one connection/channel exists, assume 0 + token.Amount.String(), + fmt.Sprintf("--denom=%s", token.Denom), + fmt.Sprintf("--receiver=%s", recipient), + "--timeout-height-offset=1000", + }, + }) + s.Require().NoError(err) + + var ( + outBuf bytes.Buffer + errBuf bytes.Buffer + ) + + err = s.dkrPool.Client.StartExec(exec.ID, docker.StartExecOptions{ + Context: ctx, + Detach: false, + OutputStream: &outBuf, + ErrorStream: &errBuf, + }) + s.Require().NoErrorf( + err, + "failed to send IBC tokens; stdout: %s, stderr: %s", outBuf.String(), errBuf.String(), + ) + + s.T().Log("successfully sent IBC tokens") +} diff --git a/tests/e2e/scripts/hermes_bootstrap.sh b/tests/e2e/scripts/hermes_bootstrap.sh new file mode 100644 index 00000000000..4a438c1f9ff --- /dev/null +++ b/tests/e2e/scripts/hermes_bootstrap.sh @@ -0,0 +1,72 @@ +#!/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 <