aboutsummaryrefslogtreecommitdiffstats
path: root/p2p/simulations/network_test.go
blob: f34935265127fe47535c7e91e950850163612e74 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package simulations

import (
    "context"
    "fmt"
    "testing"
    "time"

    "github.com/ethereum/go-ethereum/p2p/enode"
    "github.com/ethereum/go-ethereum/p2p/simulations/adapters"
)

// TestNetworkSimulation creates a multi-node simulation network with each node
// connected in a ring topology, checks that all nodes successfully handshake
// with each other and that a snapshot fully represents the desired topology
func TestNetworkSimulation(t *testing.T) {
    // create simulation network with 20 testService nodes
    adapter := adapters.NewSimAdapter(adapters.Services{
        "test": newTestService,
    })
    network := NewNetwork(adapter, &NetworkConfig{
        DefaultService: "test",
    })
    defer network.Shutdown()
    nodeCount := 20
    ids := make([]enode.ID, nodeCount)
    for i := 0; i < nodeCount; i++ {
        conf := adapters.RandomNodeConfig()
        node, err := network.NewNodeWithConfig(conf)
        if err != nil {
            t.Fatalf("error creating node: %s", err)
        }
        if err := network.Start(node.ID()); err != nil {
            t.Fatalf("error starting node: %s", err)
        }
        ids[i] = node.ID()
    }

    // perform a check which connects the nodes in a ring (so each node is
    // connected to exactly two peers) and then checks that all nodes
    // performed two handshakes by checking their peerCount
    action := func(_ context.Context) error {
        for i, id := range ids {
            peerID := ids[(i+1)%len(ids)]
            if err := network.Connect(id, peerID); err != nil {
                return err
            }
        }
        return nil
    }
    check := func(ctx context.Context, id enode.ID) (bool, error) {
        // check we haven't run out of time
        select {
        case <-ctx.Done():
            return false, ctx.Err()
        default:
        }

        // get the node
        node := network.GetNode(id)
        if node == nil {
            return false, fmt.Errorf("unknown node: %s", id)
        }

        // check it has exactly two peers
        client, err := node.Client()
        if err != nil {
            return false, err
        }
        var peerCount int64
        if err := client.CallContext(ctx, &peerCount, "test_peerCount"); err != nil {
            return false, err
        }
        switch {
        case peerCount < 2:
            return false, nil
        case peerCount == 2:
            return true, nil
        default:
            return false, fmt.Errorf("unexpected peerCount: %d", peerCount)
        }
    }

    timeout := 30 * time.Second
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    // trigger a check every 100ms
    trigger := make(chan enode.ID)
    go triggerChecks(ctx, ids, trigger, 100*time.Millisecond)

    result := NewSimulation(network).Run(ctx, &Step{
        Action:  action,
        Trigger: trigger,
        Expect: &Expectation{
            Nodes: ids,
            Check: check,
        },
    })
    if result.Error != nil {
        t.Fatalf("simulation failed: %s", result.Error)
    }

    // take a network snapshot and check it contains the correct topology
    snap, err := network.Snapshot()
    if err != nil {
        t.Fatal(err)
    }
    if len(snap.Nodes) != nodeCount {
        t.Fatalf("expected snapshot to contain %d nodes, got %d", nodeCount, len(snap.Nodes))
    }
    if len(snap.Conns) != nodeCount {
        t.Fatalf("expected snapshot to contain %d connections, got %d", nodeCount, len(snap.Conns))
    }
    for i, id := range ids {
        conn := snap.Conns[i]
        if conn.One != id {
            t.Fatalf("expected conn[%d].One to be %s, got %s", i, id, conn.One)
        }
        peerID := ids[(i+1)%len(ids)]
        if conn.Other != peerID {
            t.Fatalf("expected conn[%d].Other to be %s, got %s", i, peerID, conn.Other)
        }
    }
}

func triggerChecks(ctx context.Context, ids []enode.ID, trigger chan enode.ID, interval time.Duration) {
    tick := time.NewTicker(interval)
    defer tick.Stop()
    for {
        select {
        case <-tick.C:
            for _, id := range ids {
                select {
                case trigger <- id:
                case <-ctx.Done():
                    return
                }
            }
        case <-ctx.Done():
            return
        }
    }
}