From 050790e882bcb14490b1ccc730e073c882a6ce8d Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Mon, 20 Nov 2023 17:40:06 +0300 Subject: [PATCH] test: add major upgrade test from v1 -> main (latest) (#2814) This PR adds a test which spins up a set of 4 nodes on a random v1 version. It then gets all nodes to upgrade at height 10 and asserts that the upgrade is successful (that all nodes reach height 12). This can be helpful to catching any problems with the upgrade (i.e. missing migrations) --- Makefile | 4 +- test/e2e/node.go | 8 +++- test/e2e/readme.md | 6 ++- test/e2e/setup.go | 1 + test/e2e/simple_test.go | 20 +++++++--- test/e2e/testnet.go | 14 +++---- test/e2e/upgrade_test.go | 85 +++++++++++++++++++++++++++++++++++++++- test/e2e/versions.go | 41 ++++++++++++------- 8 files changed, 146 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index 918a1e639e..9f66f9035d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ VERSION := $(shell echo $(shell git describe --tags 2>/dev/null || git log -1 --format='%h') | sed 's/^v//') -COMMIT := $(shell git log -1 --format='%H') +COMMIT := $(shell git rev-parse --short HEAD) DOCKER := $(shell which docker) ALL_VERSIONS := $(shell git tag -l) DOCKER_BUF := $(DOCKER) run --rm -v $(CURDIR):/workspace --workdir /workspace bufbuild/buf @@ -119,7 +119,7 @@ test-short: ## test-e2e: Run end to end tests via knuu. This command requires a kube/config file to configure kubernetes. test-e2e: @echo "--> Running end to end tests" - @KNUU_NAMESPACE=test KNUU_TIMEOUT=20m E2E_VERSIONS="$(ALL_VERSIONS)" E2E=true go test ./test/e2e/... -timeout 20m -v + @KNUU_NAMESPACE=test KNUU_TIMEOUT=20m E2E_LATEST_VERSION=$(shell git rev-parse --short main) E2E_VERSIONS="$(ALL_VERSIONS)" E2E=true go test ./test/e2e/... -timeout 20m -v .PHONY: test-e2e ## test-race: Run tests in race mode. diff --git a/test/e2e/node.go b/test/e2e/node.go index c73c45dcb7..7e3c84866e 100644 --- a/test/e2e/node.go +++ b/test/e2e/node.go @@ -45,6 +45,7 @@ func NewNode( startHeight, selfDelegation int64, peers []string, signerKey, networkKey, accountKey crypto.PrivKey, + upgradeHeight int64, ) (*Node, error) { instance, err := knuu.NewInstance(name) if err != nil { @@ -75,7 +76,12 @@ func NewNode( if err != nil { return nil, err } - err = instance.SetArgs("start", fmt.Sprintf("--home=%s", remoteRootDir), "--rpc.laddr=tcp://0.0.0.0:26657") + args := []string{"start", fmt.Sprintf("--home=%s", remoteRootDir), "--rpc.laddr=tcp://0.0.0.0:26657"} + if upgradeHeight != 0 { + args = append(args, fmt.Sprintf("--v2-upgrade-height=%d", upgradeHeight)) + } + + err = instance.SetArgs(args...) if err != nil { return nil, err } diff --git a/test/e2e/readme.md b/test/e2e/readme.md index 6a20d3b72e..acdec30c7c 100644 --- a/test/e2e/readme.md +++ b/test/e2e/readme.md @@ -7,9 +7,11 @@ Celestia uses the [knuu](https://github.com/celestiaorg/knuu) framework to orche E2E tests can be simply run through go tests. They are distinguished from unit tets through an environment variable. To run all e2e tests run: ```shell -E2E=true KNUU_NAMESPACE=test go test ./test/e2e/... -timeout 30m +E2E=true KNUU_NAMESPACE=test E2E_LATEST_VERSION="$(git rev-parse --short main)" E2E_VERSIONS="$(git tag -l)" go test ./test/e2e/... -timeout 30m -v ``` +You can optionally set a global timeout using `KNUU_TIMEOUT` (default is 60m). + ## Observation -Logs of each of the nodes are posted to Grafana and can be accessed through Celestia's dashboard (using the `celestia-app` namespace). +Logs of each of the nodes are posted to Grafana and can be accessed through Celestia's dashboard (using the `test` namespace). diff --git a/test/e2e/setup.go b/test/e2e/setup.go index 709c410876..a58118409e 100644 --- a/test/e2e/setup.go +++ b/test/e2e/setup.go @@ -140,6 +140,7 @@ func MakeConfig(node *Node) (*config.Config, error) { cfg.P2P.PersistentPeers = strings.Join(node.InitialPeers, ",") cfg.Consensus.TimeoutPropose = time.Second cfg.Consensus.TimeoutCommit = time.Second + cfg.Instrumentation.Prometheus = true return cfg, nil } diff --git a/test/e2e/simple_test.go b/test/e2e/simple_test.go index 0843e1e1ba..1e7843ab85 100644 --- a/test/e2e/simple_test.go +++ b/test/e2e/simple_test.go @@ -9,6 +9,7 @@ import ( "github.com/celestiaorg/celestia-app/app" "github.com/celestiaorg/celestia-app/app/encoding" + v1 "github.com/celestiaorg/celestia-app/pkg/appconsts/v1" "github.com/celestiaorg/celestia-app/test/txsim" "github.com/celestiaorg/celestia-app/test/util/testnode" "github.com/stretchr/testify/require" @@ -26,11 +27,17 @@ func TestE2ESimple(t *testing.T) { t.Skip("skipping e2e test") } - if os.Getenv("E2E_VERSIONS") != "" { - versionsStr := os.Getenv("E2E_VERSIONS") - versions := ParseVersions(versionsStr) - if len(versions) > 0 { - latestVersion = versions.GetLatest().String() + if os.Getenv("E2E_LATEST_VERSION") != "" { + latestVersion = os.Getenv("E2E_LATEST_VERSION") + _, isSemVer := ParseVersion(latestVersion) + switch { + case isSemVer: + case latestVersion == "latest": + case len(latestVersion) == 8: + // assume this is a git commit hash (we need to trim the last digit to match the docker image tag) + latestVersion = latestVersion[:7] + default: + t.Fatalf("unrecognised version: %s", latestVersion) } } t.Log("Running simple e2e test", "version", latestVersion) @@ -38,7 +45,7 @@ func TestE2ESimple(t *testing.T) { testnet, err := New(t.Name(), seed) require.NoError(t, err) t.Cleanup(testnet.Cleanup) - require.NoError(t, testnet.CreateGenesisNodes(4, latestVersion, 10000000)) + require.NoError(t, testnet.CreateGenesisNodes(4, latestVersion, 10000000, 0)) kr, err := testnet.CreateAccount("alice", 1e12) require.NoError(t, err) @@ -61,6 +68,7 @@ func TestE2ESimple(t *testing.T) { totalTxs := 0 for _, block := range blockchain { + require.Equal(t, v1.Version, block.Version.App) totalTxs += len(block.Data.Txs) } require.Greater(t, totalTxs, 10) diff --git a/test/e2e/testnet.go b/test/e2e/testnet.go index 3bda11279e..1bbb5ee804 100644 --- a/test/e2e/testnet.go +++ b/test/e2e/testnet.go @@ -34,11 +34,11 @@ func New(name string, seed int64) (*Testnet, error) { }, nil } -func (t *Testnet) CreateGenesisNode(version string, selfDelegation int64) error { +func (t *Testnet) CreateGenesisNode(version string, selfDelegation, upgradeHeight int64) error { signerKey := t.keygen.Generate(ed25519Type) networkKey := t.keygen.Generate(ed25519Type) accountKey := t.keygen.Generate(secp256k1Type) - node, err := NewNode(fmt.Sprintf("val%d", len(t.nodes)), version, 0, selfDelegation, nil, signerKey, networkKey, accountKey) + node, err := NewNode(fmt.Sprintf("val%d", len(t.nodes)), version, 0, selfDelegation, nil, signerKey, networkKey, accountKey, upgradeHeight) if err != nil { return err } @@ -46,20 +46,20 @@ func (t *Testnet) CreateGenesisNode(version string, selfDelegation int64) error return nil } -func (t *Testnet) CreateGenesisNodes(num int, version string, selfDelegation int64) error { - for i := -0; i < num; i++ { - if err := t.CreateGenesisNode(version, selfDelegation); err != nil { +func (t *Testnet) CreateGenesisNodes(num int, version string, selfDelegation, upgradeHeight int64) error { + for i := 0; i < num; i++ { + if err := t.CreateGenesisNode(version, selfDelegation, upgradeHeight); err != nil { return err } } return nil } -func (t *Testnet) CreateNode(version string, startHeight int64) error { +func (t *Testnet) CreateNode(version string, startHeight, upgradeHeight int64) error { signerKey := t.keygen.Generate(ed25519Type) networkKey := t.keygen.Generate(ed25519Type) accountKey := t.keygen.Generate(secp256k1Type) - node, err := NewNode(fmt.Sprintf("val%d", len(t.nodes)), version, startHeight, 0, nil, signerKey, networkKey, accountKey) + node, err := NewNode(fmt.Sprintf("val%d", len(t.nodes)), version, startHeight, 0, nil, signerKey, networkKey, accountKey, upgradeHeight) if err != nil { return err } diff --git a/test/e2e/upgrade_test.go b/test/e2e/upgrade_test.go index ebbc7d3c1f..05d5efad34 100644 --- a/test/e2e/upgrade_test.go +++ b/test/e2e/upgrade_test.go @@ -11,6 +11,7 @@ import ( "github.com/celestiaorg/celestia-app/app" "github.com/celestiaorg/celestia-app/app/encoding" + v2 "github.com/celestiaorg/celestia-app/pkg/appconsts/v2" "github.com/celestiaorg/celestia-app/test/txsim" "github.com/celestiaorg/knuu/pkg/knuu" "github.com/stretchr/testify/require" @@ -55,7 +56,7 @@ func TestMinorVersionCompatibility(t *testing.T) { // each node begins with a random version within the same major version set v := versions.Random(r).String() t.Log("Starting node", "node", i, "version", v) - require.NoError(t, testnet.CreateGenesisNode(v, 10000000)) + require.NoError(t, testnet.CreateGenesisNode(v, 10000000, 0)) } kr, err := testnet.CreateAccount("alice", 1e12) @@ -64,6 +65,7 @@ func TestMinorVersionCompatibility(t *testing.T) { require.NoError(t, testnet.Setup()) require.NoError(t, testnet.Start()) + // TODO: with upgrade tests we should simulate a far broader range of transactions sequences := txsim.NewBlobSequence(txsim.NewRange(200, 4000), txsim.NewRange(1, 3)).Clone(5) sequences = append(sequences, txsim.NewSendSequence(4, 1000, 100).Clone(5)...) @@ -121,6 +123,87 @@ func TestMinorVersionCompatibility(t *testing.T) { require.True(t, errors.Is(err, context.Canceled), err.Error()) } +func TestMajorUpgradeToV2(t *testing.T) { + if os.Getenv("E2E") == "" { + t.Skip("skipping e2e test") + } + + if os.Getenv("E2E_LATEST_VERSION") != "" { + latestVersion = os.Getenv("E2E_LATEST_VERSION") + _, isSemVer := ParseVersion(latestVersion) + switch { + case isSemVer: + case latestVersion == "latest": + case len(latestVersion) == 8: + // assume this is a git commit hash (we need to trim the last digit to match the docker image tag) + latestVersion = latestVersion[:7] + default: + t.Fatalf("unrecognised version: %s", latestVersion) + } + } + + numNodes := 4 + upgradeHeight := int64(10) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + testnet, err := New(t.Name(), seed) + require.NoError(t, err) + t.Cleanup(testnet.Cleanup) + + preloader, err := knuu.NewPreloader() + require.NoError(t, err) + t.Cleanup(func() { _ = preloader.EmptyImages() }) + err = preloader.AddImage(DockerImageName(latestVersion)) + require.NoError(t, err) + + for i := 0; i < numNodes; i++ { + t.Log("Starting node", "node", i, "version", latestVersion) + require.NoError(t, testnet.CreateGenesisNode(latestVersion, 10000000, upgradeHeight)) + } + + kr, err := testnet.CreateAccount("alice", 1e12) + require.NoError(t, err) + + require.NoError(t, testnet.Setup()) + require.NoError(t, testnet.Start()) + + // assert that the network is initially running on v1 + for i := 0; i < numNodes; i++ { + client, err := testnet.Node(i).Client() + require.NoError(t, err) + resp, err := client.Header(ctx, nil) + require.NoError(t, err) + // FIXME: we are not correctly setting the app version at genesis + require.Equal(t, uint64(0), resp.Header.Version.App, "version mismatch before upgrade") + } + + errCh := make(chan error) + encCfg := encoding.MakeConfig(app.ModuleEncodingRegisters...) + opts := txsim.DefaultOptions().WithSeed(seed).SuppressLogs() + sequences := txsim.NewBlobSequence(txsim.NewRange(200, 4000), txsim.NewRange(1, 3)).Clone(5) + sequences = append(sequences, txsim.NewSendSequence(4, 1000, 100).Clone(5)...) + go func() { + errCh <- txsim.Run(ctx, testnet.GRPCEndpoints()[0], kr, encCfg, opts, sequences...) + }() + + // wait for all nodes to move past the upgrade height + for i := 0; i < numNodes; i++ { + client, err := testnet.Node(i).Client() + require.NoError(t, err) + require.NoError(t, waitForHeight(ctx, client, upgradeHeight+2, time.Minute)) + resp, err := client.Header(ctx, nil) + require.NoError(t, err) + require.Equal(t, v2.Version, resp.Header.Version.App, "version mismatch after upgrade") + } + + // end txsim + cancel() + + err = <-errCh + require.True(t, errors.Is(err, context.Canceled), err.Error()) +} + func getHeight(ctx context.Context, client *http.HTTP, period time.Duration) (int64, error) { timer := time.NewTimer(period) ticker := time.NewTicker(100 * time.Millisecond) diff --git a/test/e2e/versions.go b/test/e2e/versions.go index 4d3329a2c8..51e0f5c329 100644 --- a/test/e2e/versions.go +++ b/test/e2e/versions.go @@ -40,29 +40,42 @@ func (v Version) IsGreater(v2 Version) bool { type VersionSet []Version +// ParseVersions takes a string of space-separated versions and returns a +// VersionSet. Invalid versions are ignored. func ParseVersions(versionStr string) VersionSet { versions := strings.Split(versionStr, " ") output := make(VersionSet, 0, len(versions)) for _, v := range versions { - var major, minor, patch, rc uint64 - isRC := false - if strings.Contains(v, "rc") { - _, err := fmt.Sscanf(v, "v%d.%d.%d-rc%d", &major, &minor, &patch, &rc) - isRC = true - if err != nil { - continue - } - } else { - _, err := fmt.Sscanf(v, "v%d.%d.%d", &major, &minor, &patch) - if err != nil { - continue - } + version, isValid := ParseVersion(v) + if !isValid { + continue } - output = append(output, Version{major, minor, patch, isRC, rc}) + output = append(output, version) } return output } +// ParseVersion takes a string and returns a Version. If the string is not a +// valid version, the second return value is false. +// Must be of the format v1.0.0 or v1.0.0-rc1 (i.e. following SemVer) +func ParseVersion(version string) (Version, bool) { + var major, minor, patch, rc uint64 + isRC := false + if strings.Contains(version, "rc") { + _, err := fmt.Sscanf(version, "v%d.%d.%d-rc%d", &major, &minor, &patch, &rc) + isRC = true + if err != nil { + return Version{}, false + } + } else { + _, err := fmt.Sscanf(version, "v%d.%d.%d", &major, &minor, &patch) + if err != nil { + return Version{}, false + } + } + return Version{major, minor, patch, isRC, rc}, true +} + func (v VersionSet) FilterMajor(majorVersion uint64) VersionSet { output := make(VersionSet, 0, len(v)) for _, version := range v {