From dd49904baa0cf705a66af7875d2f1719a8244515 Mon Sep 17 00:00:00 2001
From: Maru Newby <maru.newby@avalabs.org>
Date: Tue, 7 May 2024 18:06:31 -0700
Subject: [PATCH 1/2] [tmpnet] Enable single node networks

---
 tests/e2e/c/dynamic_fees.go          |  4 +--
 tests/e2e/e2e_test.go                |  6 +----
 tests/fixture/e2e/flags.go           | 11 ++++++++
 tests/fixture/e2e/helpers.go         |  1 -
 tests/fixture/tmpnet/README.md       |  5 ++--
 tests/fixture/tmpnet/cmd/main.go     |  2 +-
 tests/fixture/tmpnet/network.go      | 39 +++++++++++++++++-----------
 tests/fixture/tmpnet/network_test.go |  4 +--
 tests/fixture/tmpnet/node.go         |  6 ++---
 tests/upgrade/upgrade_test.go        |  4 +--
 10 files changed, 47 insertions(+), 35 deletions(-)

diff --git a/tests/e2e/c/dynamic_fees.go b/tests/e2e/c/dynamic_fees.go
index 9af074894afc..c3dda77b985c 100644
--- a/tests/e2e/c/dynamic_fees.go
+++ b/tests/e2e/c/dynamic_fees.go
@@ -37,9 +37,7 @@ var _ = e2e.DescribeCChain("[Dynamic Fees]", func() {
 
 	ginkgo.It("should ensure that the gas price is affected by load", func() {
 		ginkgo.By("creating a new private network to ensure isolation from other tests")
-		privateNetwork := &tmpnet.Network{
-			Owner: "avalanchego-e2e-dynamic-fees",
-		}
+		privateNetwork := tmpnet.NewDefaultNetwork("avalanchego-e2e-dynamic-fees")
 		e2e.Env.StartPrivateNetwork(privateNetwork)
 
 		ginkgo.By("allocating a pre-funded key")
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index 443a781281a3..08b0f2d2c03c 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -7,7 +7,6 @@ import (
 	"testing"
 
 	"github.com/onsi/gomega"
-	"github.com/stretchr/testify/require"
 
 	// ensure test packages are scanned by ginkgo
 	_ "github.com/ava-labs/avalanchego/tests/e2e/banff"
@@ -38,11 +37,8 @@ func init() {
 var _ = ginkgo.SynchronizedBeforeSuite(func() []byte {
 	// Run only once in the first ginkgo process
 
-	nodes, err := tmpnet.NewNodes(tmpnet.DefaultNodeCount)
-	require.NoError(ginkgo.GinkgoT(), err)
-
+	nodes := tmpnet.NewNodesOrDie(flagVars.NodeCount())
 	subnets := vms.XSVMSubnets(nodes...)
-
 	return e2e.NewTestEnvironment(
 		flagVars,
 		&tmpnet.Network{
diff --git a/tests/fixture/e2e/flags.go b/tests/fixture/e2e/flags.go
index 8af3cce6d787..bc752adaf7df 100644
--- a/tests/fixture/e2e/flags.go
+++ b/tests/fixture/e2e/flags.go
@@ -19,6 +19,7 @@ type FlagVars struct {
 	reuseNetwork         bool
 	networkShutdownDelay time.Duration
 	stopNetwork          bool
+	nodeCount            int
 }
 
 func (v *FlagVars) AvalancheGoExecPath() string {
@@ -51,6 +52,10 @@ func (v *FlagVars) StopNetwork() bool {
 	return v.stopNetwork
 }
 
+func (v *FlagVars) NodeCount() int {
+	return v.nodeCount
+}
+
 func RegisterFlags() *FlagVars {
 	vars := FlagVars{}
 	flag.StringVar(
@@ -89,6 +94,12 @@ func RegisterFlags() *FlagVars {
 		false,
 		"[optional] stop an existing network and exit without executing any tests.",
 	)
+	flag.IntVar(
+		&vars.nodeCount,
+		"node-count",
+		tmpnet.DefaultNodeCount,
+		"number of nodes the network should initially consist of",
+	)
 
 	return &vars
 }
diff --git a/tests/fixture/e2e/helpers.go b/tests/fixture/e2e/helpers.go
index 1395db1f8c29..358f07946055 100644
--- a/tests/fixture/e2e/helpers.go
+++ b/tests/fixture/e2e/helpers.go
@@ -233,7 +233,6 @@ func StartNetwork(
 			DefaultNetworkDir,
 			avalancheGoExecPath,
 			pluginDir,
-			tmpnet.DefaultNodeCount,
 		),
 	)
 
diff --git a/tests/fixture/tmpnet/README.md b/tests/fixture/tmpnet/README.md
index b1158773d628..a7652ce43d8c 100644
--- a/tests/fixture/tmpnet/README.md
+++ b/tests/fixture/tmpnet/README.md
@@ -49,7 +49,7 @@ A temporary network can be managed by the `tmpnetctl` cli tool:
 # Build the tmpnetctl binary
 $ ./scripts/build_tmpnetctl.sh
 
-# Start a new network
+# Start a new network. Possible to specify the number of nodes (> 1) with --node-count.
 $ ./build/tmpnetctl start-network --avalanchego-path=/path/to/avalanchego
 ...
 Started network /home/me/.tmpnet/networks/20240306-152305.924531 (UUID: abaab590-b375-44f6-9ca5-f8a6dc061725)
@@ -87,6 +87,7 @@ network := &tmpnet.Network{                   // Configure non-default values fo
     DefaultFlags: tmpnet.FlagsMap{
         config.LogLevelKey: "INFO",           // Change one of the network's defaults
     },
+    Nodes: tmpnet.NewNodesOrDie(5),           // Number of initial validating nodes
     Subnets: []*tmpnet.Subnet{                // Subnets to create on the new network once it is running
         {
             Name: "xsvm-a",                   // User-defined name used to reference subnet in code and on disk
@@ -97,6 +98,7 @@ network := &tmpnet.Network{                   // Configure non-default values fo
                     PreFundedKey: <key>,      // (Optional) A private key that is funded in the genesis bytes
                 },
             },
+            ValidatorIDs: <node ids>,         // The IDs of nodes that validate the subnet
         },
     },
 }
@@ -108,7 +110,6 @@ _ := tmpnet.StartNewNetwork(              // Start the network
     "",                                   // Empty string uses the default network path (~/tmpnet/networks)
     "/path/to/avalanchego",               // The path to the binary that nodes will execute
     "/path/to/plugins",                   // The path nodes will use for plugin binaries (suggested value ~/.avalanchego/plugins)
-    5,                                    // Number of initial validating nodes
 )
 
 uris := network.GetNodeURIs()
diff --git a/tests/fixture/tmpnet/cmd/main.go b/tests/fixture/tmpnet/cmd/main.go
index 7b415b32788b..039dd4c0b4da 100644
--- a/tests/fixture/tmpnet/cmd/main.go
+++ b/tests/fixture/tmpnet/cmd/main.go
@@ -66,6 +66,7 @@ func main() {
 
 			network := &tmpnet.Network{
 				Owner: networkOwner,
+				Nodes: tmpnet.NewNodesOrDie(int(nodeCount)),
 			}
 
 			// Extreme upper bound, should never take this long
@@ -80,7 +81,6 @@ func main() {
 				rootDir,
 				avalancheGoPath,
 				pluginDir,
-				int(nodeCount),
 			)
 			if err != nil {
 				return err
diff --git a/tests/fixture/tmpnet/network.go b/tests/fixture/tmpnet/network.go
index faea1dde50f7..5e7eea3be807 100644
--- a/tests/fixture/tmpnet/network.go
+++ b/tests/fixture/tmpnet/network.go
@@ -47,9 +47,13 @@ const (
 	HardHatKeyStr = "56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027"
 )
 
-// HardhatKey is a legacy used for hardhat testing in subnet-evm
-// TODO(marun) Remove when no longer needed.
-var HardhatKey *secp256k1.PrivateKey
+var (
+	// Key expected to be funded for subnet-evm hardhat testing
+	// TODO(marun) Remove when subnet-evm configures the genesis with this key.
+	HardhatKey *secp256k1.PrivateKey
+
+	errInsufficientNodes = errors.New("network needs at least one node to start")
+)
 
 func init() {
 	hardhatKeyBytes, err := hex.DecodeString(HardHatKeyStr)
@@ -105,6 +109,13 @@ type Network struct {
 	Subnets []*Subnet
 }
 
+func NewDefaultNetwork(owner string) *Network {
+	return &Network{
+		Owner: owner,
+		Nodes: NewNodesOrDie(DefaultNodeCount),
+	}
+}
+
 // Ensure a real and absolute network dir so that node
 // configuration that embeds the network path will continue to
 // work regardless of symlink and working directory changes.
@@ -123,9 +134,11 @@ func StartNewNetwork(
 	rootNetworkDir string,
 	avalancheGoExecPath string,
 	pluginDir string,
-	nodeCount int,
 ) error {
-	if err := network.EnsureDefaultConfig(w, avalancheGoExecPath, pluginDir, nodeCount); err != nil {
+	if len(network.Nodes) == 0 {
+		return errInsufficientNodes
+	}
+	if err := network.EnsureDefaultConfig(w, avalancheGoExecPath, pluginDir); err != nil {
 		return err
 	}
 	if err := network.Create(rootNetworkDir); err != nil {
@@ -171,7 +184,7 @@ func ReadNetwork(dir string) (*Network, error) {
 }
 
 // Initializes a new network with default configuration.
-func (n *Network) EnsureDefaultConfig(w io.Writer, avalancheGoPath string, pluginDir string, nodeCount int) error {
+func (n *Network) EnsureDefaultConfig(w io.Writer, avalancheGoPath string, pluginDir string) error {
 	if _, err := fmt.Fprintf(w, "Preparing configuration for new network with %s\n", avalancheGoPath); err != nil {
 		return err
 	}
@@ -187,6 +200,11 @@ func (n *Network) EnsureDefaultConfig(w io.Writer, avalancheGoPath string, plugi
 	}
 	n.DefaultFlags.SetDefaults(DefaultFlags())
 
+	if len(n.Nodes) == 1 {
+		// Sybil protection needs to be disabled for a single node network to start
+		n.DefaultFlags[config.SybilProtectionEnabledKey] = false
+	}
+
 	// Only configure the plugin dir with a non-empty value to ensure
 	// the use of the default value (`[datadir]/plugins`) when
 	// no plugin dir is configured.
@@ -222,15 +240,6 @@ func (n *Network) EnsureDefaultConfig(w io.Writer, avalancheGoPath string, plugi
 		n.DefaultRuntimeConfig.AvalancheGoPath = avalancheGoPath
 	}
 
-	// Ensure nodes are created
-	if len(n.Nodes) == 0 {
-		nodes, err := NewNodes(nodeCount)
-		if err != nil {
-			return err
-		}
-		n.Nodes = nodes
-	}
-
 	// Ensure nodes are configured
 	for i := range n.Nodes {
 		if err := n.EnsureNodeConfig(n.Nodes[i]); err != nil {
diff --git a/tests/fixture/tmpnet/network_test.go b/tests/fixture/tmpnet/network_test.go
index c04c497c2485..db8d1c404716 100644
--- a/tests/fixture/tmpnet/network_test.go
+++ b/tests/fixture/tmpnet/network_test.go
@@ -15,8 +15,8 @@ func TestNetworkSerialization(t *testing.T) {
 
 	tmpDir := t.TempDir()
 
-	network := &Network{}
-	require.NoError(network.EnsureDefaultConfig(&bytes.Buffer{}, "/path/to/avalanche/go", "", 1))
+	network := NewDefaultNetwork("testnet")
+	require.NoError(network.EnsureDefaultConfig(&bytes.Buffer{}, "/path/to/avalanche/go", ""))
 	require.NoError(network.Create(tmpDir))
 	// Ensure node runtime is initialized
 	require.NoError(network.readNodes())
diff --git a/tests/fixture/tmpnet/node.go b/tests/fixture/tmpnet/node.go
index 452d8d8e78ad..6b5d8759c951 100644
--- a/tests/fixture/tmpnet/node.go
+++ b/tests/fixture/tmpnet/node.go
@@ -104,16 +104,16 @@ func NewEphemeralNode(flags FlagsMap) *Node {
 }
 
 // Initializes the specified number of nodes.
-func NewNodes(count int) ([]*Node, error) {
+func NewNodesOrDie(count int) []*Node {
 	nodes := make([]*Node, count)
 	for i := range nodes {
 		node := NewNode("")
 		if err := node.EnsureKeys(); err != nil {
-			return nil, err
+			panic(err)
 		}
 		nodes[i] = node
 	}
-	return nodes, nil
+	return nodes
 }
 
 // Reads a node's configuration from the specified directory.
diff --git a/tests/upgrade/upgrade_test.go b/tests/upgrade/upgrade_test.go
index 7114ac291d43..d3632853bc31 100644
--- a/tests/upgrade/upgrade_test.go
+++ b/tests/upgrade/upgrade_test.go
@@ -45,9 +45,7 @@ var _ = ginkgo.Describe("[Upgrade]", func() {
 	require := require.New(ginkgo.GinkgoT())
 
 	ginkgo.It("can upgrade versions", func() {
-		network := &tmpnet.Network{
-			Owner: "avalanchego-upgrade",
-		}
+		network := tmpnet.NewDefaultNetwork("avalanchego-upgrade")
 		e2e.StartNetwork(network, avalancheGoExecPath, "" /* pluginDir */, 0 /* shutdownDelay */, false /* reuseNetwork */)
 
 		ginkgo.By(fmt.Sprintf("restarting all nodes with %q binary", avalancheGoExecPathToUpgradeTo))

From b5c6acc059fa8a287af89e474853ad52c9cd03a2 Mon Sep 17 00:00:00 2001
From: Maru Newby <maru.newby@avalabs.org>
Date: Tue, 7 May 2024 18:27:00 -0700
Subject: [PATCH 2/2] fixup: s/NewNodesOrDie/NewNodesOrPanic/

---
 tests/e2e/e2e_test.go            | 2 +-
 tests/fixture/tmpnet/README.md   | 2 +-
 tests/fixture/tmpnet/cmd/main.go | 2 +-
 tests/fixture/tmpnet/network.go  | 2 +-
 tests/fixture/tmpnet/node.go     | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index 08b0f2d2c03c..73c29b3bc83f 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -37,7 +37,7 @@ func init() {
 var _ = ginkgo.SynchronizedBeforeSuite(func() []byte {
 	// Run only once in the first ginkgo process
 
-	nodes := tmpnet.NewNodesOrDie(flagVars.NodeCount())
+	nodes := tmpnet.NewNodesOrPanic(flagVars.NodeCount())
 	subnets := vms.XSVMSubnets(nodes...)
 	return e2e.NewTestEnvironment(
 		flagVars,
diff --git a/tests/fixture/tmpnet/README.md b/tests/fixture/tmpnet/README.md
index a7652ce43d8c..3c0679b4410d 100644
--- a/tests/fixture/tmpnet/README.md
+++ b/tests/fixture/tmpnet/README.md
@@ -87,7 +87,7 @@ network := &tmpnet.Network{                   // Configure non-default values fo
     DefaultFlags: tmpnet.FlagsMap{
         config.LogLevelKey: "INFO",           // Change one of the network's defaults
     },
-    Nodes: tmpnet.NewNodesOrDie(5),           // Number of initial validating nodes
+    Nodes: tmpnet.NewNodesOrPanic(5),           // Number of initial validating nodes
     Subnets: []*tmpnet.Subnet{                // Subnets to create on the new network once it is running
         {
             Name: "xsvm-a",                   // User-defined name used to reference subnet in code and on disk
diff --git a/tests/fixture/tmpnet/cmd/main.go b/tests/fixture/tmpnet/cmd/main.go
index 039dd4c0b4da..0e6bcb1fd0a0 100644
--- a/tests/fixture/tmpnet/cmd/main.go
+++ b/tests/fixture/tmpnet/cmd/main.go
@@ -66,7 +66,7 @@ func main() {
 
 			network := &tmpnet.Network{
 				Owner: networkOwner,
-				Nodes: tmpnet.NewNodesOrDie(int(nodeCount)),
+				Nodes: tmpnet.NewNodesOrPanic(int(nodeCount)),
 			}
 
 			// Extreme upper bound, should never take this long
diff --git a/tests/fixture/tmpnet/network.go b/tests/fixture/tmpnet/network.go
index 5e7eea3be807..bd5b1b914efc 100644
--- a/tests/fixture/tmpnet/network.go
+++ b/tests/fixture/tmpnet/network.go
@@ -112,7 +112,7 @@ type Network struct {
 func NewDefaultNetwork(owner string) *Network {
 	return &Network{
 		Owner: owner,
-		Nodes: NewNodesOrDie(DefaultNodeCount),
+		Nodes: NewNodesOrPanic(DefaultNodeCount),
 	}
 }
 
diff --git a/tests/fixture/tmpnet/node.go b/tests/fixture/tmpnet/node.go
index 6b5d8759c951..99777e674c04 100644
--- a/tests/fixture/tmpnet/node.go
+++ b/tests/fixture/tmpnet/node.go
@@ -104,7 +104,7 @@ func NewEphemeralNode(flags FlagsMap) *Node {
 }
 
 // Initializes the specified number of nodes.
-func NewNodesOrDie(count int) []*Node {
+func NewNodesOrPanic(count int) []*Node {
 	nodes := make([]*Node, count)
 	for i := range nodes {
 		node := NewNode("")