Skip to content

Commit 1d60ff1

Browse files
committed
e2e: Migrate duplicate node id test from kurtosis
1 parent 5315f3e commit 1d60ff1

File tree

8 files changed

+274
-0
lines changed

8 files changed

+274
-0
lines changed

tests/e2e/e2e.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
package e2e
66

77
import (
8+
"os"
89
"time"
910

1011
ginkgo "github.com/onsi/ginkgo/v2"
1112
"github.com/stretchr/testify/require"
1213

14+
cfg "github.com/ava-labs/avalanchego/config"
15+
"github.com/ava-labs/avalanchego/tests"
1316
"github.com/ava-labs/avalanchego/tests/fixture"
1417
"github.com/ava-labs/avalanchego/tests/fixture/testnet"
1518
"github.com/ava-labs/avalanchego/tests/fixture/testnet/local"
@@ -24,6 +27,10 @@ const (
2427
// Enough for test/custom networks.
2528
DefaultConfirmTxTimeout = 20 * time.Second
2629

30+
// Defines default node bootstrap timeout.
31+
// Enough for test/custom networks.
32+
DefaultBootstrapTimeout = 20 * time.Second
33+
2734
DefaultTimeout = 2 * time.Minute
2835
)
2936

@@ -48,3 +55,27 @@ func (te *TestEnvironment) AllocateTestKeys(count int) []*secp256k1.PrivateKey {
4855
require.NoError(ginkgo.GinkgoT(), err)
4956
return keys
5057
}
58+
59+
// Add a temporary node that is only intended to be used by a single
60+
// test. Its ID and URI are not intended to be returned from The
61+
// Network instance to avoid accessibility from other tests.
62+
func AddTemporaryNode(network testnet.Network, flags testnet.FlagsMap) testnet.Node {
63+
require := require.New(ginkgo.GinkgoT())
64+
65+
// A temporary location ensures the node won't be accessible from
66+
// the Network instance.
67+
nodeDir, err := os.MkdirTemp("", "")
68+
require.NoError(err)
69+
70+
flags[cfg.DataDirKey] = nodeDir
71+
node, err := network.AddNode(ginkgo.GinkgoWriter, flags)
72+
require.NoError(err)
73+
74+
// Ensure node is stopped and its configuration removed
75+
ginkgo.DeferCleanup(func() {
76+
tests.Outf("Shutting down temporary node %s\n", node.GetID())
77+
require.NoError(node.Remove())
78+
})
79+
80+
return node
81+
}

tests/e2e/e2e_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
// ensure test packages are scanned by ginkgo
2525
_ "github.com/ava-labs/avalanchego/tests/e2e/banff"
26+
_ "github.com/ava-labs/avalanchego/tests/e2e/faultinjection"
2627
_ "github.com/ava-labs/avalanchego/tests/e2e/p"
2728
_ "github.com/ava-labs/avalanchego/tests/e2e/static-handlers"
2829
_ "github.com/ava-labs/avalanchego/tests/e2e/x/transfer"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package faultinjection
5+
6+
import (
7+
"context"
8+
"time"
9+
10+
ginkgo "github.com/onsi/ginkgo/v2"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/ava-labs/avalanchego/api/info"
14+
cfg "github.com/ava-labs/avalanchego/config"
15+
"github.com/ava-labs/avalanchego/ids"
16+
"github.com/ava-labs/avalanchego/tests/e2e"
17+
"github.com/ava-labs/avalanchego/tests/fixture/testnet"
18+
"github.com/ava-labs/avalanchego/utils/set"
19+
)
20+
21+
var _ = ginkgo.Describe("Duplicate node handling", func() {
22+
require := require.New(ginkgo.GinkgoT())
23+
24+
ginkgo.It("should ensure that a given Node ID (i.e. staking keypair) can be used at most once on a network", func() {
25+
network := e2e.Env.GetNetwork()
26+
nodes := network.GetNodes()
27+
28+
ginkgo.By("creating new node")
29+
newNode1 := e2e.AddTemporaryNode(network, testnet.FlagsMap{})
30+
ctx, cancel := context.WithTimeout(context.Background(), e2e.DefaultBootstrapTimeout)
31+
defer cancel()
32+
require.NoError(newNode1.WaitForHealth(ctx))
33+
34+
ginkgo.By("checking that the new node is connected to its peers")
35+
checkConnectedPeers(nodes, newNode1)
36+
37+
ginkgo.By("creating a second new node with the same staking keypair as the first new node")
38+
node1Flags := newNode1.GetConfig().Flags
39+
node2Flags := testnet.FlagsMap{
40+
cfg.StakingTLSKeyContentKey: node1Flags[cfg.StakingTLSKeyContentKey],
41+
cfg.StakingCertContentKey: node1Flags[cfg.StakingCertContentKey],
42+
}
43+
newNode2 := e2e.AddTemporaryNode(network, node2Flags)
44+
45+
ginkgo.By("checking that the second new node fails to become healthy within timeout")
46+
require.Never(func() bool {
47+
isHealthy, err := newNode2.IsHealthy(context.Background())
48+
require.NoError(err)
49+
return isHealthy
50+
}, e2e.DefaultBootstrapTimeout, time.Second, "node2 was able to bootstrap")
51+
52+
ginkgo.By("stopping the first new node")
53+
require.NoError(newNode1.Stop())
54+
55+
ginkgo.By("checking that the second new node becomes healthy within timeout")
56+
require.Eventually(func() bool {
57+
isHealthy, err := newNode2.IsHealthy(context.Background())
58+
require.NoError(err)
59+
return isHealthy
60+
}, e2e.DefaultBootstrapTimeout, time.Second, "node2 was not able to bootstrap")
61+
62+
ginkgo.By("checking that the second new node is connected to its peers")
63+
checkConnectedPeers(nodes, newNode2)
64+
})
65+
})
66+
67+
// Check that a new node is connected to existing nodes and vice versa
68+
func checkConnectedPeers(existingNodes []testnet.Node, newNode testnet.Node) {
69+
require := require.New(ginkgo.GinkgoT())
70+
71+
// Collect the node ids of the new node's peers
72+
infoClient := info.NewClient(newNode.GetProcessContext().URI)
73+
peers, err := infoClient.Peers(context.Background())
74+
require.NoError(err)
75+
peerIDs := set.NewSet[ids.NodeID](len(existingNodes))
76+
for _, peer := range peers {
77+
peerIDs.Add(peer.ID)
78+
}
79+
80+
newNodeID := newNode.GetID().String()
81+
for _, existingNode := range existingNodes {
82+
// Check that the existing node is a peer of the new node
83+
require.True(peerIDs.Contains(existingNode.GetID()))
84+
85+
// Check that the new node is a peer
86+
infoClient := info.NewClient(existingNode.GetProcessContext().URI)
87+
peers, err := infoClient.Peers(context.Background())
88+
require.NoError(err)
89+
isPeer := false
90+
for _, peer := range peers {
91+
if peer.ID.String() == newNodeID {
92+
isPeer = true
93+
break
94+
}
95+
}
96+
require.True(isPeer)
97+
}
98+
}

tests/fixture/testnet/cmd/main.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,32 @@ func main() {
9797
}
9898
rootCmd.AddCommand(stopNetworkCmd)
9999

100+
addNodeCmd := &cobra.Command{
101+
Use: "add-node",
102+
Short: "Adds a node to a local network",
103+
RunE: func(cmd *cobra.Command, args []string) error {
104+
if len(networkDir) == 0 {
105+
return fmt.Errorf("--network-dir or %s are required", local.NetworkDirEnvName)
106+
}
107+
network, err := local.ReadNetwork(networkDir)
108+
if err != nil {
109+
return err
110+
}
111+
node, err := network.AddLocalNode(os.Stdout, nil)
112+
if err != nil {
113+
return err
114+
}
115+
ctx, cancel := context.WithTimeout(context.Background(), local.DefaultNodeStartTimeout)
116+
defer cancel()
117+
if err := node.WaitForHealth(ctx); err != nil {
118+
return err
119+
}
120+
fmt.Fprintf(os.Stdout, "Added node %q to network configured at: %s\n", node.NodeID, network.Dir)
121+
return nil
122+
},
123+
}
124+
rootCmd.AddCommand(addNodeCmd)
125+
100126
if err := rootCmd.Execute(); err != nil {
101127
fmt.Fprintf(os.Stderr, "testnetctl failed: %v\n", err)
102128
os.Exit(1)

tests/fixture/testnet/interfaces.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
package testnet
55

66
import (
7+
"context"
8+
"io"
9+
710
"github.com/ava-labs/avalanchego/ids"
811
"github.com/ava-labs/avalanchego/node"
912
)
@@ -12,11 +15,16 @@ import (
1215
type Network interface {
1316
GetConfig() NetworkConfig
1417
GetNodes() []Node
18+
AddNode(w io.Writer, flags FlagsMap) (Node, error)
1519
}
1620

1721
// Defines node capabilities supportable regardless of how a network is orchestrated.
1822
type Node interface {
1923
GetID() ids.NodeID
2024
GetConfig() NodeConfig
2125
GetProcessContext() node.NodeProcessContext
26+
IsHealthy(ctx context.Context) (bool, error)
27+
WaitForHealth(ctx context.Context) error
28+
Stop() error
29+
Remove() error
2230
}

tests/fixture/testnet/local/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
RootDirEnvName = "TESTNETCTL_ROOT_DIR"
1919

2020
DefaultNetworkStartTimeout = 2 * time.Minute
21+
DefaultNodeStartTimeout = 30 * time.Second
2122

2223
DefaultNodeStartTimeout = 10 * time.Second
2324

tests/fixture/testnet/local/network.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"path/filepath"
1515
"strconv"
16+
"strings"
1617
"time"
1718

1819
"github.com/spf13/cast"
@@ -93,6 +94,22 @@ func (ln *LocalNetwork) GetNodes() []testnet.Node {
9394
return nodes
9495
}
9596

97+
// Adds a backend-agnostic node to the network
98+
func (ln *LocalNetwork) AddNode(w io.Writer, flags testnet.FlagsMap) (testnet.Node, error) {
99+
if flags == nil {
100+
flags = testnet.FlagsMap{}
101+
}
102+
node, err := ln.AddLocalNode(w, &LocalNode{
103+
NodeConfig: testnet.NodeConfig{
104+
Flags: flags,
105+
},
106+
})
107+
if err != nil {
108+
return nil, err
109+
}
110+
return node, nil
111+
}
112+
96113
// Starts a new network stored under the provided root dir. Required
97114
// configuration will be defaulted if not provided.
98115
func StartNetwork(
@@ -656,3 +673,67 @@ func (ln *LocalNetwork) ReadAll() error {
656673
}
657674
return ln.ReadNodes()
658675
}
676+
677+
func (ln *LocalNetwork) AddLocalNode(w io.Writer, node *LocalNode) (*LocalNode, error) {
678+
// Assume network configuration has been written to disk and is current in memory
679+
680+
if err := ln.ReadNodes(); err != nil {
681+
return nil, err
682+
}
683+
684+
maxPort := 0
685+
bootstrapIPs := []string{}
686+
bootstrapIDs := []string{}
687+
for _, node := range ln.Nodes {
688+
if len(node.StakingAddress) == 0 {
689+
// Node is not running
690+
continue
691+
}
692+
693+
bootstrapIPs = append(bootstrapIPs, node.StakingAddress)
694+
bootstrapIDs = append(bootstrapIDs, node.NodeID.String())
695+
696+
// Find the maximum port if using static ports
697+
if ln.UseStaticPorts {
698+
addressParts := strings.Split(node.StakingAddress, ":")
699+
rawPort := addressParts[len(addressParts)-1]
700+
port, err := strconv.Atoi(rawPort)
701+
if err != nil {
702+
return nil, err
703+
}
704+
if port > maxPort {
705+
maxPort = port
706+
}
707+
}
708+
}
709+
710+
if len(bootstrapIDs) == 0 {
711+
return nil, errors.New("failed to add node due to missing bootstrap nodes")
712+
}
713+
714+
if node == nil {
715+
node = NewLocalNode("")
716+
}
717+
718+
if err := ln.PopulateNodeConfig(node); err != nil {
719+
return nil, err
720+
}
721+
722+
// Configure the provided node for this network
723+
httpPort := 0
724+
stakingPort := 0
725+
if ln.UseStaticPorts {
726+
httpPort = maxPort
727+
stakingPort = maxPort + 1
728+
}
729+
node.SetNetworkingConfig(httpPort, stakingPort, bootstrapIDs, bootstrapIPs)
730+
731+
if err := node.WriteConfig(); err != nil {
732+
return nil, err
733+
}
734+
if err := node.Start(w, ln.ExecPath); err != nil {
735+
return nil, err
736+
}
737+
738+
return node, nil
739+
}

tests/fixture/testnet/local/node.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,22 @@ func (n *LocalNode) IsHealthy(ctx context.Context) (bool, error) {
272272
return health.Healthy, nil
273273
}
274274

275+
func (n *LocalNode) WaitForHealth(ctx context.Context) error {
276+
for {
277+
select {
278+
case <-ctx.Done():
279+
return errors.New("failed to see node health before timeout")
280+
default:
281+
if healthy, err := n.IsHealthy(ctx); err != nil {
282+
return err
283+
} else if healthy {
284+
return nil
285+
}
286+
}
287+
time.Sleep(200 * time.Millisecond)
288+
}
289+
}
290+
275291
func (n *LocalNode) WaitForProcessContext(ctx context.Context) error {
276292
ctx, cancel := context.WithTimeout(ctx, DefaultNodeStartTimeout)
277293
defer cancel()
@@ -293,3 +309,15 @@ func (n *LocalNode) WaitForProcessContext(ctx context.Context) error {
293309
}
294310
return nil
295311
}
312+
313+
// Ensure the node is stopped and its configuration removed.
314+
func (n *LocalNode) Remove() error {
315+
// Ensure the node is stopped before removing its configuration
316+
if err := n.Stop(); err != nil {
317+
return fmt.Errorf("failed to stop node %s: %w", n.NodeID, err)
318+
}
319+
if err := os.RemoveAll(n.GetDataDir()); err != nil {
320+
return fmt.Errorf("failed to remove configuration for node %s: %w", n.NodeID, err)
321+
}
322+
return nil
323+
}

0 commit comments

Comments
 (0)