Skip to content

Commit 70b0af8

Browse files
committed
e2e: Migrate duplicate node id test from kurtosis
1 parent 35a3297 commit 70b0af8

File tree

9 files changed

+275
-10
lines changed

9 files changed

+275
-10
lines changed

.github/workflows/test.e2e.persistent.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ jobs:
2828
run: ./scripts/build.sh -r
2929
- name: Run e2e tests with persistent network
3030
shell: bash
31-
run: ./scripts/tests.e2e.persistent.sh ./build/avalanchego
31+
run: E2E_SERIAL=1 ./scripts/tests.e2e.persistent.sh ./build/avalanchego

.github/workflows/test.e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ jobs:
2828
run: ./scripts/build.sh -r
2929
- name: Run e2e tests
3030
shell: bash
31-
run: scripts/tests.e2e.sh ./build/avalanchego
31+
run: E2E_SERIAL=1 ./scripts/tests.e2e.sh ./build/avalanchego

scripts/tests.e2e.sh

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,27 @@ else
4444
fi
4545

4646
#################################
47-
# - Execute in parallel (-p) with the ginkgo cli to minimize execution time.
48-
# The test binary by itself isn't capable of running specs in parallel.
47+
# Determine ginkgo args
48+
GINKGO_ARGS=""
49+
if [[ -n "${E2E_SERIAL:-}" ]]; then
50+
# Specs will be executed serially. This supports running e2e tests in CI
51+
# where parallel execution of tests that start new nodes beyond the
52+
# initial set of validators could overload the free tier CI workers.
53+
# Forcing serial execution in this test script instead of marking
54+
# resource-hungry tests as serial supports executing the test suite faster
55+
# on powerful development workstations.
56+
echo "tests will be executed serially to minimize resource requirements"
57+
else
58+
# Enable parallel execution of specs defined in the test binary, which
59+
# requires invoking the binary via the ginkgo cli. The test binary by
60+
# itself isn't capable of executing specs in parallel.
61+
echo "tests will be executed in parallel"
62+
GINKGO_ARGS="-p"
63+
fi
64+
65+
#################################
4966
# - Execute in random order to identify unwanted dependency
50-
ginkgo -p -v --randomize-all ./tests/e2e/e2e.test -- ${E2E_ARGS} \
67+
ginkgo ${GINKGO_ARGS} -v --randomize-all ./tests/e2e/e2e.test -- ${E2E_ARGS} \
5168
&& EXIT_CODE=$? || EXIT_CODE=$?
5269

5370
if [[ ${EXIT_CODE} -gt 0 ]]; then

tests/e2e/e2e.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
package e2e
66

