From 5bb8d4106dcf69832a34610b7e48c200b91dbc9a Mon Sep 17 00:00:00 2001 From: Maru Newby Date: Sat, 9 Dec 2023 20:58:24 -0800 Subject: [PATCH] tmpnet: Merge tmpnet/local to tmpnet package This is the first step in a refactor in support of deploying temporary networks to kubernetes. --- tests/e2e/c/dynamic_fees.go | 8 +- tests/e2e/faultinjection/duplicate_node_id.go | 18 +- tests/e2e/p/interchain_workflow.go | 4 +- tests/e2e/p/staking_rewards.go | 8 +- tests/fixture/e2e/env.go | 19 +- tests/fixture/e2e/flags.go | 10 +- tests/fixture/e2e/helpers.go | 25 +- tests/fixture/tmpnet/README.md | 217 +++++++++++- tests/fixture/tmpnet/cmd/main.go | 27 +- tests/fixture/tmpnet/config.go | 15 +- .../tmpnet/{local/config.go => defaults.go} | 19 +- tests/fixture/tmpnet/interfaces.go | 28 -- tests/fixture/tmpnet/local/README.md | 219 ------------ tests/fixture/tmpnet/{local => }/network.go | 319 +++++++++--------- .../tmpnet/{local => }/network_test.go | 6 +- tests/fixture/tmpnet/{local => }/node.go | 113 +++---- tests/fixture/tmpnet/{local => }/node_test.go | 4 +- tests/fixture/tmpnet/{common.go => utils.go} | 14 +- tests/upgrade/upgrade_test.go | 7 +- 19 files changed, 499 insertions(+), 581 deletions(-) rename tests/fixture/tmpnet/{local/config.go => defaults.go} (77%) delete mode 100644 tests/fixture/tmpnet/interfaces.go delete mode 100644 tests/fixture/tmpnet/local/README.md rename tests/fixture/tmpnet/{local => }/network.go (63%) rename tests/fixture/tmpnet/{local => }/network_test.go (83%) rename tests/fixture/tmpnet/{local => }/node.go (69%) rename tests/fixture/tmpnet/{local => }/node_test.go (90%) rename tests/fixture/tmpnet/{common.go => utils.go} (73%) diff --git a/tests/e2e/c/dynamic_fees.go b/tests/e2e/c/dynamic_fees.go index 38bbd668079b..57adb7cedd4c 100644 --- a/tests/e2e/c/dynamic_fees.go +++ b/tests/e2e/c/dynamic_fees.go @@ -42,14 +42,14 @@ var _ = e2e.DescribeCChain("[Dynamic Fees]", func() { privateNetwork := e2e.Env.NewPrivateNetwork() ginkgo.By("allocating a pre-funded key") - key := privateNetwork.GetConfig().FundedKeys[0] + key := privateNetwork.FundedKeys[0] ethAddress := evm.GetEthAddress(key) ginkgo.By("initializing a coreth client") - node := privateNetwork.GetNodes()[0] + node := privateNetwork.Nodes[0] nodeURI := tmpnet.NodeURI{ - NodeID: node.GetID(), - URI: node.GetProcessContext().URI, + ID: node.ID, + URI: node.NodeProcessContext.URI, } ethClient := e2e.NewEthClient(nodeURI) diff --git a/tests/e2e/faultinjection/duplicate_node_id.go b/tests/e2e/faultinjection/duplicate_node_id.go index 9278c1bd5b8d..18ae70d3568c 100644 --- a/tests/e2e/faultinjection/duplicate_node_id.go +++ b/tests/e2e/faultinjection/duplicate_node_id.go @@ -24,17 +24,16 @@ var _ = ginkgo.Describe("Duplicate node handling", func() { ginkgo.It("should ensure that a given Node ID (i.e. staking keypair) can be used at most once on a network", func() { network := e2e.Env.GetNetwork() - nodes := network.GetNodes() ginkgo.By("creating new node") node1 := e2e.AddEphemeralNode(network, tmpnet.FlagsMap{}) e2e.WaitForHealthy(node1) ginkgo.By("checking that the new node is connected to its peers") - checkConnectedPeers(nodes, node1) + checkConnectedPeers(network.Nodes, node1) ginkgo.By("creating a second new node with the same staking keypair as the first new node") - node1Flags := node1.GetConfig().Flags + node1Flags := node1.Flags node2Flags := tmpnet.FlagsMap{ config.StakingTLSKeyContentKey: node1Flags[config.StakingTLSKeyContentKey], config.StakingCertContentKey: node1Flags[config.StakingCertContentKey], @@ -56,18 +55,18 @@ var _ = ginkgo.Describe("Duplicate node handling", func() { e2e.WaitForHealthy(node2) ginkgo.By("checking that the second new node is connected to its peers") - checkConnectedPeers(nodes, node2) + checkConnectedPeers(network.Nodes, node2) // A bootstrap check was already performed by the second node. }) }) // Check that a new node is connected to existing nodes and vice versa -func checkConnectedPeers(existingNodes []tmpnet.Node, newNode tmpnet.Node) { +func checkConnectedPeers(existingNodes []*tmpnet.Node, newNode *tmpnet.Node) { require := require.New(ginkgo.GinkgoT()) // Collect the node ids of the new node's peers - infoClient := info.NewClient(newNode.GetProcessContext().URI) + infoClient := info.NewClient(newNode.NodeProcessContext.URI) peers, err := infoClient.Peers(e2e.DefaultContext()) require.NoError(err) peerIDs := set.NewSet[ids.NodeID](len(existingNodes)) @@ -75,18 +74,17 @@ func checkConnectedPeers(existingNodes []tmpnet.Node, newNode tmpnet.Node) { peerIDs.Add(peer.ID) } - newNodeID := newNode.GetID() for _, existingNode := range existingNodes { // Check that the existing node is a peer of the new node - require.True(peerIDs.Contains(existingNode.GetID())) + require.True(peerIDs.Contains(existingNode.ID)) // Check that the new node is a peer - infoClient := info.NewClient(existingNode.GetProcessContext().URI) + infoClient := info.NewClient(existingNode.NodeProcessContext.URI) peers, err := infoClient.Peers(e2e.DefaultContext()) require.NoError(err) isPeer := false for _, peer := range peers { - if peer.ID == newNodeID { + if peer.ID == newNode.ID { isPeer = true break } diff --git a/tests/e2e/p/interchain_workflow.go b/tests/e2e/p/interchain_workflow.go index 678f9b5cc204..10b6e353b88b 100644 --- a/tests/e2e/p/interchain_workflow.go +++ b/tests/e2e/p/interchain_workflow.go @@ -43,7 +43,7 @@ var _ = e2e.DescribePChain("[Interchain Workflow]", ginkgo.Label(e2e.UsesCChainL network := e2e.Env.GetNetwork() ginkgo.By("checking that the network has a compatible minimum stake duration", func() { - minStakeDuration := cast.ToDuration(network.GetConfig().DefaultFlags[config.MinStakeDurationKey]) + minStakeDuration := cast.ToDuration(network.DefaultFlags[config.MinStakeDurationKey]) require.Equal(tmpnet.DefaultMinStakeDuration, minStakeDuration) }) @@ -91,7 +91,7 @@ var _ = e2e.DescribePChain("[Interchain Workflow]", ginkgo.Label(e2e.UsesCChainL e2e.WaitForHealthy(node) ginkgo.By("retrieving new node's id and pop") - infoClient := info.NewClient(node.GetProcessContext().URI) + infoClient := info.NewClient(node.NodeProcessContext.URI) nodeID, nodePOP, err := infoClient.GetNodeID(e2e.DefaultContext()) require.NoError(err) diff --git a/tests/e2e/p/staking_rewards.go b/tests/e2e/p/staking_rewards.go index 475c3de261dd..7348ed669c22 100644 --- a/tests/e2e/p/staking_rewards.go +++ b/tests/e2e/p/staking_rewards.go @@ -42,7 +42,7 @@ var _ = ginkgo.Describe("[Staking Rewards]", func() { network := e2e.Env.GetNetwork() ginkgo.By("checking that the network has a compatible minimum stake duration", func() { - minStakeDuration := cast.ToDuration(network.GetConfig().DefaultFlags[config.MinStakeDurationKey]) + minStakeDuration := cast.ToDuration(network.DefaultFlags[config.MinStakeDurationKey]) require.Equal(tmpnet.DefaultMinStakeDuration, minStakeDuration) }) @@ -94,16 +94,16 @@ var _ = ginkgo.Describe("[Staking Rewards]", func() { pWallet := baseWallet.P() ginkgo.By("retrieving alpha node id and pop") - alphaInfoClient := info.NewClient(alphaNode.GetProcessContext().URI) + alphaInfoClient := info.NewClient(alphaNode.NodeProcessContext.URI) alphaNodeID, alphaPOP, err := alphaInfoClient.GetNodeID(e2e.DefaultContext()) require.NoError(err) ginkgo.By("retrieving beta node id and pop") - betaInfoClient := info.NewClient(betaNode.GetProcessContext().URI) + betaInfoClient := info.NewClient(betaNode.NodeProcessContext.URI) betaNodeID, betaPOP, err := betaInfoClient.GetNodeID(e2e.DefaultContext()) require.NoError(err) - pvmClient := platformvm.NewClient(alphaNode.GetProcessContext().URI) + pvmClient := platformvm.NewClient(alphaNode.NodeProcessContext.URI) const ( delegationPercent = 0.10 // 10% diff --git a/tests/fixture/e2e/env.go b/tests/fixture/e2e/env.go index 07c24866a9f0..4e0027d655c1 100644 --- a/tests/fixture/e2e/env.go +++ b/tests/fixture/e2e/env.go @@ -17,7 +17,6 @@ import ( "github.com/ava-labs/avalanchego/tests" "github.com/ava-labs/avalanchego/tests/fixture" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" - "github.com/ava-labs/avalanchego/tests/fixture/tmpnet/local" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/perms" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -60,14 +59,14 @@ func NewTestEnvironment(flagVars *FlagVars) *TestEnvironment { networkDir := flagVars.NetworkDir() // Load or create a test network - var network *local.LocalNetwork + var network *tmpnet.Network if len(networkDir) > 0 { var err error - network, err = local.ReadNetwork(networkDir) + network, err = tmpnet.ReadNetwork(networkDir) require.NoError(err) tests.Outf("{{yellow}}Using an existing network configured at %s{{/}}\n", network.Dir) } else { - network = StartLocalNetwork(flagVars.AvalancheGoExecPath(), DefaultNetworkDir) + network = StartNetwork(flagVars.AvalancheGoExecPath(), DefaultNetworkDir) } uris := network.GetURIs() @@ -92,13 +91,13 @@ func NewTestEnvironment(flagVars *FlagVars) *TestEnvironment { func (te *TestEnvironment) GetRandomNodeURI() tmpnet.NodeURI { r := rand.New(rand.NewSource(time.Now().Unix())) //#nosec G404 nodeURI := te.URIs[r.Intn(len(te.URIs))] - tests.Outf("{{blue}} targeting node %s with URI: %s{{/}}\n", nodeURI.NodeID, nodeURI.URI) + tests.Outf("{{blue}} targeting node %s with URI: %s{{/}}\n", nodeURI.ID, nodeURI.URI) return nodeURI } // Retrieve the network to target for testing. -func (te *TestEnvironment) GetNetwork() tmpnet.Network { - network, err := local.ReadNetwork(te.NetworkDir) +func (te *TestEnvironment) GetNetwork() *tmpnet.Network { + network, err := tmpnet.ReadNetwork(te.NetworkDir) te.require.NoError(err) return network } @@ -123,9 +122,9 @@ func (te *TestEnvironment) NewKeychain(count int) *secp256k1fx.Keychain { } // Create a new private network that is not shared with other tests. -func (te *TestEnvironment) NewPrivateNetwork() tmpnet.Network { +func (te *TestEnvironment) NewPrivateNetwork() *tmpnet.Network { // Load the shared network to retrieve its path and exec path - sharedNetwork, err := local.ReadNetwork(te.NetworkDir) + sharedNetwork, err := tmpnet.ReadNetwork(te.NetworkDir) te.require.NoError(err) // The private networks dir is under the shared network dir to ensure it @@ -133,5 +132,5 @@ func (te *TestEnvironment) NewPrivateNetwork() tmpnet.Network { privateNetworksDir := filepath.Join(sharedNetwork.Dir, PrivateNetworksDirName) te.require.NoError(os.MkdirAll(privateNetworksDir, perms.ReadWriteExecute)) - return StartLocalNetwork(sharedNetwork.ExecPath, privateNetworksDir) + return StartNetwork(sharedNetwork.ExecPath, privateNetworksDir) } diff --git a/tests/fixture/e2e/flags.go b/tests/fixture/e2e/flags.go index 23952b5dcd91..b1354473fe86 100644 --- a/tests/fixture/e2e/flags.go +++ b/tests/fixture/e2e/flags.go @@ -8,7 +8,7 @@ import ( "fmt" "os" - "github.com/ava-labs/avalanchego/tests/fixture/tmpnet/local" + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" ) type FlagVars struct { @@ -24,7 +24,7 @@ func (v *FlagVars) NetworkDir() string { if len(v.networkDir) > 0 { return v.networkDir } - return os.Getenv(local.NetworkDirEnvName) + return os.Getenv(tmpnet.NetworkDirEnvName) } func (v *FlagVars) AvalancheGoExecPath() string { @@ -40,14 +40,14 @@ func RegisterFlags() *FlagVars { flag.StringVar( &vars.avalancheGoExecPath, "avalanchego-path", - os.Getenv(local.AvalancheGoPathEnvName), - fmt.Sprintf("avalanchego executable path (required if not using an existing network). Also possible to configure via the %s env variable.", local.AvalancheGoPathEnvName), + 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.networkDir, "network-dir", "", - fmt.Sprintf("[optional] the dir containing the configuration of an existing network to target for testing. Will only be used if --use-existing-network is specified. Also possible to configure via the %s env variable.", local.NetworkDirEnvName), + fmt.Sprintf("[optional] the dir containing the configuration of an existing network to target for testing. Will only be used if --use-existing-network is specified. Also possible to configure via the %s env variable.", tmpnet.NetworkDirEnvName), ) flag.BoolVar( &vars.useExistingNetwork, diff --git a/tests/fixture/e2e/helpers.go b/tests/fixture/e2e/helpers.go index 8b7eb5260b8c..640536d85a66 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/tests/fixture/tmpnet/local" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary" @@ -63,7 +62,7 @@ const ( // Create a new wallet for the provided keychain against the specified node URI. func NewWallet(keychain *secp256k1fx.Keychain, nodeURI tmpnet.NodeURI) primary.Wallet { - tests.Outf("{{blue}} initializing a new wallet for node %s with URI: %s {{/}}\n", nodeURI.NodeID, nodeURI.URI) + tests.Outf("{{blue}} initializing a new wallet for node %s with URI: %s {{/}}\n", nodeURI.ID, nodeURI.URI) baseWallet, err := primary.MakeWallet(DefaultContext(), &primary.WalletConfig{ URI: nodeURI.URI, AVAXKeychain: keychain, @@ -82,7 +81,7 @@ func NewWallet(keychain *secp256k1fx.Keychain, nodeURI tmpnet.NodeURI) primary.W // Create a new eth client targeting the specified node URI. func NewEthClient(nodeURI tmpnet.NodeURI) ethclient.Client { - tests.Outf("{{blue}} initializing a new eth client for node %s with URI: %s {{/}}\n", nodeURI.NodeID, nodeURI.URI) + tests.Outf("{{blue}} initializing a new eth client for node %s with URI: %s {{/}}\n", nodeURI.ID, nodeURI.URI) nodeAddress := strings.Split(nodeURI.URI, "//")[1] uri := fmt.Sprintf("ws://%s/ext/bc/C/ws", nodeAddress) client, err := ethclient.Dial(uri) @@ -128,7 +127,7 @@ func Eventually(condition func() bool, waitFor time.Duration, tick time.Duration // Add an ephemeral node that is only intended to be used by a single test. Its ID and // URI are not intended to be returned from the Network instance to minimize // accessibility from other tests. -func AddEphemeralNode(network tmpnet.Network, flags tmpnet.FlagsMap) tmpnet.Node { +func AddEphemeralNode(network *tmpnet.Network, flags tmpnet.FlagsMap) *tmpnet.Node { require := require.New(ginkgo.GinkgoT()) node, err := network.AddEphemeralNode(ginkgo.GinkgoWriter, flags) @@ -137,7 +136,7 @@ func AddEphemeralNode(network tmpnet.Network, flags tmpnet.FlagsMap) tmpnet.Node // Ensure node is stopped on teardown. It's configuration is not removed to enable // collection in CI to aid in troubleshooting failures. ginkgo.DeferCleanup(func() { - tests.Outf("Shutting down ephemeral node %s\n", node.GetID()) + tests.Outf("Shutting down ephemeral node %s\n", node.ID) require.NoError(node.Stop()) }) @@ -145,7 +144,7 @@ func AddEphemeralNode(network tmpnet.Network, flags tmpnet.FlagsMap) tmpnet.Node } // Wait for the given node to report healthy. -func WaitForHealthy(node tmpnet.Node) { +func WaitForHealthy(node *tmpnet.Node) { // Need to use explicit context (vs DefaultContext()) to support use with DeferCleanup ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) defer cancel() @@ -197,7 +196,7 @@ func WithSuggestedGasPrice(ethClient ethclient.Client) common.Option { } // Verify that a new node can bootstrap into the network. -func CheckBootstrapIsPossible(network tmpnet.Network) { +func CheckBootstrapIsPossible(network *tmpnet.Network) { require := require.New(ginkgo.GinkgoT()) if len(os.Getenv(SkipBootstrapChecksEnvName)) > 0 { @@ -214,23 +213,23 @@ func CheckBootstrapIsPossible(network tmpnet.Network) { require.NoError(err) defer func() { - tests.Outf("Shutting down ephemeral node %s\n", node.GetID()) + tests.Outf("Shutting down ephemeral node %s\n", node.ID) require.NoError(node.Stop()) }() WaitForHealthy(node) } -// Start a local test-managed network with the provided avalanchego binary. -func StartLocalNetwork(avalancheGoExecPath string, networkDir string) *local.LocalNetwork { +// Start a temporary network with the provided avalanchego binary. +func StartNetwork(avalancheGoExecPath string, networkDir string) *tmpnet.Network { require := require.New(ginkgo.GinkgoT()) - network, err := local.StartNetwork( + network, err := tmpnet.StartNetwork( DefaultContext(), ginkgo.GinkgoWriter, networkDir, - &local.LocalNetwork{ - LocalConfig: local.LocalConfig{ + &tmpnet.Network{ + NodeRuntimeConfig: tmpnet.NodeRuntimeConfig{ ExecPath: avalancheGoExecPath, }, }, diff --git a/tests/fixture/tmpnet/README.md b/tests/fixture/tmpnet/README.md index ca48d553105e..db07852c9551 100644 --- a/tests/fixture/tmpnet/README.md +++ b/tests/fixture/tmpnet/README.md @@ -1,11 +1,11 @@ # tmpnet (temporary network fixture) -This package contains configuration and interfaces that are -independent of a given orchestration mechanism -(e.g. [local](local/README.md)). The intent is to enable tests to be -written against the interfaces defined in this package and for -implementation-specific details of test network orchestration to be -limited to test setup and teardown. +This package implements a simple orchestrator for the avalanchego +nodes of a temporary network. Configuration is stored on disk, and +nodes run as independent processes whose process details are also +written to disk. Using the filesystem to store configuration and +process details allows for the `tmpnetctl` cli and e2e test fixture to +orchestrate the same temporary networks without the use of an rpc daemon. ## What's in a name? @@ -18,3 +18,208 @@ To avoid confusion, the name was changed to `tmpnet` and its cli networks it deploys are likely to live for a limited duration in support of the development and testing of avalanchego and its related repositories. + +## Package details + +The functionality in this package is grouped by logical purpose into +the following non-test files: + +| Filename | Types | Purpose | +|:------------|:--------|:----------------------------------------------| +| defaults.go | | Default configuration | +| network.go | Network | Network-level orchestration and configuration | +| node.go | Node | Node-level orchestration and configuration | +| util.go | | Shared utility functions | + +## Usage + +### Via tmpnetctl + +A temporary network can be managed by the `tmpnetctl` cli tool: + +```bash +# From the root of the avalanchego repo + +# Build the tmpnetctl binary +$ ./scripts/build_tmpnetctl.sh + +# Start a new network +$ ./build/tmpnetctl start-network --avalanchego-path=/path/to/avalanchego +... +Started network 1000 @ /home/me/.tmpnet/networks/1000 + +Configure tmpnetctl to target this network by default with one of the following statements: + - source /home/me/.tmpnet/networks/1000/network.env + - export TMPNET_NETWORK_DIR=/home/me/.tmpnet/networks/1000 + - export TMPNET_NETWORK_DIR=/home/me/.tmpnet/networks/latest + +# Stop the network +$ ./build/tmpnetctl stop-network --network-dir=/path/to/network +``` + +Note the export of the path ending in `latest`. This is a symlink that +is set to the last network created by `tmpnetctl start-network`. Setting +the `TMPNET_NETWORK_DIR` env var to this symlink ensures that +`tmpnetctl` commands and e2e execution with +`--use-existing-network` will target the most recently deployed temporary +network. + +### Via code + +A temporary network can be managed in code: + +```golang +network, _ := tmpnet.StartNetwork( + ctx, // Context used to limit duration of waiting for network health + ginkgo.GinkgoWriter, // Writer to report progress of network start + "", // Use default root dir (~/.tmpnet) + &tmpnet.Network{ + DefaultRuntime: tmpnet.NodeRuntimeConfig{ + ExecPath: "/path/to/avalanchego", // Defining the avalanchego exec path is required + }, + }, + 5, // Number of initial validating nodes + 50, // Number of pre-funded keys to create +) + +uris := network.GetURIs() + +// Use URIs to interact with the network + +// Stop all nodes in the network +network.Stop() +``` + +If non-default node behavior is required, the `Network` instance +supplied to `StartNetwork()` can be initialized with explicit node +configuration and by supplying a nodeCount argument of `0`: + +```golang +network, _ := tmpnet.StartNetwork( + ctx, + ginkgo.GinkgoWriter, + "", + &tmpnet.Network{ + DefaultRuntime: tmpnet.NodeRuntimeConfig{ + ExecPath: "/path/to/avalanchego", + }, + Nodes: []*Node{ + { // node1 configuration is customized + Flags: FlagsMap{ // Any and all node flags can be configured here + config.DataDirKey: "/custom/path/to/node/data", + } + }, + }, + {}, // node2 uses default configuration + {}, // node3 uses default configuration + {}, // node4 uses default configuration + {}, // node5 uses default configuration + }, + 0, // Node count must be zero when setting node config + 50, +) +``` + +Further examples of code-based usage are located in the [e2e +tests](../../../e2e/e2e_test.go). + +## Networking configuration + +By default, nodes in a temporary network will be started with staking and +API ports set to `0` to ensure that ports will be dynamically +chosen. The tmpnet fixture discovers the ports used by a given node +by reading the `[base-data-dir]/process.json` file written by +avalanchego on node start. The use of dynamic ports supports testing +with many temporary networks without having to manually select compatible +port ranges. + +## Configuration on disk + +A temporary network relies on configuration written to disk in the following structure: + +``` +HOME +└── .tmpnet // Root path for the temporary network fixture + └── networks // Default parent directory for temporary networks + └── 1000 // The networkID is used to name the network dir and starts at 1000 + ├── NodeID-37E8UK3x2YFsHE3RdALmfWcppcZ1eTuj9 // The ID of a node is the name of its data dir + │ ├── chainData + │ │ └── ... + │ ├── config.json // Node flags + │ ├── db + │ │ └── ... + │ ├── logs + │ │ └── ... + │ ├── plugins + │ │ └── ... + │ └── process.json // Node process details (PID, API URI, staking address) + ├── chains + │ └── C + │ └── config.json // C-Chain config for all nodes + ├── defaults.json // Default flags and configuration for network + ├── genesis.json // Genesis for all nodes + ├── network.env // Sets network dir env to simplify use of network + └── ephemeral // Parent directory for ephemeral nodes (e.g. created by tests) + └─ NodeID-FdxnAvr4jK9XXAwsYZPgWAHW2QnwSZ // Data dir for an ephemeral node + └── ... + +``` + +### Default flags and configuration + +The default avalanchego node flags (e.g. `--staking-port=`) and +default configuration like the avalanchego path are stored at +`[network-dir]/defaults.json`. The value for a given defaulted flag +will be set on initial and subsequently added nodes that do not supply +values for a given defaulted flag. + +### Genesis + +The genesis file is stored at `[network-dir]/genesis.json` and +referenced by default by all nodes in the network. The genesis file +content will be generated with reasonable defaults if not +supplied. Each node in the network can override the default by setting +an explicit value for `--genesis-file` or `--genesis-file-content`. + +### C-Chain config + +The C-Chain config for a temporary network is stored at +`[network-dir]/chains/C/config.json` and referenced by default by all +nodes in the network. The C-Chain config will be generated with +reasonable defaults if not supplied. Each node in the network can +override the default by setting an explicit value for +`--chain-config-dir` and ensuring the C-Chain config file exists at +`[chain-config-dir]/C/config.json`. + +TODO(marun) Enable configuration of X-Chain and P-Chain. + +### Network env + +A shell script that sets the `TMPNET_NETWORK_DIR` env var to the +path of the network is stored at `[network-dir]/network.env`. Sourcing +this file (i.e. `source network.env`) in a shell will configure ginkgo +e2e and the `tmpnetctl` cli to target the network path specified in +the env var. + +### Node configuration + +The data dir for a node is set by default to +`[network-path]/[node-id]`. A node can be configured to use a +non-default path by explicitly setting the `--data-dir` +flag. + +#### Flags + +All flags used to configure a node are written to +`[network-path]/[node-id]/config.json` so that a node can be +configured with only a single argument: +`--config-file=/path/to/config.json`. This simplifies node launch and +ensures all parameters used to launch a node can be modified by +editing the config file. + +#### Process details + +The process details of a node are written by avalanchego to +`[base-data-dir]/process.json`. The file contains the PID of the node +process, the URI of the node's API, and the address other nodes can +use to bootstrap themselves (aka staking address). diff --git a/tests/fixture/tmpnet/cmd/main.go b/tests/fixture/tmpnet/cmd/main.go index a9f5c1865291..264216802bad 100644 --- a/tests/fixture/tmpnet/cmd/main.go +++ b/tests/fixture/tmpnet/cmd/main.go @@ -14,15 +14,14 @@ import ( "github.com/spf13/cobra" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" - "github.com/ava-labs/avalanchego/tests/fixture/tmpnet/local" "github.com/ava-labs/avalanchego/version" ) const cliVersion = "0.0.1" var ( - errAvalancheGoRequired = fmt.Errorf("--avalanchego-path or %s are required", local.AvalancheGoPathEnvName) - errNetworkDirRequired = fmt.Errorf("--network-dir or %s are required", local.NetworkDirEnvName) + errAvalancheGoRequired = fmt.Errorf("--avalanchego-path or %s are required", tmpnet.AvalancheGoPathEnvName) + errNetworkDirRequired = fmt.Errorf("--network-dir or %s are required", tmpnet.NetworkDirEnvName) ) func main() { @@ -53,7 +52,7 @@ func main() { ) startNetworkCmd := &cobra.Command{ Use: "start-network", - Short: "Start a new local network", + Short: "Start a new temporary network", RunE: func(*cobra.Command, []string) error { if len(execPath) == 0 { return errAvalancheGoRequired @@ -61,14 +60,14 @@ func main() { // Root dir will be defaulted on start if not provided - network := &local.LocalNetwork{ - LocalConfig: local.LocalConfig{ + network := &tmpnet.Network{ + NodeRuntimeConfig: tmpnet.NodeRuntimeConfig{ ExecPath: execPath, }, } - ctx, cancel := context.WithTimeout(context.Background(), local.DefaultNetworkStartTimeout) + ctx, cancel := context.WithTimeout(context.Background(), tmpnet.DefaultNetworkStartTimeout) defer cancel() - network, err := local.StartNetwork(ctx, os.Stdout, rootDir, network, int(nodeCount), int(fundedKeyCount)) + network, err := tmpnet.StartNetwork(ctx, os.Stdout, rootDir, network, int(nodeCount), int(fundedKeyCount)) if err != nil { return err } @@ -87,13 +86,13 @@ func main() { fmt.Fprintf(os.Stdout, "\nConfigure tmpnetctl to target this network by default with one of the following statements:") fmt.Fprintf(os.Stdout, "\n - source %s\n", network.EnvFilePath()) fmt.Fprintf(os.Stdout, " - %s\n", network.EnvFileContents()) - fmt.Fprintf(os.Stdout, " - export %s=%s\n", local.NetworkDirEnvName, latestSymlinkPath) + fmt.Fprintf(os.Stdout, " - export %s=%s\n", tmpnet.NetworkDirEnvName, latestSymlinkPath) return nil }, } - startNetworkCmd.PersistentFlags().StringVar(&rootDir, "root-dir", os.Getenv(local.RootDirEnvName), "The path to the root directory for local networks") - startNetworkCmd.PersistentFlags().StringVar(&execPath, "avalanchego-path", os.Getenv(local.AvalancheGoPathEnvName), "The path to an avalanchego binary") + 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().Uint8Var(&nodeCount, "node-count", tmpnet.DefaultNodeCount, "Number of nodes the network should initially consist of") startNetworkCmd.PersistentFlags().Uint8Var(&fundedKeyCount, "funded-key-count", tmpnet.DefaultFundedKeyCount, "Number of funded keys the network should start with") rootCmd.AddCommand(startNetworkCmd) @@ -101,19 +100,19 @@ func main() { var networkDir string stopNetworkCmd := &cobra.Command{ Use: "stop-network", - Short: "Stop a local network", + Short: "Stop a temporary network", RunE: func(*cobra.Command, []string) error { if len(networkDir) == 0 { return errNetworkDirRequired } - if err := local.StopNetwork(networkDir); err != nil { + if err := tmpnet.StopNetwork(networkDir); err != nil { return err } fmt.Fprintf(os.Stdout, "Stopped network configured at: %s\n", networkDir) return nil }, } - stopNetworkCmd.PersistentFlags().StringVar(&networkDir, "network-dir", os.Getenv(local.NetworkDirEnvName), "The path to the configuration directory of a local network") + stopNetworkCmd.PersistentFlags().StringVar(&networkDir, "network-dir", os.Getenv(tmpnet.NetworkDirEnvName), "The path to the configuration directory of a temporary network") rootCmd.AddCommand(stopNetworkCmd) if err := rootCmd.Execute(); err != nil { diff --git a/tests/fixture/tmpnet/config.go b/tests/fixture/tmpnet/config.go index 30e439177d68..219eb5933c7b 100644 --- a/tests/fixture/tmpnet/config.go +++ b/tests/fixture/tmpnet/config.go @@ -113,11 +113,6 @@ func ReadFlagsMap(path string, description string) (*FlagsMap, error) { return flagsMap, nil } -// Marshal to json with default prefix and indent. -func DefaultJSONMarshal(v interface{}) ([]byte, error) { - return json.MarshalIndent(v, "", " ") -} - // NetworkConfig defines configuration shared or // common to all nodes in a given network. type NetworkConfig struct { @@ -158,14 +153,14 @@ func (c *NetworkConfig) EnsureGenesis(networkID uint32, initialStakers []genesis // NodeURI associates a node ID with its API URI. type NodeURI struct { - NodeID ids.NodeID - URI string + ID ids.NodeID + URI string } // NodeConfig defines configuration for an AvalancheGo node. type NodeConfig struct { - NodeID ids.NodeID - Flags FlagsMap + ID ids.NodeID + Flags FlagsMap } func NewNodeConfig() *NodeConfig { @@ -311,7 +306,7 @@ func (nc *NodeConfig) EnsureNodeID() error { return fmt.Errorf("failed to ensure node ID: failed to load tls cert: %w", err) } stakingCert := staking.CertificateFromX509(tlsCert.Leaf) - nc.NodeID = ids.NodeIDFromCert(stakingCert) + nc.ID = ids.NodeIDFromCert(stakingCert) return nil } diff --git a/tests/fixture/tmpnet/local/config.go b/tests/fixture/tmpnet/defaults.go similarity index 77% rename from tests/fixture/tmpnet/local/config.go rename to tests/fixture/tmpnet/defaults.go index 70ef9a443185..c8b5c42c13d2 100644 --- a/tests/fixture/tmpnet/local/config.go +++ b/tests/fixture/tmpnet/defaults.go @@ -1,18 +1,17 @@ // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package local +package tmpnet import ( "time" "github.com/ava-labs/avalanchego/config" - "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" ) const ( // Constants defining the names of shell variables whose value can - // configure local network orchestration. + // configure temporary network orchestration. AvalancheGoPathEnvName = "AVALANCHEGO_PATH" NetworkDirEnvName = "TMPNET_NETWORK_DIR" RootDirEnvName = "TMPNET_ROOT_DIR" @@ -22,10 +21,10 @@ const ( DefaultNodeStopTimeout = 5 * time.Second ) -// A set of flags appropriate for local testing. -func LocalFlags() tmpnet.FlagsMap { +// A set of flags appropriate for testing. +func DefaultFlags() FlagsMap { // Supply only non-default configuration to ensure that default values will be used. - return tmpnet.FlagsMap{ + return FlagsMap{ config.NetworkPeerListGossipFreqKey: "250ms", config.NetworkMaxReconnectDelayKey: "1s", config.PublicIPKey: "127.0.0.1", @@ -37,16 +36,16 @@ func LocalFlags() tmpnet.FlagsMap { config.IndexEnabledKey: true, config.LogDisplayLevelKey: "INFO", config.LogLevelKey: "DEBUG", - config.MinStakeDurationKey: tmpnet.DefaultMinStakeDuration.String(), + config.MinStakeDurationKey: DefaultMinStakeDuration.String(), } } -// C-Chain config for local testing. -func LocalCChainConfig() tmpnet.FlagsMap { +// C-Chain config for testing. +func DefaultCChainConfig() FlagsMap { // Supply only non-default configuration to ensure that default // values will be used. Available C-Chain configuration options are // defined in the `github.com/ava-labs/coreth/evm` package. - return tmpnet.FlagsMap{ + return FlagsMap{ "log-level": "trace", } } diff --git a/tests/fixture/tmpnet/interfaces.go b/tests/fixture/tmpnet/interfaces.go deleted file mode 100644 index 2fd03cbc2a98..000000000000 --- a/tests/fixture/tmpnet/interfaces.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package tmpnet - -import ( - "context" - "io" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/node" -) - -// Defines network capabilities supportable regardless of how a network is orchestrated. -type Network interface { - GetConfig() NetworkConfig - GetNodes() []Node - AddEphemeralNode(w io.Writer, flags FlagsMap) (Node, error) -} - -// Defines node capabilities supportable regardless of how a network is orchestrated. -type Node interface { - GetID() ids.NodeID - GetConfig() NodeConfig - GetProcessContext() node.NodeProcessContext - IsHealthy(ctx context.Context) (bool, error) - Stop() error -} diff --git a/tests/fixture/tmpnet/local/README.md b/tests/fixture/tmpnet/local/README.md deleted file mode 100644 index 91af35a9b805..000000000000 --- a/tests/fixture/tmpnet/local/README.md +++ /dev/null @@ -1,219 +0,0 @@ -# Local network orchestration - -This package implements a simple orchestrator for the avalanchego -nodes of a local network. Configuration is stored on disk, and nodes -run as independent processes whose process details are also written to -disk. Using the filesystem to store configuration and process details -allows for the `tmpnetctl` cli and e2e test fixture to orchestrate -the same local networks without the use of an rpc daemon. - -## Package details - -The functionality in this package is grouped by logical purpose into -the following non-test files: - -| Filename | Types | Purpose | -|:-----------|:-------------------|:----------------------------------------------| -| config.go | | Common configuration | -| network.go | LocalNetwork | Network-level orchestration and configuration | -| node.go | Local{Config,Node} | Node-level orchestration and configuration | - - -This package depends on its parent package for implementation-agnostic -network and node configuration. Only configuration and code specific -to orchestrating local networks belongs in this package to ensure that -other orchestration implementations can reuse the shared configuration -abstractions. - -## Usage - -### Via tmpnetctl - -A local network can be managed by the `tmpnetctl` cli tool: - -```bash -# From the root of the avalanchego repo - -# Build the tmpnetctl binary -$ ./scripts/build_tmpnetctl.sh - -# Start a new network -$ ./build/tmpnetctl start-network --avalanchego-path=/path/to/avalanchego -... -Started network 1000 @ /home/me/.tmpnet/networks/1000 - -Configure tmpnetctl to target this network by default with one of the following statements: - - source /home/me/.tmpnet/networks/1000/network.env - - export TMPNET_NETWORK_DIR=/home/me/.tmpnet/networks/1000 - - export TMPNET_NETWORK_DIR=/home/me/.tmpnet/networks/latest - -# Stop the network -$ ./build/tmpnetctl stop-network --network-dir=/path/to/network -``` - -Note the export of the path ending in `latest`. This is a symlink that -is set to the last network created by `tmpnetctl start-network`. Setting -the `TMPNET_NETWORK_DIR` env var to this symlink ensures that -`tmpnetctl` commands and e2e execution with -`--use-existing-network` will target the most recently deployed local -network. - -### Via code - -A local network can be managed in code: - -```golang -network, _ := local.StartNetwork( - ctx, // Context used to limit duration of waiting for network health - ginkgo.GinkgoWriter, // Writer to report progress of network start - "", // Use default root dir (~/.tmpnet) - &local.LocalNetwork{ - LocalConfig: local.LocalConfig{ - ExecPath: "/path/to/avalanchego", // Defining the avalanchego exec path is required - }, - }, - 5, // Number of initial validating nodes - 50, // Number of pre-funded keys to create -) - -uris := network.GetURIs() - -// Use URIs to interact with the network - -// Stop all nodes in the network -network.Stop() -``` - -If non-default node behavior is required, the `LocalNetwork` instance -supplied to `StartNetwork()` can be initialized with explicit node -configuration and by supplying a nodeCount argument of `0`: - -```golang -network, _ := local.StartNetwork( - ctx, - ginkgo.GinkgoWriter, - "", - &local.LocalNetwork{ - LocalConfig: local.LocalConfig{ - ExecPath: "/path/to/avalanchego", - }, - Nodes: []*LocalNode{ - { // node1 configuration is customized - Flags: FlagsMap{ // Any and all node flags can be configured here - config.DataDirKey: "/custom/path/to/node/data", - } - }, - }, - {}, // node2 uses default configuration - {}, // node3 uses default configuration - {}, // node4 uses default configuration - {}, // node5 uses default configuration - }, - 0, // Node count must be zero when setting node config - 50, -) -``` - -Further examples of code-based usage are located in the [e2e -tests](../../../e2e/e2e_test.go). - -## Networking configuration - -By default, nodes in a local network will be started with staking and -API ports set to `0` to ensure that ports will be dynamically -chosen. The tmpnet fixture discovers the ports used by a given node -by reading the `[base-data-dir]/process.json` file written by -avalanchego on node start. The use of dynamic ports supports testing -with many local networks without having to manually select compatible -port ranges. - -## Configuration on disk - -A local network relies on configuration written to disk in the following structure: - -``` -HOME -└── .tmpnet // Root path for the temporary network fixture - └── networks // Default parent directory for local networks - └── 1000 // The networkID is used to name the network dir and starts at 1000 - ├── NodeID-37E8UK3x2YFsHE3RdALmfWcppcZ1eTuj9 // The ID of a node is the name of its data dir - │ ├── chainData - │ │ └── ... - │ ├── config.json // Node flags - │ ├── db - │ │ └── ... - │ ├── logs - │ │ └── ... - │ ├── plugins - │ │ └── ... - │ └── process.json // Node process details (PID, API URI, staking address) - ├── chains - │ └── C - │ └── config.json // C-Chain config for all nodes - ├── defaults.json // Default flags and configuration for network - ├── genesis.json // Genesis for all nodes - ├── network.env // Sets network dir env to simplify use of network - └── ephemeral // Parent directory for ephemeral nodes (e.g. created by tests) - └─ NodeID-FdxnAvr4jK9XXAwsYZPgWAHW2QnwSZ // Data dir for an ephemeral node - └── ... - -``` - -### Default flags and configuration - -The default avalanchego node flags (e.g. `--staking-port=`) and -default configuration like the avalanchego path are stored at -`[network-dir]/defaults.json`. The value for a given defaulted flag -will be set on initial and subsequently added nodes that do not supply -values for a given defaulted flag. - -### Genesis - -The genesis file is stored at `[network-dir]/genesis.json` and -referenced by default by all nodes in the network. The genesis file -content will be generated with reasonable defaults if not -supplied. Each node in the network can override the default by setting -an explicit value for `--genesis-file` or `--genesis-file-content`. - -### C-Chain config - -The C-Chain config for a local network is stored at -`[network-dir]/chains/C/config.json` and referenced by default by all -nodes in the network. The C-Chain config will be generated with -reasonable defaults if not supplied. Each node in the network can -override the default by setting an explicit value for -`--chain-config-dir` and ensuring the C-Chain config file exists at -`[chain-config-dir]/C/config.json`. - -TODO(marun) Enable configuration of X-Chain and P-Chain. - -### Network env - -A shell script that sets the `TMPNET_NETWORK_DIR` env var to the -path of the network is stored at `[network-dir]/network.env`. Sourcing -this file (i.e. `source network.env`) in a shell will configure ginkgo -e2e and the `tmpnetctl` cli to target the network path specified in -the env var. - -### Node configuration - -The data dir for a node is set by default to -`[network-path]/[node-id]`. A node can be configured to use a -non-default path by explicitly setting the `--data-dir` -flag. - -#### Flags - -All flags used to configure a node are written to -`[network-path]/[node-id]/config.json` so that a node can be -configured with only a single argument: -`--config-file=/path/to/config.json`. This simplifies node launch and -ensures all parameters used to launch a node can be modified by -editing the config file. - -#### Process details - -The process details of a node are written by avalanchego to -`[base-data-dir]/process.json`. The file contains the PID of the node -process, the URI of the node's API, and the address other nodes can -use to bootstrap themselves (aka staking address). diff --git a/tests/fixture/tmpnet/local/network.go b/tests/fixture/tmpnet/network.go similarity index 63% rename from tests/fixture/tmpnet/local/network.go rename to tests/fixture/tmpnet/network.go index bf7d0056b225..e67eac7c7191 100644 --- a/tests/fixture/tmpnet/local/network.go +++ b/tests/fixture/tmpnet/network.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package local +package tmpnet import ( "context" @@ -18,7 +18,6 @@ import ( "github.com/ava-labs/avalanchego/config" "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" - "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/utils/formatting/address" @@ -37,10 +36,10 @@ const ( ) var ( - errInvalidNodeCount = errors.New("failed to populate local network config: non-zero node count is only valid for a network without nodes") - errInvalidKeyCount = errors.New("failed to populate local network config: non-zero key count is only valid for a network without keys") - errLocalNetworkDirNotSet = errors.New("local network directory not set - has Create() been called?") - errInvalidNetworkDir = errors.New("failed to write local network: invalid network directory") + errInvalidNodeCount = errors.New("failed to populate network config: non-zero node count is only valid for a network without nodes") + errInvalidKeyCount = errors.New("failed to populate network config: non-zero key count is only valid for a network without keys") + errNetworkDirNotSet = errors.New("network directory not set - has Create() been called?") + errInvalidNetworkDir = errors.New("failed to write network: invalid network directory") errMissingBootstrapNodes = errors.New("failed to add node due to missing bootstrap nodes") ) @@ -83,39 +82,25 @@ func FindNextNetworkID(rootDir string) (uint32, string, error) { } } -// Defines the configuration required for a local network (i.e. one composed of local processes). -type LocalNetwork struct { - tmpnet.NetworkConfig - LocalConfig +// Defines the configuration required for a tempoary network +type Network struct { + NetworkConfig + NodeRuntimeConfig - // Nodes with local configuration - Nodes []*LocalNode + // Nodes comprising the network + Nodes []*Node // Path where network configuration will be stored Dir string } -// Returns the configuration of the network in backend-agnostic form. -func (ln *LocalNetwork) GetConfig() tmpnet.NetworkConfig { - return ln.NetworkConfig -} - -// Returns the nodes of the network in backend-agnostic form. -func (ln *LocalNetwork) GetNodes() []tmpnet.Node { - nodes := make([]tmpnet.Node, 0, len(ln.Nodes)) - for _, node := range ln.Nodes { - nodes = append(nodes, node) - } - return nodes -} - // Adds a backend-agnostic ephemeral node to the network -func (ln *LocalNetwork) AddEphemeralNode(w io.Writer, flags tmpnet.FlagsMap) (tmpnet.Node, error) { +func (n *Network) AddEphemeralNode(w io.Writer, flags FlagsMap) (*Node, error) { if flags == nil { - flags = tmpnet.FlagsMap{} + flags = FlagsMap{} } - return ln.AddLocalNode(w, &LocalNode{ - NodeConfig: tmpnet.NodeConfig{ + return n.AddNode(w, &Node{ + NodeConfig: NodeConfig{ Flags: flags, }, }, true /* isEphemeral */) @@ -127,11 +112,11 @@ func StartNetwork( ctx context.Context, w io.Writer, rootDir string, - network *LocalNetwork, + network *Network, nodeCount int, keyCount int, -) (*LocalNetwork, error) { - if _, err := fmt.Fprintf(w, "Preparing configuration for new local network with %s\n", network.ExecPath); err != nil { +) (*Network, error) { + if _, err := fmt.Fprintf(w, "Preparing configuration for new temporary network with %s\n", network.ExecPath); err != nil { return nil, err } @@ -178,7 +163,7 @@ func StartNetwork( // nodes know where to write their configuration. network.Dir = networkDir - if err := network.PopulateLocalNetworkConfig(networkID, nodeCount, keyCount); err != nil { + if err := network.PopulateNetworkConfig(networkID, nodeCount, keyCount); err != nil { return nil, err } @@ -204,10 +189,10 @@ func StartNetwork( } // Read a network from the provided directory. -func ReadNetwork(dir string) (*LocalNetwork, error) { - network := &LocalNetwork{Dir: dir} +func ReadNetwork(dir string) (*Network, error) { + network := &Network{Dir: dir} if err := network.ReadAll(); err != nil { - return nil, fmt.Errorf("failed to read local network: %w", err) + return nil, fmt.Errorf("failed to read network: %w", err) } return network, nil } @@ -222,34 +207,34 @@ func StopNetwork(dir string) error { } // Ensure the network has the configuration it needs to start. -func (ln *LocalNetwork) PopulateLocalNetworkConfig(networkID uint32, nodeCount int, keyCount int) error { - if len(ln.Nodes) > 0 && nodeCount > 0 { +func (n *Network) PopulateNetworkConfig(networkID uint32, nodeCount int, keyCount int) error { + if len(n.Nodes) > 0 && nodeCount > 0 { return errInvalidNodeCount } - if len(ln.FundedKeys) > 0 && keyCount > 0 { + if len(n.FundedKeys) > 0 && keyCount > 0 { return errInvalidKeyCount } if nodeCount > 0 { // Add the specified number of nodes - nodes := make([]*LocalNode, 0, nodeCount) + nodes := make([]*Node, 0, nodeCount) for i := 0; i < nodeCount; i++ { - nodes = append(nodes, NewLocalNode("")) + nodes = append(nodes, NewNode("")) } - ln.Nodes = nodes + n.Nodes = nodes } // Ensure each node has keys and an associated node ID. This // ensures the availability of node IDs and proofs of possession // for genesis generation. - for _, node := range ln.Nodes { + for _, node := range n.Nodes { if err := node.EnsureKeys(); err != nil { return err } } // Assume all the initial nodes are stakers - initialStakers, err := stakersForNodes(networkID, ln.Nodes) + initialStakers, err := stakersForNodes(networkID, n.Nodes) if err != nil { return err } @@ -264,27 +249,27 @@ func (ln *LocalNetwork) PopulateLocalNetworkConfig(networkID uint32, nodeCount i } keys = append(keys, key) } - ln.FundedKeys = keys + n.FundedKeys = keys } - if err := ln.EnsureGenesis(networkID, initialStakers); err != nil { + if err := n.EnsureGenesis(networkID, initialStakers); err != nil { return err } - if ln.CChainConfig == nil { - ln.CChainConfig = LocalCChainConfig() + if n.CChainConfig == nil { + n.CChainConfig = DefaultCChainConfig() } // Default flags need to be set in advance of node config // population to ensure correct node configuration. - if ln.DefaultFlags == nil { - ln.DefaultFlags = LocalFlags() + if n.DefaultFlags == nil { + n.DefaultFlags = DefaultFlags() } - for _, node := range ln.Nodes { + for _, node := range n.Nodes { // Ensure the node is configured for use with the network and // knows where to write its configuration. - if err := ln.PopulateNodeConfig(node, ln.Dir); err != nil { + if err := n.PopulateNodeConfig(node, n.Dir); err != nil { return err } } @@ -295,18 +280,18 @@ func (ln *LocalNetwork) PopulateLocalNetworkConfig(networkID uint32, nodeCount i // Ensure 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]. Requires that the // network has valid genesis data. -func (ln *LocalNetwork) PopulateNodeConfig(node *LocalNode, nodeParentDir string) error { +func (n *Network) PopulateNodeConfig(node *Node, nodeParentDir string) error { flags := node.Flags // Set values common to all nodes - flags.SetDefaults(ln.DefaultFlags) - flags.SetDefaults(tmpnet.FlagsMap{ - config.GenesisFileKey: ln.GetGenesisPath(), - config.ChainConfigDirKey: ln.GetChainConfigDir(), + flags.SetDefaults(n.DefaultFlags) + flags.SetDefaults(FlagsMap{ + config.GenesisFileKey: n.GetGenesisPath(), + config.ChainConfigDirKey: n.GetChainConfigDir(), }) // Convert the network id to a string to ensure consistency in JSON round-tripping. - flags[config.NetworkNameKey] = strconv.FormatUint(uint64(ln.Genesis.NetworkID), 10) + flags[config.NetworkNameKey] = strconv.FormatUint(uint64(n.Genesis.NetworkID), 10) // Ensure keys are added if necessary if err := node.EnsureKeys(); err != nil { @@ -317,7 +302,7 @@ func (ln *LocalNetwork) PopulateNodeConfig(node *LocalNode, nodeParentDir string dataDir := node.GetDataDir() if len(dataDir) == 0 { // NodeID will have been set by EnsureKeys - dataDir = filepath.Join(nodeParentDir, node.NodeID.String()) + dataDir = filepath.Join(nodeParentDir, node.ID.String()) flags[config.DataDirKey] = dataDir } @@ -325,13 +310,13 @@ func (ln *LocalNetwork) PopulateNodeConfig(node *LocalNode, nodeParentDir string } // Starts a network for the first time -func (ln *LocalNetwork) Start(w io.Writer) error { - if len(ln.Dir) == 0 { - return errLocalNetworkDirNotSet +func (n *Network) Start(w io.Writer) error { + if len(n.Dir) == 0 { + return errNetworkDirNotSet } // Ensure configuration on disk is current - if err := ln.WriteAll(); err != nil { + if err := n.WriteAll(); err != nil { return err } @@ -344,11 +329,11 @@ func (ln *LocalNetwork) Start(w io.Writer) error { // 3rd node: 1st and 2nd nodes // ... // - bootstrapIDs := make([]string, 0, len(ln.Nodes)) - bootstrapIPs := make([]string, 0, len(ln.Nodes)) + bootstrapIDs := make([]string, 0, len(n.Nodes)) + bootstrapIPs := make([]string, 0, len(n.Nodes)) // Configure networking and start each node - for _, node := range ln.Nodes { + for _, node := range n.Nodes { // Update network configuration node.SetNetworkingConfigDefaults(0, 0, bootstrapIDs, bootstrapIPs) @@ -362,12 +347,12 @@ func (ln *LocalNetwork) Start(w io.Writer) error { // its staking port. The network will start faster with this // synchronization due to the avoidance of exponential backoff // if a node tries to connect to a beacon that is not ready. - if err := node.Start(w, ln.ExecPath); err != nil { + if err := node.Start(w, n.ExecPath); err != nil { return err } // Collect bootstrap nodes for subsequently started nodes to use - bootstrapIDs = append(bootstrapIDs, node.NodeID.String()) + bootstrapIDs = append(bootstrapIDs, node.ID.String()) bootstrapIPs = append(bootstrapIPs, node.StakingAddress) } @@ -375,27 +360,27 @@ func (ln *LocalNetwork) Start(w io.Writer) error { } // Wait until all nodes in the network are healthy. -func (ln *LocalNetwork) WaitForHealthy(ctx context.Context, w io.Writer) error { +func (n *Network) WaitForHealthy(ctx context.Context, w io.Writer) error { ticker := time.NewTicker(networkHealthCheckInterval) defer ticker.Stop() - healthyNodes := set.NewSet[ids.NodeID](len(ln.Nodes)) - for healthyNodes.Len() < len(ln.Nodes) { - for _, node := range ln.Nodes { - if healthyNodes.Contains(node.NodeID) { + healthyNodes := set.NewSet[ids.NodeID](len(n.Nodes)) + for healthyNodes.Len() < len(n.Nodes) { + for _, node := range n.Nodes { + if healthyNodes.Contains(node.ID) { continue } healthy, err := node.IsHealthy(ctx) - if err != nil && !errors.Is(err, tmpnet.ErrNotRunning) { + if err != nil && !errors.Is(err, ErrNotRunning) { return err } if !healthy { continue } - healthyNodes.Add(node.NodeID) - if _, err := fmt.Fprintf(w, "%s is healthy @ %s\n", node.NodeID, node.URI); err != nil { + healthyNodes.Add(node.ID) + if _, err := fmt.Fprintf(w, "%s is healthy @ %s\n", node.ID, node.URI); err != nil { return err } } @@ -411,16 +396,16 @@ func (ln *LocalNetwork) WaitForHealthy(ctx context.Context, w io.Writer) error { // Retrieve API URIs for all running primary validator nodes. URIs for // ephemeral nodes are not returned. -func (ln *LocalNetwork) GetURIs() []tmpnet.NodeURI { - uris := make([]tmpnet.NodeURI, 0, len(ln.Nodes)) - for _, node := range ln.Nodes { +func (n *Network) GetURIs() []NodeURI { + uris := make([]NodeURI, 0, len(n.Nodes)) + for _, node := range n.Nodes { // Only append URIs that are not empty. A node may have an // empty URI if it was not running at the time // node.ReadProcessContext() was called. if len(node.URI) > 0 { - uris = append(uris, tmpnet.NodeURI{ - NodeID: node.NodeID, - URI: node.URI, + uris = append(uris, NodeURI{ + ID: node.ID, + URI: node.URI, }) } } @@ -428,12 +413,12 @@ func (ln *LocalNetwork) GetURIs() []tmpnet.NodeURI { } // Stop all nodes in the network. -func (ln *LocalNetwork) Stop() error { +func (n *Network) Stop() error { var errs []error // Assume the nodes are loaded and the pids are current - for _, node := range ln.Nodes { + for _, node := range n.Nodes { if err := node.Stop(); err != nil { - errs = append(errs, fmt.Errorf("failed to stop node %s: %w", node.NodeID, err)) + errs = append(errs, fmt.Errorf("failed to stop node %s: %w", node.ID, err)) } } if len(errs) > 0 { @@ -442,12 +427,12 @@ func (ln *LocalNetwork) Stop() error { return nil } -func (ln *LocalNetwork) GetGenesisPath() string { - return filepath.Join(ln.Dir, "genesis.json") +func (n *Network) GetGenesisPath() string { + return filepath.Join(n.Dir, "genesis.json") } -func (ln *LocalNetwork) ReadGenesis() error { - bytes, err := os.ReadFile(ln.GetGenesisPath()) +func (n *Network) ReadGenesis() error { + bytes, err := os.ReadFile(n.GetGenesisPath()) if err != nil { return fmt.Errorf("failed to read genesis: %w", err) } @@ -455,107 +440,107 @@ func (ln *LocalNetwork) ReadGenesis() error { if err := json.Unmarshal(bytes, &genesis); err != nil { return fmt.Errorf("failed to unmarshal genesis: %w", err) } - ln.Genesis = &genesis + n.Genesis = &genesis return nil } -func (ln *LocalNetwork) WriteGenesis() error { - bytes, err := tmpnet.DefaultJSONMarshal(ln.Genesis) +func (n *Network) WriteGenesis() error { + bytes, err := DefaultJSONMarshal(n.Genesis) if err != nil { return fmt.Errorf("failed to marshal genesis: %w", err) } - if err := os.WriteFile(ln.GetGenesisPath(), bytes, perms.ReadWrite); err != nil { + if err := os.WriteFile(n.GetGenesisPath(), bytes, perms.ReadWrite); err != nil { return fmt.Errorf("failed to write genesis: %w", err) } return nil } -func (ln *LocalNetwork) GetChainConfigDir() string { - return filepath.Join(ln.Dir, "chains") +func (n *Network) GetChainConfigDir() string { + return filepath.Join(n.Dir, "chains") } -func (ln *LocalNetwork) GetCChainConfigPath() string { - return filepath.Join(ln.GetChainConfigDir(), "C", "config.json") +func (n *Network) GetCChainConfigPath() string { + return filepath.Join(n.GetChainConfigDir(), "C", "config.json") } -func (ln *LocalNetwork) ReadCChainConfig() error { - chainConfig, err := tmpnet.ReadFlagsMap(ln.GetCChainConfigPath(), "C-Chain config") +func (n *Network) ReadCChainConfig() error { + chainConfig, err := ReadFlagsMap(n.GetCChainConfigPath(), "C-Chain config") if err != nil { return err } - ln.CChainConfig = *chainConfig + n.CChainConfig = *chainConfig return nil } -func (ln *LocalNetwork) WriteCChainConfig() error { - path := ln.GetCChainConfigPath() +func (n *Network) WriteCChainConfig() error { + path := n.GetCChainConfigPath() dir := filepath.Dir(path) if err := os.MkdirAll(dir, perms.ReadWriteExecute); err != nil { return fmt.Errorf("failed to create C-Chain config dir: %w", err) } - return ln.CChainConfig.Write(path, "C-Chain config") + return n.CChainConfig.Write(path, "C-Chain config") } -// Used to marshal/unmarshal persistent local network defaults. -type localDefaults struct { - Flags tmpnet.FlagsMap +// Used to marshal/unmarshal persistent network defaults. +type networkDefaults struct { + Flags FlagsMap ExecPath string FundedKeys []*secp256k1.PrivateKey } -func (ln *LocalNetwork) GetDefaultsPath() string { - return filepath.Join(ln.Dir, "defaults.json") +func (n *Network) GetDefaultsPath() string { + return filepath.Join(n.Dir, "defaults.json") } -func (ln *LocalNetwork) ReadDefaults() error { - bytes, err := os.ReadFile(ln.GetDefaultsPath()) +func (n *Network) ReadDefaults() error { + bytes, err := os.ReadFile(n.GetDefaultsPath()) if err != nil { return fmt.Errorf("failed to read defaults: %w", err) } - defaults := localDefaults{} + defaults := networkDefaults{} if err := json.Unmarshal(bytes, &defaults); err != nil { return fmt.Errorf("failed to unmarshal defaults: %w", err) } - ln.DefaultFlags = defaults.Flags - ln.ExecPath = defaults.ExecPath - ln.FundedKeys = defaults.FundedKeys + n.DefaultFlags = defaults.Flags + n.ExecPath = defaults.ExecPath + n.FundedKeys = defaults.FundedKeys return nil } -func (ln *LocalNetwork) WriteDefaults() error { - defaults := localDefaults{ - Flags: ln.DefaultFlags, - ExecPath: ln.ExecPath, - FundedKeys: ln.FundedKeys, +func (n *Network) WriteDefaults() error { + defaults := networkDefaults{ + Flags: n.DefaultFlags, + ExecPath: n.ExecPath, + FundedKeys: n.FundedKeys, } - bytes, err := tmpnet.DefaultJSONMarshal(defaults) + bytes, err := DefaultJSONMarshal(defaults) if err != nil { return fmt.Errorf("failed to marshal defaults: %w", err) } - if err := os.WriteFile(ln.GetDefaultsPath(), bytes, perms.ReadWrite); err != nil { + if err := os.WriteFile(n.GetDefaultsPath(), bytes, perms.ReadWrite); err != nil { return fmt.Errorf("failed to write defaults: %w", err) } return nil } -func (ln *LocalNetwork) EnvFilePath() string { - return filepath.Join(ln.Dir, "network.env") +func (n *Network) EnvFilePath() string { + return filepath.Join(n.Dir, "network.env") } -func (ln *LocalNetwork) EnvFileContents() string { - return fmt.Sprintf("export %s=%s", NetworkDirEnvName, ln.Dir) +func (n *Network) EnvFileContents() string { + return fmt.Sprintf("export %s=%s", NetworkDirEnvName, n.Dir) } // Write an env file that sets the network dir env when sourced. -func (ln *LocalNetwork) WriteEnvFile() error { - if err := os.WriteFile(ln.EnvFilePath(), []byte(ln.EnvFileContents()), perms.ReadWrite); err != nil { - return fmt.Errorf("failed to write local network env file: %w", err) +func (n *Network) WriteEnvFile() error { + if err := os.WriteFile(n.EnvFilePath(), []byte(n.EnvFileContents()), perms.ReadWrite); err != nil { + return fmt.Errorf("failed to write network env file: %w", err) } return nil } -func (ln *LocalNetwork) WriteNodes() error { - for _, node := range ln.Nodes { +func (n *Network) WriteNodes() error { + for _, node := range n.Nodes { if err := node.WriteConfig(); err != nil { return err } @@ -564,42 +549,42 @@ func (ln *LocalNetwork) WriteNodes() error { } // Write network configuration to disk. -func (ln *LocalNetwork) WriteAll() error { - if len(ln.Dir) == 0 { +func (n *Network) WriteAll() error { + if len(n.Dir) == 0 { return errInvalidNetworkDir } - if err := ln.WriteGenesis(); err != nil { + if err := n.WriteGenesis(); err != nil { return err } - if err := ln.WriteCChainConfig(); err != nil { + if err := n.WriteCChainConfig(); err != nil { return err } - if err := ln.WriteDefaults(); err != nil { + if err := n.WriteDefaults(); err != nil { return err } - if err := ln.WriteEnvFile(); err != nil { + if err := n.WriteEnvFile(); err != nil { return err } - return ln.WriteNodes() + return n.WriteNodes() } // Read network configuration from disk. -func (ln *LocalNetwork) ReadConfig() error { - if err := ln.ReadGenesis(); err != nil { +func (n *Network) ReadConfig() error { + if err := n.ReadGenesis(); err != nil { return err } - if err := ln.ReadCChainConfig(); err != nil { + if err := n.ReadCChainConfig(); err != nil { return err } - return ln.ReadDefaults() + return n.ReadDefaults() } // Read node configuration and process context from disk. -func (ln *LocalNetwork) ReadNodes() error { - nodes := []*LocalNode{} +func (n *Network) ReadNodes() error { + nodes := []*Node{} // Node configuration / process context is stored in child directories - entries, err := os.ReadDir(ln.Dir) + entries, err := os.ReadDir(n.Dir) if err != nil { return fmt.Errorf("failed to read network path: %w", err) } @@ -608,10 +593,10 @@ func (ln *LocalNetwork) ReadNodes() error { continue } - nodeDir := filepath.Join(ln.Dir, entry.Name()) + nodeDir := filepath.Join(n.Dir, entry.Name()) node, err := ReadNode(nodeDir) if errors.Is(err, os.ErrNotExist) { - // If no config file exists, assume this is not the path of a local node + // If no config file exists, assume this is not the path of a node continue } else if err != nil { return err @@ -620,30 +605,30 @@ func (ln *LocalNetwork) ReadNodes() error { nodes = append(nodes, node) } - ln.Nodes = nodes + n.Nodes = nodes return nil } // Read network and node configuration from disk. -func (ln *LocalNetwork) ReadAll() error { - if err := ln.ReadConfig(); err != nil { +func (n *Network) ReadAll() error { + if err := n.ReadConfig(); err != nil { return err } - return ln.ReadNodes() + return n.ReadNodes() } -func (ln *LocalNetwork) AddLocalNode(w io.Writer, node *LocalNode, isEphemeral bool) (*LocalNode, error) { +func (n *Network) AddNode(w io.Writer, node *Node, isEphemeral bool) (*Node, error) { // Assume network configuration has been written to disk and is current in memory if node == nil { // Set an empty data dir so that PopulateNodeConfig will know // to set the default of `[network dir]/[node id]`. - node = NewLocalNode("") + node = NewNode("") } // Default to a data dir of [network-dir]/[node-ID] - nodeParentDir := ln.Dir + nodeParentDir := n.Dir if isEphemeral { // For an ephemeral node, default to a data dir of [network-dir]/[ephemeral-dir]/[node-ID] // to provide a clear separation between nodes that are expected to expose stable API @@ -653,14 +638,14 @@ func (ln *LocalNetwork) AddLocalNode(w io.Writer, node *LocalNode, isEphemeral b // The data for an ephemeral node is still stored in the file tree rooted at the network // dir to ensure that recursively archiving the network dir in CI will collect all node // data used for a test run. - nodeParentDir = filepath.Join(ln.Dir, defaultEphemeralDirName) + nodeParentDir = filepath.Join(n.Dir, defaultEphemeralDirName) } - if err := ln.PopulateNodeConfig(node, nodeParentDir); err != nil { + if err := n.PopulateNodeConfig(node, nodeParentDir); err != nil { return nil, err } - bootstrapIPs, bootstrapIDs, err := ln.GetBootstrapIPsAndIDs() + bootstrapIPs, bootstrapIDs, err := n.GetBootstrapIPsAndIDs() if err != nil { return nil, err } @@ -676,7 +661,7 @@ func (ln *LocalNetwork) AddLocalNode(w io.Writer, node *LocalNode, isEphemeral b return nil, err } - err = node.Start(w, ln.ExecPath) + err = node.Start(w, n.ExecPath) if err != nil { // Attempt to stop an unhealthy node to provide some assurance to the caller // that an error condition will not result in a lingering process. @@ -690,23 +675,23 @@ func (ln *LocalNetwork) AddLocalNode(w io.Writer, node *LocalNode, isEphemeral b return node, nil } -func (ln *LocalNetwork) GetBootstrapIPsAndIDs() ([]string, []string, error) { +func (n *Network) GetBootstrapIPsAndIDs() ([]string, []string, error) { // Collect staking addresses of running nodes for use in bootstrapping a node - if err := ln.ReadNodes(); err != nil { - return nil, nil, fmt.Errorf("failed to read local network nodes: %w", err) + if err := n.ReadNodes(); err != nil { + return nil, nil, fmt.Errorf("failed to read network nodes: %w", err) } var ( - bootstrapIPs = make([]string, 0, len(ln.Nodes)) - bootstrapIDs = make([]string, 0, len(ln.Nodes)) + bootstrapIPs = make([]string, 0, len(n.Nodes)) + bootstrapIDs = make([]string, 0, len(n.Nodes)) ) - for _, node := range ln.Nodes { + for _, node := range n.Nodes { if len(node.StakingAddress) == 0 { // Node is not running continue } bootstrapIPs = append(bootstrapIPs, node.StakingAddress) - bootstrapIDs = append(bootstrapIDs, node.NodeID.String()) + bootstrapIDs = append(bootstrapIDs, node.ID.String()) } if len(bootstrapIDs) == 0 { @@ -717,7 +702,7 @@ func (ln *LocalNetwork) GetBootstrapIPsAndIDs() ([]string, []string, error) { } // Returns staker configuration for the given set of nodes. -func stakersForNodes(networkID uint32, nodes []*LocalNode) ([]genesis.UnparsedStaker, error) { +func stakersForNodes(networkID uint32, nodes []*Node) ([]genesis.UnparsedStaker, error) { // Give staking rewards for initial validators to a random address. Any testing of staking rewards // will be easier to perform with nodes other than the initial validators since the timing of // staking can be more easily controlled. @@ -734,7 +719,7 @@ func stakersForNodes(networkID uint32, nodes []*LocalNode) ([]genesis.UnparsedSt return nil, fmt.Errorf("failed to derive proof of possession: %w", err) } initialStakers[i] = genesis.UnparsedStaker{ - NodeID: node.NodeID, + NodeID: node.ID, RewardAddress: rewardAddr, DelegationFee: .01 * reward.PercentDenominator, Signer: pop, diff --git a/tests/fixture/tmpnet/local/network_test.go b/tests/fixture/tmpnet/network_test.go similarity index 83% rename from tests/fixture/tmpnet/local/network_test.go rename to tests/fixture/tmpnet/network_test.go index 8f7e66d37d5b..ec4569fd3693 100644 --- a/tests/fixture/tmpnet/local/network_test.go +++ b/tests/fixture/tmpnet/network_test.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package local +package tmpnet import ( "testing" @@ -14,8 +14,8 @@ func TestNetworkSerialization(t *testing.T) { tmpDir := t.TempDir() - network := &LocalNetwork{Dir: tmpDir} - require.NoError(network.PopulateLocalNetworkConfig(1337, 1, 1)) + network := &Network{Dir: tmpDir} + require.NoError(network.PopulateNetworkConfig(1337, 1, 1)) require.NoError(network.WriteAll()) loadedNetwork, err := ReadNetwork(tmpDir) diff --git a/tests/fixture/tmpnet/local/node.go b/tests/fixture/tmpnet/node.go similarity index 69% rename from tests/fixture/tmpnet/local/node.go rename to tests/fixture/tmpnet/node.go index 908d3fd5f474..db1db1c85a72 100644 --- a/tests/fixture/tmpnet/local/node.go +++ b/tests/fixture/tmpnet/node.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package local +package tmpnet import ( "context" @@ -21,88 +21,69 @@ import ( "github.com/ava-labs/avalanchego/api/health" "github.com/ava-labs/avalanchego/config" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/node" - "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" "github.com/ava-labs/avalanchego/utils/perms" ) -var errNodeAlreadyRunning = errors.New("failed to start local node: node is already running") +var errNodeAlreadyRunning = errors.New("failed to start node : node is already running") -// Defines local-specific node configuration. Supports setting default -// and node-specific values. +// Defines configuration to execute a node. // // TODO(marun) Support persisting this configuration per-node when // node restart is implemented. Currently it can be supplied for node // start but won't survive restart. -type LocalConfig struct { +type NodeRuntimeConfig struct { // Path to avalanchego binary ExecPath string } -// Stores the configuration and process details of a node in a local network. -type LocalNode struct { - tmpnet.NodeConfig - LocalConfig +// Stores the configuration and process details of a node in a temporary network. +type Node struct { + NodeConfig + NodeRuntimeConfig node.NodeProcessContext // Configuration is intended to be stored at the path identified in NodeConfig.Flags[config.DataDirKey] } -func NewLocalNode(dataDir string) *LocalNode { - return &LocalNode{ - NodeConfig: tmpnet.NodeConfig{ - Flags: tmpnet.FlagsMap{ +func NewNode(dataDir string) *Node { + return &Node{ + NodeConfig: NodeConfig{ + Flags: FlagsMap{ config.DataDirKey: dataDir, }, }, } } -// Attempt to read configuration and process details for a local node +// Attempt to read configuration and process details for a node // from the specified directory. -func ReadNode(dataDir string) (*LocalNode, error) { - node := NewLocalNode(dataDir) +func ReadNode(dataDir string) (*Node, error) { + node := NewNode(dataDir) if _, err := os.Stat(node.GetConfigPath()); err != nil { - return nil, fmt.Errorf("failed to read local node config file: %w", err) + return nil, fmt.Errorf("failed to read node config file: %w", err) } return node, node.ReadAll() } -// Retrieve the ID of the node. The returned value may be nil if the -// node configuration has not yet been populated or read. -func (n *LocalNode) GetID() ids.NodeID { - return n.NodeConfig.NodeID -} - -// Retrieve backend-agnostic node configuration. -func (n *LocalNode) GetConfig() tmpnet.NodeConfig { - return n.NodeConfig -} - -// Retrieve backend-agnostic process details. -func (n *LocalNode) GetProcessContext() node.NodeProcessContext { - return n.NodeProcessContext -} - -func (n *LocalNode) GetDataDir() string { +func (n *Node) GetDataDir() string { return cast.ToString(n.Flags[config.DataDirKey]) } -func (n *LocalNode) GetConfigPath() string { +func (n *Node) GetConfigPath() string { return filepath.Join(n.GetDataDir(), "config.json") } -func (n *LocalNode) ReadConfig() error { +func (n *Node) ReadConfig() error { bytes, err := os.ReadFile(n.GetConfigPath()) if err != nil { - return fmt.Errorf("failed to read local node config: %w", err) + return fmt.Errorf("failed to read node config: %w", err) } - flags := tmpnet.FlagsMap{} + flags := FlagsMap{} if err := json.Unmarshal(bytes, &flags); err != nil { - return fmt.Errorf("failed to unmarshal local node config: %w", err) + return fmt.Errorf("failed to unmarshal node config: %w", err) } - config := tmpnet.NodeConfig{Flags: flags} + config := NodeConfig{Flags: flags} if err := config.EnsureNodeID(); err != nil { return err } @@ -110,27 +91,27 @@ func (n *LocalNode) ReadConfig() error { return nil } -func (n *LocalNode) WriteConfig() error { +func (n *Node) WriteConfig() error { if err := os.MkdirAll(n.GetDataDir(), perms.ReadWriteExecute); err != nil { return fmt.Errorf("failed to create node dir: %w", err) } - bytes, err := tmpnet.DefaultJSONMarshal(n.Flags) + bytes, err := DefaultJSONMarshal(n.Flags) if err != nil { - return fmt.Errorf("failed to marshal local node config: %w", err) + return fmt.Errorf("failed to marshal node config: %w", err) } if err := os.WriteFile(n.GetConfigPath(), bytes, perms.ReadWrite); err != nil { - return fmt.Errorf("failed to write local node config: %w", err) + return fmt.Errorf("failed to write node config: %w", err) } return nil } -func (n *LocalNode) GetProcessContextPath() string { +func (n *Node) GetProcessContextPath() string { return filepath.Join(n.GetDataDir(), config.DefaultProcessContextFilename) } -func (n *LocalNode) ReadProcessContext() error { +func (n *Node) ReadProcessContext() error { path := n.GetProcessContextPath() if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { // The absence of the process context file indicates the node is not running @@ -140,28 +121,28 @@ func (n *LocalNode) ReadProcessContext() error { bytes, err := os.ReadFile(path) if err != nil { - return fmt.Errorf("failed to read local node process context: %w", err) + return fmt.Errorf("failed to read node process context: %w", err) } processContext := node.NodeProcessContext{} if err := json.Unmarshal(bytes, &processContext); err != nil { - return fmt.Errorf("failed to unmarshal local node process context: %w", err) + return fmt.Errorf("failed to unmarshal node process context: %w", err) } n.NodeProcessContext = processContext return nil } -func (n *LocalNode) ReadAll() error { +func (n *Node) ReadAll() error { if err := n.ReadConfig(); err != nil { return err } return n.ReadProcessContext() } -func (n *LocalNode) Start(w io.Writer, defaultExecPath string) error { +func (n *Node) Start(w io.Writer, defaultExecPath string) error { // Avoid attempting to start an already running node. proc, err := n.GetProcess() if err != nil { - return fmt.Errorf("failed to start local node: %w", err) + return fmt.Errorf("failed to start node: %w", err) } if proc != nil { return errNodeAlreadyRunning @@ -184,12 +165,12 @@ func (n *LocalNode) Start(w io.Writer, defaultExecPath string) error { } // Determine appropriate level of node description detail - nodeDescription := fmt.Sprintf("node %q", n.NodeID) + nodeDescription := fmt.Sprintf("node %q", n.ID) isEphemeralNode := filepath.Base(filepath.Dir(n.GetDataDir())) == defaultEphemeralDirName if isEphemeralNode { nodeDescription = "ephemeral " + nodeDescription } - nonDefaultNodeDir := filepath.Base(n.GetDataDir()) != n.NodeID.String() + nonDefaultNodeDir := filepath.Base(n.GetDataDir()) != n.ID.String() if nonDefaultNodeDir { // Only include the data dir if its base is not the default (the node ID) nodeDescription = fmt.Sprintf("%s with path: %s", nodeDescription, n.GetDataDir()) @@ -208,7 +189,7 @@ func (n *LocalNode) Start(w io.Writer, defaultExecPath string) error { // found in a reasonable amount of time, the node is unlikely to have // started successfully. if err := n.WaitForProcessContext(context.Background()); err != nil { - return fmt.Errorf("failed to start local node: %w", err) + return fmt.Errorf("failed to start node: %w", err) } _, err = fmt.Fprintf(w, "Started %s\n", nodeDescription) @@ -218,7 +199,7 @@ func (n *LocalNode) Start(w io.Writer, defaultExecPath string) error { // Retrieve the node process if it is running. As part of determining // process liveness, the node's process context will be refreshed if // live or cleared if not running. -func (n *LocalNode) GetProcess() (*os.Process, error) { +func (n *Node) GetProcess() (*os.Process, error) { // Read the process context to ensure freshness. The node may have // stopped or been restarted since last read. if err := n.ReadProcessContext(); err != nil { @@ -251,7 +232,7 @@ func (n *LocalNode) GetProcess() (*os.Process, error) { // Signals the node process to stop and waits for the node process to // stop running. -func (n *LocalNode) Stop() error { +func (n *Node) Stop() error { proc, err := n.GetProcess() if err != nil { return fmt.Errorf("failed to retrieve process to stop: %w", err) @@ -265,7 +246,7 @@ func (n *LocalNode) Stop() error { } // Wait for the node process to stop - ticker := time.NewTicker(tmpnet.DefaultNodeTickerInterval) + ticker := time.NewTicker(DefaultNodeTickerInterval) defer ticker.Stop() ctx, cancel := context.WithTimeout(context.Background(), DefaultNodeStopTimeout) defer cancel() @@ -280,13 +261,13 @@ func (n *LocalNode) Stop() error { select { case <-ctx.Done(): - return fmt.Errorf("failed to see node process stop %q before timeout: %w", n.NodeID, ctx.Err()) + return fmt.Errorf("failed to see node process stop %q before timeout: %w", n.ID, ctx.Err()) case <-ticker.C: } } } -func (n *LocalNode) IsHealthy(ctx context.Context) (bool, error) { +func (n *Node) IsHealthy(ctx context.Context) (bool, error) { // Check that the node process is running as a precondition for // checking health. GetProcess will also ensure that the node's // API URI is current. @@ -295,7 +276,7 @@ func (n *LocalNode) IsHealthy(ctx context.Context) (bool, error) { return false, fmt.Errorf("failed to determine process status: %w", err) } if proc == nil { - return false, tmpnet.ErrNotRunning + return false, ErrNotRunning } // Check that the node is reporting healthy @@ -320,8 +301,8 @@ func (n *LocalNode) IsHealthy(ctx context.Context) (bool, error) { return false, fmt.Errorf("failed to query node health: %w", err) } -func (n *LocalNode) WaitForProcessContext(ctx context.Context) error { - ticker := time.NewTicker(tmpnet.DefaultNodeTickerInterval) +func (n *Node) WaitForProcessContext(ctx context.Context) error { + ticker := time.NewTicker(DefaultNodeTickerInterval) defer ticker.Stop() ctx, cancel := context.WithTimeout(ctx, DefaultNodeInitTimeout) @@ -329,12 +310,12 @@ func (n *LocalNode) WaitForProcessContext(ctx context.Context) error { for len(n.URI) == 0 { err := n.ReadProcessContext() if err != nil { - return fmt.Errorf("failed to read process context for node %q: %w", n.NodeID, err) + return fmt.Errorf("failed to read process context for node %q: %w", n.ID, err) } select { case <-ctx.Done(): - return fmt.Errorf("failed to load process context for node %q before timeout: %w", n.NodeID, ctx.Err()) + return fmt.Errorf("failed to load process context for node %q before timeout: %w", n.ID, ctx.Err()) case <-ticker.C: } } diff --git a/tests/fixture/tmpnet/local/node_test.go b/tests/fixture/tmpnet/node_test.go similarity index 90% rename from tests/fixture/tmpnet/local/node_test.go rename to tests/fixture/tmpnet/node_test.go index 64cd77928a4d..ab060105804a 100644 --- a/tests/fixture/tmpnet/local/node_test.go +++ b/tests/fixture/tmpnet/node_test.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package local +package tmpnet import ( "testing" @@ -14,7 +14,7 @@ func TestNodeSerialization(t *testing.T) { tmpDir := t.TempDir() - node := NewLocalNode(tmpDir) + node := NewNode(tmpDir) require.NoError(node.EnsureKeys()) require.NoError(node.WriteConfig()) diff --git a/tests/fixture/tmpnet/common.go b/tests/fixture/tmpnet/utils.go similarity index 73% rename from tests/fixture/tmpnet/common.go rename to tests/fixture/tmpnet/utils.go index 4b0281f45242..e8c950857c8d 100644 --- a/tests/fixture/tmpnet/common.go +++ b/tests/fixture/tmpnet/utils.go @@ -5,6 +5,7 @@ package tmpnet import ( "context" + "encoding/json" "errors" "fmt" "time" @@ -17,9 +18,9 @@ const ( var ErrNotRunning = errors.New("not running") // WaitForHealthy blocks until Node.IsHealthy returns true or an error (including context timeout) is observed. -func WaitForHealthy(ctx context.Context, node Node) error { +func WaitForHealthy(ctx context.Context, node *Node) error { if _, ok := ctx.Deadline(); !ok { - return fmt.Errorf("unable to wait for health for node %q with a context without a deadline", node.GetID()) + return fmt.Errorf("unable to wait for health for node %q with a context without a deadline", node.ID) } ticker := time.NewTicker(DefaultNodeTickerInterval) defer ticker.Stop() @@ -27,7 +28,7 @@ func WaitForHealthy(ctx context.Context, node Node) error { for { healthy, err := node.IsHealthy(ctx) if err != nil && !errors.Is(err, ErrNotRunning) { - return fmt.Errorf("failed to wait for health of node %q: %w", node.GetID(), err) + return fmt.Errorf("failed to wait for health of node %q: %w", node.ID, err) } if healthy { return nil @@ -35,8 +36,13 @@ func WaitForHealthy(ctx context.Context, node Node) error { select { case <-ctx.Done(): - return fmt.Errorf("failed to wait for health of node %q before timeout: %w", node.GetID(), ctx.Err()) + return fmt.Errorf("failed to wait for health of node %q before timeout: %w", node.ID, ctx.Err()) case <-ticker.C: } } } + +// Marshal to json with default prefix and indent. +func DefaultJSONMarshal(v interface{}) ([]byte, error) { + return json.MarshalIndent(v, "", " ") +} diff --git a/tests/upgrade/upgrade_test.go b/tests/upgrade/upgrade_test.go index b8f8147c831a..764fb6afa68d 100644 --- a/tests/upgrade/upgrade_test.go +++ b/tests/upgrade/upgrade_test.go @@ -48,12 +48,11 @@ var _ = ginkgo.Describe("[Upgrade]", func() { require := require.New(ginkgo.GinkgoT()) ginkgo.It("can upgrade versions", func() { - // TODO(marun) How many nodes should the target network have to best validate upgrade? - network := e2e.StartLocalNetwork(avalancheGoExecPath, e2e.DefaultNetworkDir) + network := e2e.StartNetwork(avalancheGoExecPath, e2e.DefaultNetworkDir) ginkgo.By(fmt.Sprintf("restarting all nodes with %q binary", avalancheGoExecPathToUpgradeTo)) for _, node := range network.Nodes { - ginkgo.By(fmt.Sprintf("restarting node %q with %q binary", node.GetID(), avalancheGoExecPathToUpgradeTo)) + ginkgo.By(fmt.Sprintf("restarting node %q with %q binary", node.ID, avalancheGoExecPathToUpgradeTo)) require.NoError(node.Stop()) // A node must start with sufficient bootstrap nodes to represent a quorum. Since the node's current @@ -71,7 +70,7 @@ var _ = ginkgo.Describe("[Upgrade]", func() { require.NoError(node.Start(ginkgo.GinkgoWriter, avalancheGoExecPath)) - ginkgo.By(fmt.Sprintf("waiting for node %q to report healthy after restart", node.GetID())) + ginkgo.By(fmt.Sprintf("waiting for node %q to report healthy after restart", node.ID)) e2e.WaitForHealthy(node) }