Skip to content

Commit 35908b1

Browse files
Or Neemanfjltimcooijmans
authored
Oneeman/cherry pick discovery bootnode fix (ethereum#1194)
* p2p/discover: add initial discovery v5 implementation (ethereum#20750) celo-blockchain cherry-pick conflicts: cmd/devp2p/discv4cmd.go p2p/discover/common.go p2p/discover/v4_udp.go This adds an implementation of the current discovery v5 spec. There is full integration with cmd/devp2p and enode.Iterator in this version. In theory we could enable the new protocol as a replacement of discovery v4 at any time. In practice, there will likely be a few more changes to the spec and implementation before this can happen. * p2p/discover: move discv4 encoding to new 'v4wire' package (ethereum#21147) celo-blockchain cherry-pick conflicts: p2p/discover/v4_udp.go p2p/discover/v4_udp_test.go This moves all v4 protocol definitions to a new package, p2p/discover/v4wire. The new package will be used for low-level protocol tests. * p2p/discover: avoid dropping unverified nodes when table is almost empty (ethereum#21396) This change improves discovery behavior in small networks. Very small networks would often fail to bootstrap because all member nodes were dropping table content due to findnode failure. The check is now changed to avoid dropping nodes on findnode failure when their bucket is almost empty. It also relaxes the liveness check requirement for FINDNODE/v4 response nodes, returning unverified nodes as results when there aren't any verified nodes yet. The "findnode failed" log now reports whether the node was dropped instead of the number of results. The value of the "results" was always zero by definition. Co-authored-by: Felix Lange <[email protected]> * Don't remove the last node from a discovery table bucket If a node initially has trouble reaching the bootnode, the bootnode was being removed in doRevalidate(), leaving the table empty. * Take care of lint errors Co-authored-by: Felix Lange <[email protected]> Co-authored-by: timcooijmans <[email protected]>
1 parent b3f2314 commit 35908b1

22 files changed

+3770
-612
lines changed

cmd/devp2p/crawl.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@ import (
2020
"time"
2121

2222
"github.com/ethereum/go-ethereum/log"
23-
"github.com/ethereum/go-ethereum/p2p/discover"
2423
"github.com/ethereum/go-ethereum/p2p/enode"
2524
)
2625

2726
type crawler struct {
2827
input nodeSet
2928
output nodeSet
30-
disc *discover.UDPv4
29+
disc resolver
3130
iters []enode.Iterator
3231
inputIter enode.Iterator
3332
ch chan *enode.Node
@@ -37,7 +36,11 @@ type crawler struct {
3736
revalidateInterval time.Duration
3837
}
3938

40-
func newCrawler(input nodeSet, disc *discover.UDPv4, iters ...enode.Iterator) *crawler {
39+
type resolver interface {
40+
RequestENR(*enode.Node) (*enode.Node, error)
41+
}
42+
43+
func newCrawler(input nodeSet, disc resolver, iters ...enode.Iterator) *crawler {
4144
c := &crawler{
4245
input: input,
4346
output: make(nodeSet, len(input)),

cmd/devp2p/discv4cmd.go

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ var (
8484
Name: "bootnodes",
8585
Usage: "Comma separated nodes used for bootstrapping",
8686
}
87+
nodekeyFlag = cli.StringFlag{
88+
Name: "nodekey",
89+
Usage: "Hex-encoded node key",
90+
}
91+
nodedbFlag = cli.StringFlag{
92+
Name: "nodedb",
93+
Usage: "Nodes database location",
94+
}
95+
listenAddrFlag = cli.StringFlag{
96+
Name: "addr",
97+
Usage: "Listening address",
98+
}
8799
crawlTimeoutFlag = cli.DurationFlag{
88100
Name: "timeout",
89101
Usage: "Time limit for the crawl.",
@@ -181,56 +193,75 @@ func discv4Crawl(ctx *cli.Context) error {
181193
return nil
182194
}
183195

184-
func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
185-
s := utils.GetBootstrapNodes(ctx)
186-
if ctx.IsSet(bootnodesFlag.Name) {
187-
s = strings.Split(ctx.String(bootnodesFlag.Name), ",")
188-
}
189-
nodes := make([]*enode.Node, len(s))
190-
var err error
191-
for i, record := range s {
192-
nodes[i], err = parseNode(record)
193-
if err != nil {
194-
return nil, fmt.Errorf("invalid bootstrap node: %v", err)
195-
}
196-
}
197-
return nodes, nil
198-
}
199-
200196
// startV4 starts an ephemeral discovery V4 node.
201197
func startV4(ctx *cli.Context) *discover.UDPv4 {
202198
networkId := ctx.GlobalUint64(networkIdFlag.Name)
203-
socket, ln, cfg, err := listen(networkId)
199+
ln, config := makeDiscoveryConfig(ctx, networkId)
200+
socket := listen(ln, ctx.String(listenAddrFlag.Name))
201+
disc, err := discover.ListenV4(socket, ln, config)
204202
if err != nil {
205203
exit(err)
206204
}
205+
return disc
206+
}
207+
208+
func makeDiscoveryConfig(ctx *cli.Context, networkId uint64) (*enode.LocalNode, discover.Config) {
209+
var cfg discover.Config
210+
211+
if ctx.IsSet(nodekeyFlag.Name) {
212+
key, err := crypto.HexToECDSA(ctx.String(nodekeyFlag.Name))
213+
if err != nil {
214+
exit(fmt.Errorf("-%s: %v", nodekeyFlag.Name, err))
215+
}
216+
cfg.PrivateKey = key
217+
} else {
218+
cfg.PrivateKey, _ = crypto.GenerateKey()
219+
}
220+
207221
if commandHasFlag(ctx, bootnodesFlag) {
208222
bn, err := parseBootnodes(ctx)
209223
if err != nil {
210224
exit(err)
211225
}
212226
cfg.Bootnodes = bn
213227
}
214-
disc, err := discover.ListenV4(socket, ln, cfg)
228+
229+
dbpath := ctx.String(nodedbFlag.Name)
230+
db, err := enode.OpenDB(dbpath)
215231
if err != nil {
216232
exit(err)
217233
}
218-
return disc
219-
}
220-
221-
func listen(networkId uint64) (*net.UDPConn, *enode.LocalNode, discover.Config, error) {
222-
var cfg discover.Config
223-
cfg.PrivateKey, _ = crypto.GenerateKey()
224-
db, _ := enode.OpenDB("")
225234
ln := enode.NewLocalNode(db, cfg.PrivateKey, networkId)
235+
return ln, cfg
236+
}
226237

227-
socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{0, 0, 0, 0}})
238+
func listen(ln *enode.LocalNode, addr string) *net.UDPConn {
239+
if addr == "" {
240+
addr = "0.0.0.0:0"
241+
}
242+
socket, err := net.ListenPacket("udp4", addr)
228243
if err != nil {
229-
db.Close()
230-
return nil, nil, cfg, err
244+
exit(err)
231245
}
232-
addr := socket.LocalAddr().(*net.UDPAddr)
246+
usocket := socket.(*net.UDPConn)
247+
uaddr := socket.LocalAddr().(*net.UDPAddr)
233248
ln.SetFallbackIP(net.IP{127, 0, 0, 1})
234-
ln.SetFallbackUDP(addr.Port)
235-
return socket, ln, cfg, nil
249+
ln.SetFallbackUDP(uaddr.Port)
250+
return usocket
251+
}
252+
253+
func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
254+
s := utils.GetBootstrapNodes(ctx)
255+
if ctx.IsSet(bootnodesFlag.Name) {
256+
s = strings.Split(ctx.String(bootnodesFlag.Name), ",")
257+
}
258+
nodes := make([]*enode.Node, len(s))
259+
var err error
260+
for i, record := range s {
261+
nodes[i], err = parseNode(record)
262+
if err != nil {
263+
return nil, fmt.Errorf("invalid bootstrap node: %v", err)
264+
}
265+
}
266+
return nodes, nil
236267
}

cmd/devp2p/discv5cmd.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2019 The go-ethereum Authors
2+
// This file is part of go-ethereum.
3+
//
4+
// go-ethereum is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// go-ethereum is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"time"
22+
23+
"github.com/ethereum/go-ethereum/common"
24+
"github.com/ethereum/go-ethereum/p2p/discover"
25+
"gopkg.in/urfave/cli.v1"
26+
)
27+
28+
var (
29+
discv5Command = cli.Command{
30+
Name: "discv5",
31+
Usage: "Node Discovery v5 tools",
32+
Subcommands: []cli.Command{
33+
discv5PingCommand,
34+
discv5ResolveCommand,
35+
discv5CrawlCommand,
36+
discv5ListenCommand,
37+
},
38+
}
39+
discv5PingCommand = cli.Command{
40+
Name: "ping",
41+
Usage: "Sends ping to a node",
42+
Action: discv5Ping,
43+
}
44+
discv5ResolveCommand = cli.Command{
45+
Name: "resolve",
46+
Usage: "Finds a node in the DHT",
47+
Action: discv5Resolve,
48+
Flags: []cli.Flag{bootnodesFlag},
49+
}
50+
discv5CrawlCommand = cli.Command{
51+
Name: "crawl",
52+
Usage: "Updates a nodes.json file with random nodes found in the DHT",
53+
Action: discv5Crawl,
54+
Flags: []cli.Flag{bootnodesFlag, crawlTimeoutFlag},
55+
}
56+
discv5ListenCommand = cli.Command{
57+
Name: "listen",
58+
Usage: "Runs a node",
59+
Action: discv5Listen,
60+
Flags: []cli.Flag{
61+
bootnodesFlag,
62+
nodekeyFlag,
63+
nodedbFlag,
64+
listenAddrFlag,
65+
},
66+
}
67+
)
68+
69+
func discv5Ping(ctx *cli.Context) error {
70+
n := getNodeArg(ctx)
71+
disc := startV5(ctx)
72+
defer disc.Close()
73+
74+
fmt.Println(disc.Ping(n))
75+
return nil
76+
}
77+
78+
func discv5Resolve(ctx *cli.Context) error {
79+
n := getNodeArg(ctx)
80+
disc := startV5(ctx)
81+
defer disc.Close()
82+
83+
fmt.Println(disc.Resolve(n))
84+
return nil
85+
}
86+
87+
func discv5Crawl(ctx *cli.Context) error {
88+
if ctx.NArg() < 1 {
89+
return fmt.Errorf("need nodes file as argument")
90+
}
91+
nodesFile := ctx.Args().First()
92+
var inputSet nodeSet
93+
if common.FileExist(nodesFile) {
94+
inputSet = loadNodesJSON(nodesFile)
95+
}
96+
97+
disc := startV5(ctx)
98+
defer disc.Close()
99+
c := newCrawler(inputSet, disc, disc.RandomNodes())
100+
c.revalidateInterval = 10 * time.Minute
101+
output := c.run(ctx.Duration(crawlTimeoutFlag.Name))
102+
writeNodesJSON(nodesFile, output)
103+
return nil
104+
}
105+
106+
func discv5Listen(ctx *cli.Context) error {
107+
disc := startV5(ctx)
108+
defer disc.Close()
109+
110+
fmt.Println(disc.Self())
111+
select {}
112+
}
113+
114+
// startV5 starts an ephemeral discovery v5 node.
115+
func startV5(ctx *cli.Context) *discover.UDPv5 {
116+
networkId := ctx.GlobalUint64(networkIdFlag.Name)
117+
ln, config := makeDiscoveryConfig(ctx, networkId)
118+
socket := listen(ln, ctx.String(listenAddrFlag.Name))
119+
disc, err := discover.ListenV5(socket, ln, config)
120+
if err != nil {
121+
exit(err)
122+
}
123+
return disc
124+
}

cmd/devp2p/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func init() {
5959
app.Commands = []cli.Command{
6060
enrdumpCommand,
6161
discv4Command,
62+
discv5Command,
6263
dnsCommand,
6364
nodesetCommand,
6465
}

p2p/discover/common.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import (
2020
"crypto/ecdsa"
2121
"net"
2222

23+
"github.com/ethereum/go-ethereum/common/mclock"
2324
"github.com/ethereum/go-ethereum/log"
2425
"github.com/ethereum/go-ethereum/p2p/enode"
26+
"github.com/ethereum/go-ethereum/p2p/enr"
2527
"github.com/ethereum/go-ethereum/p2p/netutil"
2628
)
2729

@@ -44,6 +46,21 @@ type Config struct {
4446
Unhandled chan<- ReadPacket // unhandled packets are sent on this channel
4547
Log log.Logger // if set, log messages go here
4648
PingIPFromPacket bool
49+
ValidSchemes enr.IdentityScheme // allowed identity schemes
50+
Clock mclock.Clock
51+
}
52+
53+
func (cfg Config) withDefaults() Config {
54+
if cfg.Log == nil {
55+
cfg.Log = log.Root()
56+
}
57+
if cfg.ValidSchemes == nil {
58+
cfg.ValidSchemes = enode.ValidSchemes
59+
}
60+
if cfg.Clock == nil {
61+
cfg.Clock = mclock.System{}
62+
}
63+
return cfg
4764
}
4865

4966
// ListenUDP starts listening for discovery packets on the given UDP socket.
@@ -52,8 +69,15 @@ func ListenUDP(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) {
5269
}
5370

5471
// ReadPacket is a packet that couldn't be handled. Those packets are sent to the unhandled
55-
// channel if configured. This is exported for internal use, do not use this type.
72+
// channel if configured.
5673
type ReadPacket struct {
5774
Data []byte
5875
Addr *net.UDPAddr
5976
}
77+
78+
func min(x, y int) int {
79+
if x > y {
80+
return y
81+
}
82+
return x
83+
}

p2p/discover/lookup.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,7 @@ func (it *lookup) startQueries() bool {
104104

105105
// The first query returns nodes from the local table.
106106
if it.queries == -1 {
107-
it.tab.mutex.Lock()
108-
closest := it.tab.closest(it.result.target, bucketSize, false)
109-
it.tab.mutex.Unlock()
107+
closest := it.tab.findnodeByID(it.result.target, bucketSize, false)
110108
// Avoid finishing the lookup too quickly if table is empty. It'd be better to wait
111109
// for the table to fill in this case, but there is no good mechanism for that
112110
// yet.
@@ -150,11 +148,14 @@ func (it *lookup) query(n *node, reply chan<- []*node) {
150148
} else if len(r) == 0 {
151149
fails++
152150
it.tab.db.UpdateFindFails(n.ID(), n.IP(), fails)
153-
it.tab.log.Trace("Findnode failed", "id", n.ID(), "failcount", fails, "err", err)
154-
if fails >= maxFindnodeFailures {
155-
it.tab.log.Trace("Too many findnode failures, dropping", "id", n.ID(), "failcount", fails)
151+
// Remove the node from the local table if it fails to return anything useful too
152+
// many times, but only if there are enough other nodes in the bucket.
153+
dropped := false
154+
if fails >= maxFindnodeFailures && it.tab.bucketLen(n.ID()) >= bucketSize/2 {
155+
dropped = true
156156
it.tab.delete(n)
157157
}
158+
it.tab.log.Trace("FINDNODE failed", "id", n.ID(), "failcount", fails, "dropped", dropped, "err", err)
158159
} else if fails > 0 {
159160
// Reset failure counter because it counts _consecutive_ failures.
160161
it.tab.db.UpdateFindFails(n.ID(), n.IP(), 0)

0 commit comments

Comments
 (0)