diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 104953cb4bf9..e708cae41df3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,9 +74,51 @@ jobs: uses: actions/upload-artifact@v3 if: always() with: - name: e2e-tmpnet-data + name: e2e-local-tmpnet-data path: ${{ env.tmpnet_data_path }} if-no-files-found: error + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: ${{ env.go_version }} + check-latest: true + - name: Build AvalancheGo Binary + shell: bash + run: ./scripts/build.sh -r + - name: Run e2e tests + shell: bash + run: E2E_SERIAL=1 ./scripts/tests.e2e.sh --network-id=808 + - name: Upload tmpnet network dir + uses: actions/upload-artifact@v3 + if: always() + with: + name: e2e-mainnet-tmpnet-data + path: ~/.tmpnet/networks/808* + if-no-files-found: error + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: ${{ env.go_version }} + check-latest: true + - name: Build AvalancheGo Binary + shell: bash + run: ./scripts/build.sh -r + - name: Run e2e tests + shell: bash + run: E2E_SERIAL=1 ./scripts/tests.e2e.sh --network-id=909 + - name: Upload tmpnet network dir + uses: actions/upload-artifact@v3 + if: always() + with: + name: e2e-fuji-tmpnet-data + path: ~/.tmpnet/networks/909* + if-no-files-found: error e2e_existing_network: runs-on: ubuntu-latest steps: diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index d363ff775086..14c3e355dba3 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -10,6 +10,7 @@ import ( "github.com/onsi/gomega" + "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/tests/fixture/e2e" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" @@ -35,7 +36,11 @@ func init() { var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { // Run only once in the first ginkgo process - return e2e.NewTestEnvironment(flagVars, &tmpnet.Network{}).Marshal() + return e2e.NewTestEnvironment(flagVars, &tmpnet.Network{ + Genesis: &genesis.UnparsedConfig{ + NetworkID: uint32(flagVars.NetworkID()), + }, + }).Marshal() }, func(envBytes []byte) { // Run in every ginkgo process diff --git a/tests/e2e/p/validator_sets.go b/tests/e2e/p/validator_sets.go new file mode 100644 index 000000000000..d369448dee44 --- /dev/null +++ b/tests/e2e/p/validator_sets.go @@ -0,0 +1,101 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package p + +import ( + "fmt" + "time" + + ginkgo "github.com/onsi/ginkgo/v2" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/tests" + "github.com/ava-labs/avalanchego/tests/fixture/e2e" + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +var _ = e2e.DescribePChain("[Validator Sets]", func() { + require := require.New(ginkgo.GinkgoT()) + + ginkgo.It("should be identical for every height for all nodes in the network", func() { + network := e2e.Env.GetNetwork() + + ginkgo.By("creating wallet with a funded key to source delegated funds from") + keychain := e2e.Env.NewKeychain(1) + nodeURI := e2e.Env.GetRandomNodeURI() + baseWallet := e2e.NewWallet(keychain, nodeURI) + pWallet := baseWallet.P() + + const delegatorCount = 15 + ginkgo.By(fmt.Sprintf("adding %d delegators", delegatorCount), func() { + rewardKey, err := secp256k1.NewPrivateKey() + require.NoError(err) + avaxAssetID := pWallet.AVAXAssetID() + startTime := time.Now().Add(tmpnet.DefaultValidatorStartTimeDiff) + endTime := startTime.Add(time.Second * 360) + // TODO(marun) Ensure this is appropriate for the targeted network (is it accessible from the API?) + weight := genesis.LocalParams.StakingConfig.MinDelegatorStake + + for i := 0; i < delegatorCount; i++ { + _, err = pWallet.IssueAddPermissionlessDelegatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeURI.NodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: weight, + }, + Subnet: constants.PrimaryNetworkID, + }, + avaxAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardKey.Address()}, + }, + e2e.WithDefaultContext(), + ) + require.NoError(err) + } + }) + + ginkgo.By("checking that validator sets are equal across all heights for all nodes", func() { + pvmClients := make([]platformvm.Client, len(e2e.Env.URIs)) + for i, nodeURI := range e2e.Env.URIs { + pvmClients[i] = platformvm.NewClient(nodeURI.URI) + } + + initialHeight, err := pvmClients[0].GetHeight(e2e.DefaultContext()) + require.NoError(err) + + for height := initialHeight; height > 0; height-- { + tests.Outf(" checked validator sets for height %d\n", height) + var observedValidatorSet map[ids.NodeID]*validators.GetValidatorOutput + for _, pvmClient := range pvmClients { + validatorSet, err := pvmClient.GetValidatorsAt( + e2e.DefaultContext(), + constants.PrimaryNetworkID, + height, + ) + require.NoError(err) + if observedValidatorSet == nil { + observedValidatorSet = validatorSet + continue + } + require.Equal(observedValidatorSet, validatorSet) + } + } + }) + + e2e.CheckBootstrapIsPossible(network) + }) +}) diff --git a/tests/fixture/e2e/flags.go b/tests/fixture/e2e/flags.go index 2a00df97a885..dae2ca926625 100644 --- a/tests/fixture/e2e/flags.go +++ b/tests/fixture/e2e/flags.go @@ -15,6 +15,7 @@ type FlagVars struct { avalancheGoExecPath string pluginDir string networkDir string + networkID uint useExistingNetwork bool } @@ -36,6 +37,10 @@ func (v *FlagVars) NetworkDir() string { return os.Getenv(tmpnet.NetworkDirEnvName) } +func (v *FlagVars) NetworkID() uint { + return v.networkID +} + func (v *FlagVars) UseExistingNetwork() bool { return v.useExistingNetwork } @@ -66,6 +71,12 @@ func RegisterFlags() *FlagVars { false, "[optional] whether to target the existing network identified by --network-dir.", ) + flag.UintVar( + &vars.networkID, + "network-id", + 0, + "[optional] the network ID to use. By default a compatible network ID will be generated. Use 808 for mainnet configuration and 909 for fuji configuration.", + ) return &vars } diff --git a/tests/fixture/tmpnet/README.md b/tests/fixture/tmpnet/README.md index 909a29c6ee12..5ac33d934ab5 100644 --- a/tests/fixture/tmpnet/README.md +++ b/tests/fixture/tmpnet/README.md @@ -70,12 +70,24 @@ the `TMPNET_NETWORK_DIR` env var to this symlink ensures that `--use-existing-network` will target the most recently deployed temporary network. +#### Configuring a network with mainnet or fuji configuration + +The `tmpnetctl startnetwork` command supports creating a network with +the same ruleset as mainnet or fuji by using specific network IDs. The +network ID `808` is used to configure a local test network with the +same ruleset as mainnet, and `909` is used to configure a local test +network with the same ruleset as fuji. The network ID can be specified +to `tmpnetctl start-network` via the `--network-id` flag. + ### Via code A temporary network can be managed in code: ```golang network := &tmpnet.Network{ // Configure non-default values for the new network + Genesis: &genesis.UnparsedConfig{ + NetworkID: 808, // (Optional) Configure the network with the mainnet ruleset (see previous section) + }, DefaultFlags: tmpnet.FlagsMap{ config.LogLevelKey: "INFO", // Change one of the network's defaults }, diff --git a/tests/fixture/tmpnet/cmd/main.go b/tests/fixture/tmpnet/cmd/main.go index dd59c300bbb3..223101810306 100644 --- a/tests/fixture/tmpnet/cmd/main.go +++ b/tests/fixture/tmpnet/cmd/main.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" + "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" "github.com/ava-labs/avalanchego/version" ) @@ -52,6 +53,7 @@ func main() { avalancheGoPath string pluginDir string nodeCount uint8 + networkID uint32 ) startNetworkCmd := &cobra.Command{ Use: "start-network", @@ -63,7 +65,11 @@ func main() { // Root dir will be defaulted on start if not provided - network := &tmpnet.Network{} + network := &tmpnet.Network{ + Genesis: &genesis.UnparsedConfig{ + NetworkID: networkID, + }, + } // Extreme upper bound, should never take this long networkStartTimeout := 2 * time.Minute @@ -106,6 +112,7 @@ func main() { startNetworkCmd.PersistentFlags().StringVar(&avalancheGoPath, "avalanchego-path", os.Getenv(tmpnet.AvalancheGoPathEnvName), "The path to an avalanchego binary") startNetworkCmd.PersistentFlags().StringVar(&pluginDir, "plugin-dir", os.ExpandEnv("$HOME/.avalanchego/plugins"), "[optional] the dir containing VM plugins") startNetworkCmd.PersistentFlags().Uint8Var(&nodeCount, "node-count", tmpnet.DefaultNodeCount, "Number of nodes the network should initially consist of") + startNetworkCmd.PersistentFlags().Uint32Var(&networkID, "network-id", 0, "The network ID to use. By default a compatible network ID will be generated. Use 808 for mainnet configuration and 909 for fuji configuration.") rootCmd.AddCommand(startNetworkCmd) stopNetworkCmd := &cobra.Command{ diff --git a/tests/fixture/tmpnet/network.go b/tests/fixture/tmpnet/network.go index 01829da70da5..f1575705c672 100644 --- a/tests/fixture/tmpnet/network.go +++ b/tests/fixture/tmpnet/network.go @@ -263,7 +263,7 @@ func (n *Network) Create(rootDir string) error { } } - if n.Genesis == nil { + if n.Genesis == nil || len(n.Genesis.Allocations) == 0 { // Pre-fund known legacy keys to support ad-hoc testing. Usage of a legacy key will // require knowing the key beforehand rather than retrieving it from the set of pre-funded // keys exposed by a network. Since allocation will not be exclusive, a test using a diff --git a/utils/constants/network_ids.go b/utils/constants/network_ids.go index d00472a39f32..ae2a6f451440 100644 --- a/utils/constants/network_ids.go +++ b/utils/constants/network_ids.go @@ -25,6 +25,10 @@ const ( UnitTestID uint32 = 10 LocalID uint32 = 12345 + // IDs used for validating mainnet and fuji in e2e + TestMainnetID uint32 = 808 + TestFujiID uint32 = 909 + MainnetName = "mainnet" CascadeName = "cascade" DenaliName = "denali" diff --git a/version/constants.go b/version/constants.go index 35320d4ccf33..3ad1334cd315 100644 --- a/version/constants.go +++ b/version/constants.go @@ -130,6 +130,9 @@ var ( constants.MainnetID: time.Date(10000, time.December, 1, 0, 0, 0, 0, time.UTC), constants.FujiID: time.Date(10000, time.December, 1, 0, 0, 0, 0, time.UTC), } + + // Reminder: Add new time variables to updateTimesForTesting to + // ensure e2e is able to test with new versions. ) func init() { @@ -174,6 +177,30 @@ func init() { constants.MainnetID: mainnetXChainStopVertexID, constants.FujiID: fujiXChainStopVertexID, } + + updateTimesForTesting( + ApricotPhase1Times, + ApricotPhase2Times, + ApricotPhase3Times, + ApricotPhase4Times, + ApricotPhase5Times, + ApricotPhasePre6Times, + ApricotPhase6Times, + ApricotPhasePost6Times, + BanffTimes, + CortinaTimes, + DurangoTimes, + ) +} + +// updateTimesForTesting ensures that the provided time maps are updated +// to include entries for contants.TestMainnetID and constants.TestFujiID +// that mirror their mainnet and fuji equivalents to enable e2e testing. +func updateTimesForTesting(timeMaps ...map[uint32]time.Time) { + for _, timeMap := range timeMaps { + timeMap[constants.TestFujiID] = timeMap[constants.FujiID] + timeMap[constants.TestMainnetID] = timeMap[constants.MainnetID] + } } func GetApricotPhase1Time(networkID uint32) time.Time {