aboutsummaryrefslogtreecommitdiffstats
path: root/p2p/dial_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'p2p/dial_test.go')
-rw-r--r--p2p/dial_test.go482
1 files changed, 482 insertions, 0 deletions
diff --git a/p2p/dial_test.go b/p2p/dial_test.go
new file mode 100644
index 000000000..78568c5ed
--- /dev/null
+++ b/p2p/dial_test.go
@@ -0,0 +1,482 @@
+package p2p
+
+import (
+ "encoding/binary"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/davecgh/go-spew/spew"
+ "github.com/ethereum/go-ethereum/p2p/discover"
+)
+
+func init() {
+ spew.Config.Indent = "\t"
+}
+
+type dialtest struct {
+ init *dialstate // state before and after the test.
+ rounds []round
+}
+
+type round struct {
+ peers []*Peer // current peer set
+ done []task // tasks that got done this round
+ new []task // the result must match this one
+}
+
+func runDialTest(t *testing.T, test dialtest) {
+ var (
+ vtime time.Time
+ running int
+ )
+ pm := func(ps []*Peer) map[discover.NodeID]*Peer {
+ m := make(map[discover.NodeID]*Peer)
+ for _, p := range ps {
+ m[p.rw.id] = p
+ }
+ return m
+ }
+ for i, round := range test.rounds {
+ for _, task := range round.done {
+ running--
+ if running < 0 {
+ panic("running task counter underflow")
+ }
+ test.init.taskDone(task, vtime)
+ }
+
+ new := test.init.newTasks(running, pm(round.peers), vtime)
+ if !sametasks(new, round.new) {
+ t.Errorf("round %d: new tasks mismatch:\ngot %v\nwant %v\nstate: %v\nrunning: %v\n",
+ i, spew.Sdump(new), spew.Sdump(round.new), spew.Sdump(test.init), spew.Sdump(running))
+ }
+
+ // Time advances by 16 seconds on every round.
+ vtime = vtime.Add(16 * time.Second)
+ running += len(new)
+ }
+}
+
+type fakeTable []*discover.Node
+
+func (t fakeTable) Self() *discover.Node { return new(discover.Node) }
+func (t fakeTable) Close() {}
+func (t fakeTable) Bootstrap([]*discover.Node) {}
+func (t fakeTable) Lookup(target discover.NodeID) []*discover.Node {
+ return nil
+}
+func (t fakeTable) ReadRandomNodes(buf []*discover.Node) int {
+ return copy(buf, t)
+}
+
+// This test checks that dynamic dials are launched from discovery results.
+func TestDialStateDynDial(t *testing.T) {
+ runDialTest(t, dialtest{
+ init: newDialState(nil, fakeTable{}, 5),
+ rounds: []round{
+ // A discovery query is launched.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: staticDialedConn, id: uintID(0)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ },
+ new: []task{&discoverTask{bootstrap: true}},
+ },
+ // Dynamic dials are launched when it completes.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: staticDialedConn, id: uintID(0)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ },
+ done: []task{
+ &discoverTask{bootstrap: true, results: []*discover.Node{
+ {ID: uintID(2)}, // this one is already connected and not dialed.
+ {ID: uintID(3)},
+ {ID: uintID(4)},
+ {ID: uintID(5)},
+ {ID: uintID(6)}, // these are not tried because max dyn dials is 5
+ {ID: uintID(7)}, // ...
+ }},
+ },
+ new: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(3)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(4)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(5)}},
+ },
+ },
+ // Some of the dials complete but no new ones are launched yet because
+ // the sum of active dial count and dynamic peer count is == maxDynDials.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: staticDialedConn, id: uintID(0)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(3)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(4)}},
+ },
+ done: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(3)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(4)}},
+ },
+ },
+ // No new dial tasks are launched in the this round because
+ // maxDynDials has been reached.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: staticDialedConn, id: uintID(0)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(3)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(4)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(5)}},
+ },
+ done: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(5)}},
+ },
+ new: []task{
+ &waitExpireTask{Duration: 14 * time.Second},
+ },
+ },
+ // In this round, the peer with id 2 drops off. The query
+ // results from last discovery lookup are reused.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: staticDialedConn, id: uintID(0)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(3)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(4)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(5)}},
+ },
+ new: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(6)}},
+ },
+ },
+ // More peers (3,4) drop off and dial for ID 6 completes.
+ // The last query result from the discovery lookup is reused
+ // and a new one is spawned because more candidates are needed.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: staticDialedConn, id: uintID(0)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(5)}},
+ },
+ done: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(6)}},
+ },
+ new: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(7)}},
+ &discoverTask{},
+ },
+ },
+ // Peer 7 is connected, but there still aren't enough dynamic peers
+ // (4 out of 5). However, a discovery is already running, so ensure
+ // no new is started.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: staticDialedConn, id: uintID(0)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(5)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(7)}},
+ },
+ done: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(7)}},
+ },
+ },
+ // Finish the running node discovery with an empty set. A new lookup
+ // should be immediately requested.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: staticDialedConn, id: uintID(0)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(5)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(7)}},
+ },
+ done: []task{
+ &discoverTask{},
+ },
+ new: []task{
+ &discoverTask{},
+ },
+ },
+ },
+ })
+}
+
+func TestDialStateDynDialFromTable(t *testing.T) {
+ // This table always returns the same random nodes
+ // in the order given below.
+ table := fakeTable{
+ {ID: uintID(1)},
+ {ID: uintID(2)},
+ {ID: uintID(3)},
+ {ID: uintID(4)},
+ {ID: uintID(5)},
+ {ID: uintID(6)},
+ {ID: uintID(7)},
+ {ID: uintID(8)},
+ }
+
+ runDialTest(t, dialtest{
+ init: newDialState(nil, table, 10),
+ rounds: []round{
+ // Discovery bootstrap is launched.
+ {
+ new: []task{&discoverTask{bootstrap: true}},
+ },
+ // 5 out of 8 of the nodes returned by ReadRandomNodes are dialed.
+ {
+ done: []task{
+ &discoverTask{bootstrap: true},
+ },
+ new: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(1)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(2)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(3)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(4)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(5)}},
+ &discoverTask{bootstrap: false},
+ },
+ },
+ // Dialing nodes 1,2 succeeds. Dials from the lookup are launched.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ },
+ done: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(1)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(2)}},
+ &discoverTask{results: []*discover.Node{
+ {ID: uintID(10)},
+ {ID: uintID(11)},
+ {ID: uintID(12)},
+ }},
+ },
+ new: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(10)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(11)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(12)}},
+ &discoverTask{bootstrap: false},
+ },
+ },
+ // Dialing nodes 3,4,5 fails. The dials from the lookup succeed.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(10)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(11)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(12)}},
+ },
+ done: []task{
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(3)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(4)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(5)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(10)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(11)}},
+ &dialTask{dynDialedConn, &discover.Node{ID: uintID(12)}},
+ },
+ },
+ // Waiting for expiry. No waitExpireTask is launched because the
+ // discovery query is still running.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(10)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(11)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(12)}},
+ },
+ },
+ // Nodes 3,4 are not tried again because only the first two
+ // returned random nodes (nodes 1,2) are tried and they're
+ // already connected.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(10)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(11)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(12)}},
+ },
+ },
+ },
+ })
+}
+
+// This test checks that static dials are launched.
+func TestDialStateStaticDial(t *testing.T) {
+ wantStatic := []*discover.Node{
+ {ID: uintID(1)},
+ {ID: uintID(2)},
+ {ID: uintID(3)},
+ {ID: uintID(4)},
+ {ID: uintID(5)},
+ }
+
+ runDialTest(t, dialtest{
+ init: newDialState(wantStatic, fakeTable{}, 0),
+ rounds: []round{
+ // Static dials are launched for the nodes that
+ // aren't yet connected.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ },
+ new: []task{
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(3)}},
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(4)}},
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(5)}},
+ },
+ },
+ // No new tasks are launched in this round because all static
+ // nodes are either connected or still being dialed.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ {rw: &conn{flags: staticDialedConn, id: uintID(3)}},
+ },
+ done: []task{
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(3)}},
+ },
+ },
+ // No new dial tasks are launched because all static
+ // nodes are now connected.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ {rw: &conn{flags: staticDialedConn, id: uintID(3)}},
+ {rw: &conn{flags: staticDialedConn, id: uintID(4)}},
+ {rw: &conn{flags: staticDialedConn, id: uintID(5)}},
+ },
+ done: []task{
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(4)}},
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(5)}},
+ },
+ new: []task{
+ &waitExpireTask{Duration: 14 * time.Second},
+ },
+ },
+ // Wait a round for dial history to expire, no new tasks should spawn.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ {rw: &conn{flags: staticDialedConn, id: uintID(3)}},
+ {rw: &conn{flags: staticDialedConn, id: uintID(4)}},
+ {rw: &conn{flags: staticDialedConn, id: uintID(5)}},
+ },
+ },
+ // If a static node is dropped, it should be immediately redialed,
+ // irrespective whether it was originally static or dynamic.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: staticDialedConn, id: uintID(3)}},
+ {rw: &conn{flags: staticDialedConn, id: uintID(5)}},
+ },
+ new: []task{
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(2)}},
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(4)}},
+ },
+ },
+ },
+ })
+}
+
+// This test checks that past dials are not retried for some time.
+func TestDialStateCache(t *testing.T) {
+ wantStatic := []*discover.Node{
+ {ID: uintID(1)},
+ {ID: uintID(2)},
+ {ID: uintID(3)},
+ }
+
+ runDialTest(t, dialtest{
+ init: newDialState(wantStatic, fakeTable{}, 0),
+ rounds: []round{
+ // Static dials are launched for the nodes that
+ // aren't yet connected.
+ {
+ peers: nil,
+ new: []task{
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(1)}},
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(2)}},
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(3)}},
+ },
+ },
+ // No new tasks are launched in this round because all static
+ // nodes are either connected or still being dialed.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: staticDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: staticDialedConn, id: uintID(2)}},
+ },
+ done: []task{
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(1)}},
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(2)}},
+ },
+ },
+ // A salvage task is launched to wait for node 3's history
+ // entry to expire.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ },
+ done: []task{
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(3)}},
+ },
+ new: []task{
+ &waitExpireTask{Duration: 14 * time.Second},
+ },
+ },
+ // Still waiting for node 3's entry to expire in the cache.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ },
+ },
+ // The cache entry for node 3 has expired and is retried.
+ {
+ peers: []*Peer{
+ {rw: &conn{flags: dynDialedConn, id: uintID(1)}},
+ {rw: &conn{flags: dynDialedConn, id: uintID(2)}},
+ },
+ new: []task{
+ &dialTask{staticDialedConn, &discover.Node{ID: uintID(3)}},
+ },
+ },
+ },
+ })
+}
+
+// compares task lists but doesn't care about the order.
+func sametasks(a, b []task) bool {
+ if len(a) != len(b) {
+ return false
+ }
+next:
+ for _, ta := range a {
+ for _, tb := range b {
+ if reflect.DeepEqual(ta, tb) {
+ continue next
+ }
+ }
+ return false
+ }
+ return true
+}
+
+func uintID(i uint32) discover.NodeID {
+ var id discover.NodeID
+ binary.BigEndian.PutUint32(id[:], i)
+ return id
+}