From 04fcae207dad27e1b7333da4cfb7319c071e622b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Mon, 10 Apr 2017 19:33:41 +0300 Subject: p2p: if no nodes are connected, attempt dialing bootnodes (#13874) --- p2p/dial.go | 28 ++++++++++++++-- p2p/dial_test.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++---- p2p/server.go | 2 +- 3 files changed, 121 insertions(+), 9 deletions(-) diff --git a/p2p/dial.go b/p2p/dial.go index bb3befab2..b77971396 100644 --- a/p2p/dial.go +++ b/p2p/dial.go @@ -38,6 +38,10 @@ const ( // once every few seconds. lookupInterval = 4 * time.Second + // If no peers are found for this amount of time, the initial bootnodes are + // attempted to be connected. + fallbackInterval = 20 * time.Second + // Endpoint resolution is throttled with bounded backoff. initialResolveDelay = 60 * time.Second maxResolveDelay = time.Hour @@ -57,6 +61,9 @@ type dialstate struct { randomNodes []*discover.Node // filled from Table static map[discover.NodeID]*dialTask hist *dialHistory + + start time.Time // time when the dialer was first used + bootnodes []*discover.Node // default dials when there are no peers } type discoverTable interface { @@ -102,16 +109,18 @@ type waitExpireTask struct { time.Duration } -func newDialState(static []*discover.Node, ntab discoverTable, maxdyn int, netrestrict *netutil.Netlist) *dialstate { +func newDialState(static []*discover.Node, bootnodes []*discover.Node, ntab discoverTable, maxdyn int, netrestrict *netutil.Netlist) *dialstate { s := &dialstate{ maxDynDials: maxdyn, ntab: ntab, netrestrict: netrestrict, static: make(map[discover.NodeID]*dialTask), dialing: make(map[discover.NodeID]connFlag), + bootnodes: make([]*discover.Node, len(bootnodes)), randomNodes: make([]*discover.Node, maxdyn/2), hist: new(dialHistory), } + copy(s.bootnodes, bootnodes) for _, n := range static { s.addStatic(n) } @@ -130,6 +139,10 @@ func (s *dialstate) removeStatic(n *discover.Node) { } func (s *dialstate) newTasks(nRunning int, peers map[discover.NodeID]*Peer, now time.Time) []task { + if s.start == (time.Time{}) { + s.start = now + } + var newtasks []task addDial := func(flag connFlag, n *discover.Node) bool { if err := s.checkDial(n, peers); err != nil { @@ -169,7 +182,18 @@ func (s *dialstate) newTasks(nRunning int, peers map[discover.NodeID]*Peer, now newtasks = append(newtasks, t) } } - + // If we don't have any peers whatsoever, try to dial a random bootnode. This + // scenario is useful for the testnet (and private networks) where the discovery + // table might be full of mostly bad peers, making it hard to find good ones. + if len(peers) == 0 && len(s.bootnodes) > 0 && needDynDials > 0 && now.Sub(s.start) > fallbackInterval { + bootnode := s.bootnodes[0] + s.bootnodes = append(s.bootnodes[:0], s.bootnodes[1:]...) + s.bootnodes = append(s.bootnodes, bootnode) + + if addDial(dynDialedConn, bootnode) { + needDynDials-- + } + } // Use random nodes from the table for half of the necessary // dynamic dials. randomCandidates := needDynDials / 2 diff --git a/p2p/dial_test.go b/p2p/dial_test.go index c850233db..08e863bae 100644 --- a/p2p/dial_test.go +++ b/p2p/dial_test.go @@ -87,7 +87,7 @@ func (t fakeTable) ReadRandomNodes(buf []*discover.Node) int { return copy(buf, // This test checks that dynamic dials are launched from discovery results. func TestDialStateDynDial(t *testing.T) { runDialTest(t, dialtest{ - init: newDialState(nil, fakeTable{}, 5, nil), + init: newDialState(nil, nil, fakeTable{}, 5, nil), rounds: []round{ // A discovery query is launched. { @@ -219,6 +219,94 @@ func TestDialStateDynDial(t *testing.T) { }) } +// Tests that bootnodes are dialed if no peers are connectd, but not otherwise. +func TestDialStateDynDialBootnode(t *testing.T) { + bootnodes := []*discover.Node{ + {ID: uintID(1)}, + {ID: uintID(2)}, + {ID: uintID(3)}, + } + table := fakeTable{ + {ID: uintID(4)}, + {ID: uintID(5)}, + {ID: uintID(6)}, + {ID: uintID(7)}, + {ID: uintID(8)}, + } + runDialTest(t, dialtest{ + init: newDialState(nil, bootnodes, table, 5, nil), + rounds: []round{ + // 2 dynamic dials attempted, bootnodes pending fallback interval + { + new: []task{ + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &discoverTask{}, + }, + }, + // No dials succeed, bootnodes still pending fallback interval + { + done: []task{ + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + }, + }, + // No dials succeed, bootnodes still pending fallback interval + {}, + // No dials succeed, 2 dynamic dials attempted and 1 bootnode too as fallback interval was reached + { + new: []task{ + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + }, + }, + // No dials succeed, 2nd bootnode is attempted + { + done: []task{ + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + }, + new: []task{ + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(2)}}, + }, + }, + // No dials succeed, 3rd bootnode is attempted + { + done: []task{ + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(2)}}, + }, + new: []task{ + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(3)}}, + }, + }, + // No dials succeed, 1st bootnode is attempted again, expired random nodes retried + { + done: []task{ + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(3)}}, + }, + new: []task{ + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + }, + }, + // Random dial succeeds, no more bootnodes are attempted + { + peers: []*Peer{ + {rw: &conn{flags: dynDialedConn, id: uintID(4)}}, + }, + done: []task{ + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, + &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + }, + }, + }, + }) +} + func TestDialStateDynDialFromTable(t *testing.T) { // This table always returns the same random nodes // in the order given below. @@ -234,7 +322,7 @@ func TestDialStateDynDialFromTable(t *testing.T) { } runDialTest(t, dialtest{ - init: newDialState(nil, table, 10, nil), + init: newDialState(nil, nil, table, 10, nil), rounds: []round{ // 5 out of 8 of the nodes returned by ReadRandomNodes are dialed. { @@ -332,7 +420,7 @@ func TestDialStateNetRestrict(t *testing.T) { restrict.Add("127.0.2.0/24") runDialTest(t, dialtest{ - init: newDialState(nil, table, 10, restrict), + init: newDialState(nil, nil, table, 10, restrict), rounds: []round{ { new: []task{ @@ -355,7 +443,7 @@ func TestDialStateStaticDial(t *testing.T) { } runDialTest(t, dialtest{ - init: newDialState(wantStatic, fakeTable{}, 0, nil), + init: newDialState(wantStatic, nil, fakeTable{}, 0, nil), rounds: []round{ // Static dials are launched for the nodes that // aren't yet connected. @@ -436,7 +524,7 @@ func TestDialStateCache(t *testing.T) { } runDialTest(t, dialtest{ - init: newDialState(wantStatic, fakeTable{}, 0, nil), + init: newDialState(wantStatic, nil, fakeTable{}, 0, nil), rounds: []round{ // Static dials are launched for the nodes that // aren't yet connected. @@ -498,7 +586,7 @@ func TestDialStateCache(t *testing.T) { func TestDialResolve(t *testing.T) { resolved := discover.NewNode(uintID(1), net.IP{127, 0, 55, 234}, 3333, 4444) table := &resolveMock{answer: resolved} - state := newDialState(nil, table, 0, nil) + state := newDialState(nil, nil, table, 0, nil) // Check that the task is generated with an incomplete ID. dest := discover.NewNode(uintID(1), nil, 0, 0) diff --git a/p2p/server.go b/p2p/server.go index 48b4e8be3..b2b8c9762 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -396,7 +396,7 @@ func (srv *Server) Start() (err error) { if !srv.Discovery { dynPeers = 0 } - dialer := newDialState(srv.StaticNodes, srv.ntab, dynPeers, srv.NetRestrict) + dialer := newDialState(srv.StaticNodes, srv.BootstrapNodes, srv.ntab, dynPeers, srv.NetRestrict) // handshake srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: discover.PubkeyID(&srv.PrivateKey.PublicKey)} -- cgit v1.2.3