77
import (
8+
"context"
89
"math/rand"
10+
"os"
911
"time"
1012

1113
ginkgo "github.com/onsi/ginkgo/v2"
1214
"github.com/stretchr/testify/require"
1315

16+
cfg "github.com/ava-labs/avalanchego/config"
17+
"github.com/ava-labs/avalanchego/tests"
1418
"github.com/ava-labs/avalanchego/tests/fixture"
1519
"github.com/ava-labs/avalanchego/tests/fixture/testnet"
1620
"github.com/ava-labs/avalanchego/tests/fixture/testnet/local"
@@ -25,6 +29,10 @@ const (
2529
// Enough for test/custom networks.
2630
DefaultConfirmTxTimeout = 20 * time.Second
2731

32+
// This interval should represent the upper bound of the time
33+
// required to start a new node on a local test network.
34+
DefaultNodeStartTimeout = 20 * time.Second
35+
2836
// A long default timeout used to timeout failed operations but
2937
// unlikely to induce flaking due to unexpected resource
3038
// contention.
@@ -69,3 +77,34 @@ func (te *TestEnvironment) AllocateFundedKeys(count int) []*secp256k1.PrivateKey
6977
func (te *TestEnvironment) AllocateFundedKey() *secp256k1.PrivateKey {
7078
return te.AllocateFundedKeys(1)[0]
7179
}
80+
81+
// Add a temporary node that is only intended to be used by a single
82+
// test. Its ID and URI are not intended to be returned from The
83+
// Network instance to avoid accessibility from other tests.
84+
func AddTemporaryNode(network testnet.Network, flags testnet.FlagsMap) testnet.Node {
85+
require := require.New(ginkgo.GinkgoT())
86+
87+
// A temporary location ensures the node won't be accessible from
88+
// the Network instance.
89+
nodeDir, err := os.MkdirTemp("", "")
90+
require.NoError(err)
91+
92+
flags[cfg.DataDirKey] = nodeDir
93+
node, err := network.AddNode(ginkgo.GinkgoWriter, flags)
94+
require.NoError(err)
95+
96+
// Ensure node is stopped and its configuration removed
97+
ginkgo.DeferCleanup(func() {
98+
tests.Outf("Shutting down temporary node %s\n", node.GetID())
99+
require.NoError(node.Remove())
100+
})
101+
102+
return node
103+
}
104+
105+
// Wait for the given node to report healthy.
106+
func WaitForHealthy(node testnet.Node) {
107+
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
108+
defer cancel()
109+
require.NoError(ginkgo.GinkgoT(), node.WaitForHealthy(ctx))
110+
}

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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
9+
ginkgo "github.com/onsi/ginkgo/v2"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/ava-labs/avalanchego/api/info"
13+
cfg "github.com/ava-labs/avalanchego/config"
14+
"github.com/ava-labs/avalanchego/ids"
15+
"github.com/ava-labs/avalanchego/tests/e2e"
16+
"github.com/ava-labs/avalanchego/tests/fixture/testnet"
17+
"github.com/ava-labs/avalanchego/utils/set"
18+
)
19+
20+
var _ = ginkgo.Describe("Duplicate node handling", func() {
21+
require := require.New(ginkgo.GinkgoT())
22+
23+
ginkgo.It("should ensure that a given Node ID (i.e. staking keypair) can be used at most once on a network", func() {
24+
network := e2e.Env.GetNetwork()
25+
nodes := network.GetNodes()
26+
27+
ginkgo.By("creating new node")
28+
node1 := e2e.AddTemporaryNode(network, testnet.FlagsMap{})
29+
e2e.WaitForHealthy(node1)
30+
31+
ginkgo.By("checking that the new node is connected to its peers")
32+
checkConnectedPeers(nodes, node1)
33+
34+
ginkgo.By("creating a second new node with the same staking keypair as the first new node")
35+
node1Flags := node1.GetConfig().Flags
36+
node2Flags := testnet.FlagsMap{
37+
cfg.StakingTLSKeyContentKey: node1Flags[cfg.StakingTLSKeyContentKey],
38+
cfg.StakingCertContentKey: node1Flags[cfg.StakingCertContentKey],
39+
}
40+
node2 := e2e.AddTemporaryNode(network, node2Flags)
41+
42+
ginkgo.By("checking that the second new node fails to become healthy within timeout")
43+
ctx, cancel := context.WithTimeout(context.Background(), e2e.DefaultNodeStartTimeout)
44+
defer cancel()
45+
err := node2.WaitForHealthy(ctx)
46+
require.ErrorIs(err, context.DeadlineExceeded)
47+
48+
ginkgo.By("stopping the first new node")
49+
require.NoError(node1.Stop())
50+
51+
ginkgo.By("checking that the second new node becomes healthy within timeout")
52+
e2e.WaitForHealthy(node2)
53+
54+
ginkgo.By("checking that the second new node is connected to its peers")
55+
checkConnectedPeers(nodes, node2)
56+
})
57+
})
58+
59+
// Check that a new node is connected to existing nodes and vice versa
60+
func checkConnectedPeers(existingNodes []testnet.Node, newNode testnet.Node) {
61+
require := require.New(ginkgo.GinkgoT())
62+
63+
// Collect the node ids of the new node's peers
64+
infoClient := info.NewClient(newNode.GetProcessContext().URI)
65+
peers, err := infoClient.Peers(context.Background())
66+
require.NoError(err)
67+
peerIDs := set.NewSet[ids.NodeID](len(existingNodes))
68+
for _, peer := range peers {
69+
peerIDs.Add(peer.ID)
70+
}
71+
72+
newNodeID := newNode.GetID().String()
73+
for _, existingNode := range existingNodes {
74+
// Check that the existing node is a peer of the new node
75+
require.True(peerIDs.Contains(existingNode.GetID()))
76+
77+
// Check that the new node is a peer
78+
infoClient := info.NewClient(existingNode.GetProcessContext().URI)
79+
peers, err := infoClient.Peers(context.Background())
80+
require.NoError(err)
81+
isPeer := false
82+
for _, peer := range peers {
83+
if peer.ID.String() == newNodeID {
84+
isPeer = true
85+
break
86+
}
87+
}
88+
require.True(isPeer)
89+
}
90+
}

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+
WaitForHealthy(ctx context.Context) error
28+
Stop() error
29+
Remove() error
2230
}

