From 6b65ce933a1f1ffb481b3c96faf1066d94628ffc Mon Sep 17 00:00:00 2001 From: Maru Newby Date: Fri, 15 Dec 2023 11:34:40 -0800 Subject: [PATCH] tmpnet: Add support for subnets --- tests/fixture/e2e/env.go | 11 +- tests/fixture/e2e/flags.go | 19 +- tests/fixture/e2e/helpers.go | 14 +- tests/fixture/tmpnet/README.md | 16 +- tests/fixture/tmpnet/cmd/main.go | 32 ++- tests/fixture/tmpnet/defaults.go | 10 + tests/fixture/tmpnet/genesis.go | 4 +- tests/fixture/tmpnet/network.go | 143 ++++++++++++- tests/fixture/tmpnet/network_config.go | 18 +- tests/fixture/tmpnet/network_test.go | 2 +- tests/fixture/tmpnet/subnet.go | 283 +++++++++++++++++++++++++ tests/fixture/tmpnet/utils.go | 9 + tests/upgrade/upgrade_test.go | 2 +- 13 files changed, 529 insertions(+), 34 deletions(-) create mode 100644 tests/fixture/tmpnet/subnet.go diff --git a/tests/fixture/e2e/env.go b/tests/fixture/e2e/env.go index 38302c92ed33..1ab573e98df9 100644 --- a/tests/fixture/e2e/env.go +++ b/tests/fixture/e2e/env.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/ava-labs/avalanchego/config" "github.com/ava-labs/avalanchego/tests" "github.com/ava-labs/avalanchego/tests/fixture" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" @@ -53,7 +54,7 @@ func (te *TestEnvironment) Marshal() []byte { } // Initialize a new test environment with a shared network (either pre-existing or newly created). -func NewTestEnvironment(flagVars *FlagVars) *TestEnvironment { +func NewTestEnvironment(flagVars *FlagVars, subnets ...*tmpnet.Subnet) *TestEnvironment { require := require.New(ginkgo.GinkgoT()) networkDir := flagVars.NetworkDir() @@ -66,9 +67,11 @@ func NewTestEnvironment(flagVars *FlagVars) *TestEnvironment { require.NoError(err) tests.Outf("{{yellow}}Using an existing network configured at %s{{/}}\n", network.Dir) } else { - network = StartNetwork(flagVars.AvalancheGoExecPath(), DefaultNetworkDir) + network = StartNetwork(flagVars.AvalancheGoExecPath(), flagVars.pluginDir, DefaultNetworkDir) } + require.NoError(network.CreateSubnets(DefaultContext(), ginkgo.GinkgoWriter, subnets)) + uris := tmpnet.GetNodeURIs(network.Nodes) require.NotEmpty(uris, "network contains no nodes") tests.Outf("{{green}}network URIs: {{/}} %+v\n", uris) @@ -132,5 +135,7 @@ func (te *TestEnvironment) NewPrivateNetwork() *tmpnet.Network { privateNetworksDir := filepath.Join(sharedNetwork.Dir, PrivateNetworksDirName) te.require.NoError(os.MkdirAll(privateNetworksDir, perms.ReadWriteExecute)) - return StartNetwork(sharedNetwork.DefaultRuntimeConfig.AvalancheGoPath, privateNetworksDir) + pluginDir, err := sharedNetwork.DefaultFlags.GetStringVal(config.PluginDirKey) + te.require.NoError(err) + return StartNetwork(sharedNetwork.DefaultRuntimeConfig.AvalancheGoPath, pluginDir, privateNetworksDir) } diff --git a/tests/fixture/e2e/flags.go b/tests/fixture/e2e/flags.go index b1354473fe86..ab6348d82f02 100644 --- a/tests/fixture/e2e/flags.go +++ b/tests/fixture/e2e/flags.go @@ -13,10 +13,19 @@ import ( type FlagVars struct { avalancheGoExecPath string + pluginDir string networkDir string useExistingNetwork bool } +func (v *FlagVars) AvalancheGoExecPath() string { + return v.avalancheGoExecPath +} + +func (v *FlagVars) PluginDir() string { + return v.pluginDir +} + func (v *FlagVars) NetworkDir() string { if !v.useExistingNetwork { return "" @@ -27,10 +36,6 @@ func (v *FlagVars) NetworkDir() string { return os.Getenv(tmpnet.NetworkDirEnvName) } -func (v *FlagVars) AvalancheGoExecPath() string { - return v.avalancheGoExecPath -} - func (v *FlagVars) UseExistingNetwork() bool { return v.useExistingNetwork } @@ -43,6 +48,12 @@ func RegisterFlags() *FlagVars { os.Getenv(tmpnet.AvalancheGoPathEnvName), fmt.Sprintf("avalanchego executable path (required if not using an existing network). Also possible to configure via the %s env variable.", tmpnet.AvalancheGoPathEnvName), ) + flag.StringVar( + &vars.pluginDir, + "plugin-dir", + os.ExpandEnv("$HOME/.avalanchego/plugins"), + "[optional] the dir containing VM plugins.", + ) flag.StringVar( &vars.networkDir, "network-dir", diff --git a/tests/fixture/e2e/helpers.go b/tests/fixture/e2e/helpers.go index 4c0061fa84e6..8cad51006fe9 100644 --- a/tests/fixture/e2e/helpers.go +++ b/tests/fixture/e2e/helpers.go @@ -23,7 +23,6 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/tests" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" @@ -35,19 +34,14 @@ const ( // contention. DefaultTimeout = 2 * time.Minute - // Interval appropriate for network operations that should be - // retried periodically but not too often. - DefaultPollingInterval = 500 * time.Millisecond + DefaultPollingInterval = tmpnet.DefaultPollingInterval // Setting this env will disable post-test bootstrap // checks. Useful for speeding up iteration during test // development. SkipBootstrapChecksEnvName = "E2E_SKIP_BOOTSTRAP_CHECKS" - // Validator start time must be a minimum of SyncBound from the - // current time for validator addition to succeed, and adding 20 - // seconds provides a buffer in case of any delay in processing. - DefaultValidatorStartTimeDiff = executor.SyncBound + 20*time.Second + DefaultValidatorStartTimeDiff = tmpnet.DefaultValidatorStartTimeDiff DefaultGasLimit = uint64(21000) // Standard gas limit @@ -207,10 +201,10 @@ func CheckBootstrapIsPossible(network *tmpnet.Network) { } // Start a temporary network with the provided avalanchego binary. -func StartNetwork(avalancheGoExecPath string, rootNetworkDir string) *tmpnet.Network { +func StartNetwork(avalancheGoExecPath string, pluginDir string, rootNetworkDir string) *tmpnet.Network { require := require.New(ginkgo.GinkgoT()) - network, err := tmpnet.NewDefaultNetwork(ginkgo.GinkgoWriter, avalancheGoExecPath, tmpnet.DefaultNodeCount) + network, err := tmpnet.NewDefaultNetwork(ginkgo.GinkgoWriter, avalancheGoExecPath, pluginDir, tmpnet.DefaultNodeCount) require.NoError(err) require.NoError(network.Create(rootNetworkDir)) require.NoError(network.Start(DefaultContext(), ginkgo.GinkgoWriter)) diff --git a/tests/fixture/tmpnet/README.md b/tests/fixture/tmpnet/README.md index 0b923704c0ef..e78b70789ac2 100644 --- a/tests/fixture/tmpnet/README.md +++ b/tests/fixture/tmpnet/README.md @@ -34,6 +34,7 @@ the following non-test files: | node.go | Node | Orchestrates and configures nodes | | node_config.go | Node | Reads and writes node configuration | | node_process.go | NodeProcess | Orchestrates node processes | +| subnet.go | Subnet | Orchestrates subnets | | utils.go | | Defines shared utility functions | ## Usage @@ -108,6 +109,10 @@ avalanchego on node start. The use of dynamic ports supports testing with many temporary networks without having to manually select compatible port ranges. +## Subnet configuration + +TODO(marun) + ## Configuration on disk A temporary network relies on configuration written to disk in the following structure: @@ -130,11 +135,16 @@ HOME │ │ └── ... │ └── process.json // Node process details (PID, API URI, staking address) ├── chains - │ └── C - │ └── config.json // C-Chain config for all nodes + │ ├── C + │ │ └── config.json // C-Chain config for all nodes + │ └── raZ51bwfepaSaZ1MNSRNYNs3ZPfj...U7pa3 + │ └── config.json // Custom chain configuration for all nodes ├── config.json // Common configuration (including defaults and pre-funded keys) ├── genesis.json // Genesis for all nodes - └── network.env // Sets network dir env var to simplify network usage + ├── network.env // Sets network dir env var to simplify network usage + └── subnets // Parent directory for subnet definitions + ├─ subnet-a.json // Configuration for subnet-a and its chain(s) + └─ subnet-b.json // Configuration for subnet-b and its chain(s) ``` ### Common networking configuration diff --git a/tests/fixture/tmpnet/cmd/main.go b/tests/fixture/tmpnet/cmd/main.go index 2e9998ed85a5..d61cca22ce72 100644 --- a/tests/fixture/tmpnet/cmd/main.go +++ b/tests/fixture/tmpnet/cmd/main.go @@ -26,10 +26,12 @@ var ( ) func main() { + var networkDir string rootCmd := &cobra.Command{ Use: "tmpnetctl", Short: "tmpnetctl commands", } + rootCmd.PersistentFlags().StringVar(&networkDir, "network-dir", os.Getenv(tmpnet.NetworkDirEnvName), "The path to the configuration directory of a temporary network") versionCmd := &cobra.Command{ Use: "version", @@ -46,21 +48,22 @@ func main() { rootCmd.AddCommand(versionCmd) var ( - rootDir string - execPath string - nodeCount uint8 + rootDir string + avalancheGoPath string + pluginDir string + nodeCount uint8 ) startNetworkCmd := &cobra.Command{ Use: "start-network", Short: "Start a new temporary network", RunE: func(*cobra.Command, []string) error { - if len(execPath) == 0 { + if len(avalancheGoPath) == 0 { return errAvalancheGoRequired } // Root dir will be defaulted on start if not provided - network, err := tmpnet.NewDefaultNetwork(os.Stdout, execPath, int(nodeCount)) + network, err := tmpnet.NewDefaultNetwork(os.Stdout, avalancheGoPath, pluginDir, int(nodeCount)) if err != nil { return err } @@ -98,11 +101,11 @@ func main() { }, } startNetworkCmd.PersistentFlags().StringVar(&rootDir, "root-dir", os.Getenv(tmpnet.RootDirEnvName), "The path to the root directory for temporary networks") - startNetworkCmd.PersistentFlags().StringVar(&execPath, "avalanchego-path", os.Getenv(tmpnet.AvalancheGoPathEnvName), "The path to an avalanchego binary") + 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") rootCmd.AddCommand(startNetworkCmd) - var networkDir string stopNetworkCmd := &cobra.Command{ Use: "stop-network", Short: "Stop a temporary network", @@ -119,9 +122,22 @@ func main() { return nil }, } - stopNetworkCmd.PersistentFlags().StringVar(&networkDir, "network-dir", os.Getenv(tmpnet.NetworkDirEnvName), "The path to the configuration directory of a temporary network") rootCmd.AddCommand(stopNetworkCmd) + restartNetworkCmd := &cobra.Command{ + Use: "restart-network", + Short: "Restart a temporary network", + RunE: func(*cobra.Command, []string) error { + if len(networkDir) == 0 { + return errNetworkDirRequired + } + ctx, cancel := context.WithTimeout(context.Background(), tmpnet.DefaultNetworkTimeout) + defer cancel() + return tmpnet.RestartNetwork(ctx, os.Stdout, networkDir) + }, + } + rootCmd.AddCommand(restartNetworkCmd) + if err := rootCmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "tmpnetctl failed: %v\n", err) os.Exit(1) diff --git a/tests/fixture/tmpnet/defaults.go b/tests/fixture/tmpnet/defaults.go index 1112e3345307..96a3066abee9 100644 --- a/tests/fixture/tmpnet/defaults.go +++ b/tests/fixture/tmpnet/defaults.go @@ -7,9 +7,19 @@ import ( "time" "github.com/ava-labs/avalanchego/config" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" ) const ( + // Interval appropriate for network operations that should be + // retried periodically but not too often. + DefaultPollingInterval = 500 * time.Millisecond + + // Validator start time must be a minimum of SyncBound from the + // current time for validator addition to succeed, and adding 20 + // seconds provides a buffer in case of any delay in processing. + DefaultValidatorStartTimeDiff = executor.SyncBound + 20*time.Second + DefaultNetworkTimeout = 2 * time.Minute // Minimum required to ensure connectivity-based health checks will pass diff --git a/tests/fixture/tmpnet/genesis.go b/tests/fixture/tmpnet/genesis.go index 5ee605702482..452956d41cd2 100644 --- a/tests/fixture/tmpnet/genesis.go +++ b/tests/fixture/tmpnet/genesis.go @@ -105,8 +105,8 @@ func NewTestGenesis( }, StartTime: uint64(now.Unix()), InitialStakedFunds: []string{stakeAddress}, - InitialStakeDuration: 365 * 24 * 60 * 60, // 1 year - InitialStakeDurationOffset: 90 * 60, // 90 minutes + InitialStakeDuration: uint64(genesis.LocalParams.MaxStakeDuration.Seconds()), + InitialStakeDurationOffset: 90 * 60, // 90 minutes Message: "hello avalanche!", InitialStakers: initialStakers, } diff --git a/tests/fixture/tmpnet/network.go b/tests/fixture/tmpnet/network.go index 8c44e1a026d4..bd331134f2a3 100644 --- a/tests/fixture/tmpnet/network.go +++ b/tests/fixture/tmpnet/network.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "time" "github.com/ava-labs/avalanchego/config" @@ -21,6 +22,7 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/perms" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm" ) // The Network type is defined in this file (network.go - orchestration) and @@ -56,6 +58,9 @@ type Network struct { // Nodes that constitute the network Nodes []*Node + + // Subnets that have been enabled on the network + Subnets []*Subnet } // Ensure a real and absolute network dir so that node @@ -70,7 +75,7 @@ func toCanonicalDir(dir string) (string, error) { } // Initializes a new network with default configuration. -func NewDefaultNetwork(w io.Writer, avalancheGoPath string, nodeCount int) (*Network, error) { +func NewDefaultNetwork(w io.Writer, avalancheGoPath string, pluginDir string, nodeCount int) (*Network, error) { if _, err := fmt.Fprintf(w, "Preparing configuration for new network with %s\n", avalancheGoPath); err != nil { return nil, err } @@ -88,6 +93,7 @@ func NewDefaultNetwork(w io.Writer, avalancheGoPath string, nodeCount int) (*Net PreFundedKeys: keys, ChainConfigs: DefaultChainConfigs(), } + network.DefaultFlags[config.PluginDirKey] = pluginDir network.Nodes = make([]*Node, nodeCount) for i := range network.Nodes { @@ -109,6 +115,15 @@ func StopNetwork(ctx context.Context, dir string) error { return network.Stop(ctx) } +// Restarts the nodes of the network configured in the provided directory. +func RestartNetwork(ctx context.Context, w io.Writer, dir string) error { + network, err := ReadNetwork(dir) + if err != nil { + return err + } + return network.Restart(ctx, w) +} + // Reads a network from the provided directory. func ReadNetwork(dir string) (*Network, error) { canonicalDir, err := toCanonicalDir(dir) @@ -320,6 +335,28 @@ func (n *Network) Stop(ctx context.Context) error { return nil } +// Restarts all non-ephemeral nodes in the network. +func (n *Network) Restart(ctx context.Context, w io.Writer) error { + if _, err := fmt.Fprintf(w, " restarting network\n"); err != nil { + return err + } + for _, node := range n.Nodes { + if err := node.Stop(ctx); err != nil { + return fmt.Errorf("failed to stop node %s: %w", node.NodeID, err) + } + if err := n.StartNode(ctx, w, node); err != nil { + return fmt.Errorf("failed to start node %s: %w", node.NodeID, err) + } + if _, err := fmt.Fprintf(w, " waiting for node %s to report healthy\n", node.NodeID); err != nil { + return err + } + if err := WaitForHealthy(ctx, node); err != nil { + return err + } + } + return nil +} + // Ensures the provided node has the configuration it needs to start. If the data dir is not // set, it will be defaulted to [nodeParentDir]/[node ID]. For a not-yet-created network, // no action will be taken. @@ -362,6 +399,110 @@ func (n *Network) EnsureNodeConfig(node *Node) error { } } + // Ensure available subnets are tracked + subnetIDs := make([]string, 0, len(n.Subnets)) + for _, subnet := range n.Subnets { + if subnet.SubnetID == ids.Empty { + continue + } + subnetIDs = append(subnetIDs, subnet.SubnetID.String()) + } + subnetIDsValue := "" + if len(subnetIDs) > 0 { + subnetIDsValue = strings.Join(subnetIDs, ",") + } + flags[config.TrackSubnetsKey] = subnetIDsValue + + return nil +} + +func (n *Network) GetSubnet(name string) *Subnet { + for _, subnet := range n.Subnets { + if subnet.Name == name { + return subnet + } + } + return nil +} + +// Create each subnet on the network and ensure it is validated by all non-ephemeral nodes in the +// network. +func (n *Network) CreateSubnets(ctx context.Context, w io.Writer, subnets []*Subnet) error { + createdSubnets := make([]*Subnet, 0, len(subnets)) + for _, subnet := range subnets { + if existingSubnet := n.GetSubnet(subnet.Name); existingSubnet != nil { + // The subnet already exists + continue + } + + if subnet.OwningKey == nil { + // Allocate a pre-funded key and remove it from the network so it won't be used for + // other purposes + if len(n.PreFundedKeys) == 0 { + return fmt.Errorf("no pre-funded keys available to create subnet %s", subnet.Name) + } + subnet.OwningKey = n.PreFundedKeys[len(n.PreFundedKeys)-1] + n.PreFundedKeys = n.PreFundedKeys[:len(n.PreFundedKeys)-1] + } + + wallet, err := subnet.GetWallet(ctx, n.Nodes[0].URI) + if err != nil { + return err + } + pWallet := wallet.P() + + // Create the subnet and its chains on the network + if err := subnet.Create(ctx, pWallet); err != nil { + return err + } + + // Persist the subnet and chain configuration + if err := subnet.Write(n.getSubnetDir(), n.getChainConfigDir()); err != nil { + return err + } + + createdSubnets = append(createdSubnets, subnet) + } + + if len(createdSubnets) == 0 { + return nil + } + + // Ensure the in-memory subnet state + n.Subnets = append(n.Subnets, createdSubnets...) + + // Ensure the pre-funded key changes are persisted to disk + if err := n.Write(); err != nil { + return err + } + + // Reconfigure nodes for the new subnets and their chains + for _, node := range n.Nodes { + if err := n.EnsureNodeConfig(node); err != nil { + return err + } + } + + // Restart nodes to allow new configuration to take effect + if err := n.Restart(ctx, w); err != nil { + return err + } + + // Add each node as a validator + for _, subnet := range createdSubnets { + if err := subnet.AddValidators(ctx, n.Nodes); err != nil { + return err + } + } + + // Wait for nodes to become validators of the network + pChainClient := platformvm.NewClient(n.Nodes[0].URI) + for _, subnet := range subnets { + if err := waitForActiveValidators(ctx, w, pChainClient, subnet, n.Nodes); err != nil { + return err + } + } + return nil } diff --git a/tests/fixture/tmpnet/network_config.go b/tests/fixture/tmpnet/network_config.go index 5d89ddc1716d..4cf61f6f6ed2 100644 --- a/tests/fixture/tmpnet/network_config.go +++ b/tests/fixture/tmpnet/network_config.go @@ -25,7 +25,10 @@ func (n *Network) Read() error { if err := n.readNetwork(); err != nil { return err } - return n.readNodes() + if err := n.readNodes(); err != nil { + return err + } + return n.readSubnets() } // Write network configuration to disk. @@ -218,3 +221,16 @@ func (n *Network) writeEnvFile() error { } return nil } + +func (n *Network) getSubnetDir() string { + return filepath.Join(n.Dir, defaultSubnetDirName) +} + +func (n *Network) readSubnets() error { + subnets, err := readSubnets(n.getSubnetDir()) + if err != nil { + return err + } + n.Subnets = subnets + return nil +} diff --git a/tests/fixture/tmpnet/network_test.go b/tests/fixture/tmpnet/network_test.go index 24c9c783992b..af95ed35fcc2 100644 --- a/tests/fixture/tmpnet/network_test.go +++ b/tests/fixture/tmpnet/network_test.go @@ -15,7 +15,7 @@ func TestNetworkSerialization(t *testing.T) { tmpDir := t.TempDir() - network, err := NewDefaultNetwork(&bytes.Buffer{}, "/path/to/avalanche/go", 1) + network, err := NewDefaultNetwork(&bytes.Buffer{}, "/path/to/avalanche/go", "/plugin/path", 1) require.NoError(err) require.NoError(network.Create(tmpDir)) diff --git a/tests/fixture/tmpnet/subnet.go b/tests/fixture/tmpnet/subnet.go new file mode 100644 index 000000000000..2bceae2e3ebf --- /dev/null +++ b/tests/fixture/tmpnet/subnet.go @@ -0,0 +1,283 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tmpnet + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/perms" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/chain/p" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" +) + +const defaultSubnetDirName = "subnets" + +type Chain struct { + // Set statically + VMName string + Config string + Genesis []byte + + // Set at runtime + ChainID ids.ID + PreFundedKey *secp256k1.PrivateKey +} + +// Write the chain configuration to the specified directory. +func (c *Chain) WriteConfig(chainDir string) error { + if len(c.Config) == 0 { + return nil + } + + chainConfigDir := filepath.Join(chainDir, c.ChainID.String()) + if err := os.MkdirAll(chainConfigDir, perms.ReadWriteExecute); err != nil { + return fmt.Errorf("failed to create chain config dir: %w", err) + } + + path := filepath.Join(chainConfigDir, defaultConfigFilename) + if err := os.WriteFile(path, []byte(c.Config), perms.ReadWrite); err != nil { + return fmt.Errorf("failed to write chain config: %w", err) + } + + return nil +} + +type Subnet struct { + // A unique string that can be used to refer to the subnet across different temporary + // networks (since the SubnetID will be different every time the subnet is created) + Name string + + // The ID of the transaction that created the subnet + SubnetID ids.ID + + // The private key that owns the subnet + OwningKey *secp256k1.PrivateKey + + Chains []*Chain +} + +// Retrieves a wallet configured for use with the subnet +func (s *Subnet) GetWallet(ctx context.Context, uri string) (primary.Wallet, error) { + keychain := secp256k1fx.NewKeychain(s.OwningKey) + + // Only fetch the subnet transaction if a subnet ID is present. This won't be true when + // the wallet is first used to create the subnet. + txIDs := set.Set[ids.ID]{} + if s.SubnetID != ids.Empty { + txIDs.Add(s.SubnetID) + } + + return primary.MakeWallet(ctx, &primary.WalletConfig{ + URI: uri, + AVAXKeychain: keychain, + EthKeychain: keychain, + PChainTxsToFetch: txIDs, + }) +} + +// Issues the subnet creation transaction and retains the result. The URI of a node is +// required to issue the transaction. +func (s *Subnet) Create(ctx context.Context, pWallet p.Wallet) error { + owner := &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + s.OwningKey.Address(), + }, + } + + subnetTx, err := pWallet.IssueCreateSubnetTx( + owner, + common.WithContext(ctx), + ) + if err != nil { + return fmt.Errorf("failed to create subnet %s: %w", s.Name, err) + } + s.SubnetID = subnetTx.ID() + + for _, chain := range s.Chains { + vmID, err := GetVMID(chain.VMName) + if err != nil { + return fmt.Errorf("failed to derive VM ID from its name: %w", err) + } + + createChainTx, err := pWallet.IssueCreateChainTx( + s.SubnetID, + chain.Genesis, + vmID, + nil, + chain.VMName, + common.WithContext(ctx), + ) + if err != nil { + return fmt.Errorf("failed to create chain: %w", err) + } + + chain.ChainID = createChainTx.ID() + } + return nil +} + +// Add validators to the subnet +func (s *Subnet) AddValidators(ctx context.Context, nodes []*Node) error { + wallet, err := s.GetWallet(ctx, nodes[0].URI) + if err != nil { + return err + } + pWallet := wallet.P() + + startTime := time.Now().Add(DefaultValidatorStartTimeDiff) + // Ensure an end time within bounds of the default validator end time (set to + // MaxStakeDuration) to account for interval between network start and subnet creation. + // TODO(marun) Is it possible to discover the validator end time? + endTime := startTime.Add(genesis.LocalParams.MaxStakeDuration - 2*time.Hour) + for _, node := range nodes { + _, err := pWallet.IssueAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: node.NodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: units.Schmeckle, + }, + Subnet: s.SubnetID, + }, + common.WithContext(ctx), + ) + if err != nil { + return err + } + } + return nil +} + +// Write the subnet configuration to disk +func (s *Subnet) Write(subnetDir string, chainDir string) error { + if err := os.MkdirAll(subnetDir, perms.ReadWriteExecute); err != nil { + return fmt.Errorf("failed to create subnet dir: %w", err) + } + path := filepath.Join(subnetDir, s.Name+".json") + _, err := os.Stat(path) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + return fmt.Errorf("a subnet with alias %s already exists", s.Name) + } + bytes, err := DefaultJSONMarshal(s) + if err != nil { + return fmt.Errorf("failed to marshal subnet %s: %w", s.Name, err) + } + if err := os.WriteFile(path, bytes, perms.ReadWrite); err != nil { + return fmt.Errorf("failed to write subnet %s: %w", s.Name, err) + } + + for _, chain := range s.Chains { + if err := chain.WriteConfig(chainDir); err != nil { + return err + } + } + + return nil +} + +func waitForActiveValidators( + ctx context.Context, + w io.Writer, + pChainClient platformvm.Client, + subnet *Subnet, + nodes []*Node, +) error { + ticker := time.NewTicker(DefaultPollingInterval) + defer ticker.Stop() + + if _, err := fmt.Fprintf(w, " "); err != nil { + return err + } + + for { + if _, err := fmt.Fprintf(w, "."); err != nil { + return err + } + validators, err := pChainClient.GetCurrentValidators(ctx, subnet.SubnetID, nil) + if err != nil { + return err + } + validatorSet := set.NewSet[ids.NodeID](len(validators)) + for _, validator := range validators { + validatorSet.Add(validator.NodeID) + } + allActive := true + for _, node := range nodes { + if !validatorSet.Contains(node.NodeID) { + allActive = false + } + } + if allActive { + if _, err := fmt.Fprintf(w, "\n saw the expected active validators of subnet %s\n", subnet.Name); err != nil { + return err + } + return nil + } + + select { + case <-ctx.Done(): + return fmt.Errorf("failed to see the expected active validators of %s before timeout", subnet.Name) + case <-ticker.C: + } + } +} + +// Reads subnets from [network dir]/subnets/[subnet alias].json +func readSubnets(subnetDir string) ([]*Subnet, error) { + if _, err := os.Stat(subnetDir); os.IsNotExist(err) { + return nil, nil + } else if err != nil { + return nil, err + } + + entries, err := os.ReadDir(subnetDir) + if err != nil { + return nil, fmt.Errorf("failed to read subnet dir: %w", err) + } + + subnets := []*Subnet{} + for _, entry := range entries { + if entry.IsDir() { + // Looking only for files + continue + } + if filepath.Ext(entry.Name()) != ".json" { + // Subnet files should have a .json extension + continue + } + + subnetPath := filepath.Join(subnetDir, entry.Name()) + bytes, err := os.ReadFile(subnetPath) + if err != nil { + return nil, fmt.Errorf("failed to read subnet file %s: %w", subnetPath, err) + } + subnet := &Subnet{} + if err := json.Unmarshal(bytes, subnet); err != nil { + return nil, fmt.Errorf("failed to unmarshal subnet from %s: %w", subnetPath, err) + } + subnets = append(subnets, subnet) + } + + return subnets, nil +} diff --git a/tests/fixture/tmpnet/utils.go b/tests/fixture/tmpnet/utils.go index 74e6e3737069..6c2b9b50f29b 100644 --- a/tests/fixture/tmpnet/utils.go +++ b/tests/fixture/tmpnet/utils.go @@ -87,3 +87,12 @@ func NewPrivateKeys(keyCount int) ([]*secp256k1.PrivateKey, error) { } return keys, nil } + +func GetVMID(vmName string) (ids.ID, error) { + if len(vmName) > 32 { + return ids.Empty, fmt.Errorf("VM name must be <= 32 bytes, found %d", len(vmName)) + } + b := make([]byte, 32) + copy(b, []byte(vmName)) + return ids.ToID(b) +} diff --git a/tests/upgrade/upgrade_test.go b/tests/upgrade/upgrade_test.go index 2a9421872b08..279e43c80120 100644 --- a/tests/upgrade/upgrade_test.go +++ b/tests/upgrade/upgrade_test.go @@ -46,7 +46,7 @@ var _ = ginkgo.Describe("[Upgrade]", func() { require := require.New(ginkgo.GinkgoT()) ginkgo.It("can upgrade versions", func() { - network := e2e.StartNetwork(avalancheGoExecPath, e2e.DefaultNetworkDir) + network := e2e.StartNetwork(avalancheGoExecPath, "" /* pluginDir */, e2e.DefaultNetworkDir) ginkgo.By(fmt.Sprintf("restarting all nodes with %q binary", avalancheGoExecPathToUpgradeTo)) for _, node := range network.Nodes {