tests/fixture/testnet/local/network.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ var (
3434
errInvalidKeyCount = errors.New("failed to populate local network config: non-zero key count is only valid for a network without keys")
3535
errLocalNetworkDirNotSet = errors.New("local network directory not set - has Create() been called?")
3636
errInvalidNetworkDir = errors.New("failed to write local network: invalid network directory")
37-
errNotHealthyBeforeTimeout = errors.New("failed to see all nodes healthy before timeout")
37+
errNotHealthyBeforeTimeout = fmt.Errorf("failed to see all nodes healthy before timeout: %w", context.DeadlineExceeded)
38+
errMissingBootstrapNodes = errors.New("failed to add node due to missing bootstrap nodes")
3839
)
3940

4041
// Default root dir for storing networks and their configuration.
@@ -101,6 +102,22 @@ func (ln *LocalNetwork) GetNodes() []testnet.Node {
101102
return nodes
102103
}
103104

105+
// Adds a backend-agnostic node to the network
106+
func (ln *LocalNetwork) AddNode(w io.Writer, flags testnet.FlagsMap) (testnet.Node, error) {
107+
if flags == nil {
108+
flags = testnet.FlagsMap{}
109+
}
110+
node, err := ln.AddLocalNode(w, &LocalNode{
111+
NodeConfig: testnet.NodeConfig{
112+
Flags: flags,
113+
},
114+
})
115+
if err != nil {
116+
return nil, err
117+
}
118+
return node, nil
119+
}
120+
104121
// Starts a new network stored under the provided root dir. Required
105122
// configuration will be defaulted if not provided.
106123
func StartNetwork(
@@ -655,3 +672,51 @@ func (ln *LocalNetwork) ReadAll() error {
655672
}
656673
return ln.ReadNodes()
657674
}
675+
676+
func (ln *LocalNetwork) AddLocalNode(w io.Writer, node *LocalNode) (*LocalNode, error) {
677+
// Assume network configuration has been written to disk and is current in memory
678+
679+
if node == nil {
680+
node = NewLocalNode("")
681+
}
682+
if err := ln.PopulateNodeConfig(node); err != nil {
683+
return nil, err
684+
}
685+
686+
// Use dynamic port allocation. Starting a network with static ports may be
687+
// faster by allowing bootstrap IPs to be determined statically, but
688+
// bootstrap IPs are already available when adding a node to an existing
689+
// network.
690+
var httpPort uint16 = 0
691+
var stakingPort uint16 = 0
692+
693+
// Collect staking addresses of running nodes for use in bootstraping the new node
694+
if err := ln.ReadNodes(); err != nil {
695+
return nil, fmt.Errorf("failed to read local network nodes: %w", err)
696+
}
697+
bootstrapIPs := []string{}
698+
bootstrapIDs := []string{}
699+
for _, node := range ln.Nodes {
700+
if len(node.StakingAddress) == 0 {
701+
// Node is not running
702+
continue
703+
}
704+
705+
bootstrapIPs = append(bootstrapIPs, node.StakingAddress)
706+
bootstrapIDs = append(bootstrapIDs, node.NodeID.String())
707+
}
708+
if len(bootstrapIDs) == 0 {
709+
return nil, errMissingBootstrapNodes
710+
}
711+
712+
node.SetNetworkingConfigDefaults(httpPort, stakingPort, bootstrapIDs, bootstrapIPs)
713+
714+
if err := node.WriteConfig(); err != nil {
715+
return nil, err
716+
}
717+
if err := node.Start(w, ln.ExecPath); err != nil {
718+
return nil, err
719+
}
720+
721+
return node, nil
722+
}

0 commit comments

Comments
 (0)