aboutsummaryrefslogtreecommitdiffstats
path: root/p2p/discover
diff options
context:
space:
mode:
Diffstat (limited to 'p2p/discover')
-rw-r--r--p2p/discover/database_test.go16
-rw-r--r--p2p/discover/node.go92
-rw-r--r--p2p/discover/node_test.go88
-rw-r--r--p2p/discover/table.go131
-rw-r--r--p2p/discover/table_test.go6
-rw-r--r--p2p/discover/udp.go12
-rw-r--r--p2p/discover/udp_test.go5
7 files changed, 251 insertions, 99 deletions
diff --git a/p2p/discover/database_test.go b/p2p/discover/database_test.go
index 80c1a6ff2..5a729f02b 100644
--- a/p2p/discover/database_test.go
+++ b/p2p/discover/database_test.go
@@ -102,7 +102,7 @@ func TestNodeDBInt64(t *testing.T) {
}
func TestNodeDBFetchStore(t *testing.T) {
- node := newNode(
+ node := NewNode(
MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{192, 168, 0, 1},
30303,
@@ -165,7 +165,7 @@ var nodeDBSeedQueryNodes = []struct {
// This one should not be in the result set because its last
// pong time is too far in the past.
{
- node: newNode(
+ node: NewNode(
MustHexID("0x84d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 3},
30303,
@@ -176,7 +176,7 @@ var nodeDBSeedQueryNodes = []struct {
// This one shouldn't be in in the result set because its
// nodeID is the local node's ID.
{
- node: newNode(
+ node: NewNode(
MustHexID("0x57d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 3},
30303,
@@ -187,7 +187,7 @@ var nodeDBSeedQueryNodes = []struct {
// These should be in the result set.
{
- node: newNode(
+ node: NewNode(
MustHexID("0x22d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 1},
30303,
@@ -196,7 +196,7 @@ var nodeDBSeedQueryNodes = []struct {
pong: time.Now().Add(-2 * time.Second),
},
{
- node: newNode(
+ node: NewNode(
MustHexID("0x44d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 2},
30303,
@@ -205,7 +205,7 @@ var nodeDBSeedQueryNodes = []struct {
pong: time.Now().Add(-3 * time.Second),
},
{
- node: newNode(
+ node: NewNode(
MustHexID("0xe2d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 3},
30303,
@@ -303,7 +303,7 @@ var nodeDBExpirationNodes = []struct {
exp bool
}{
{
- node: newNode(
+ node: NewNode(
MustHexID("0x01d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 1},
30303,
@@ -312,7 +312,7 @@ var nodeDBExpirationNodes = []struct {
pong: time.Now().Add(-nodeDBNodeExpiration + time.Minute),
exp: false,
}, {
- node: newNode(
+ node: NewNode(
MustHexID("0x02d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 2},
30303,
diff --git a/p2p/discover/node.go b/p2p/discover/node.go
index dd19df3a2..c4a3b5011 100644
--- a/p2p/discover/node.go
+++ b/p2p/discover/node.go
@@ -26,6 +26,7 @@ import (
"math/rand"
"net"
"net/url"
+ "regexp"
"strconv"
"strings"
@@ -37,6 +38,7 @@ import (
const nodeIDBits = 512
// Node represents a host on the network.
+// The fields of Node may not be modified.
type Node struct {
IP net.IP // len 4 for IPv4 or 16 for IPv6
UDP, TCP uint16 // port numbers
@@ -54,7 +56,9 @@ type Node struct {
contested bool
}
-func newNode(id NodeID, ip net.IP, udpPort, tcpPort uint16) *Node {
+// NewNode creates a new node. It is mostly meant to be used for
+// testing purposes.
+func NewNode(id NodeID, ip net.IP, udpPort, tcpPort uint16) *Node {
if ipv4 := ip.To4(); ipv4 != nil {
ip = ipv4
}
@@ -71,31 +75,65 @@ func (n *Node) addr() *net.UDPAddr {
return &net.UDPAddr{IP: n.IP, Port: int(n.UDP)}
}
+// Incomplete returns true for nodes with no IP address.
+func (n *Node) Incomplete() bool {
+ return n.IP == nil
+}
+
+// checks whether n is a valid complete node.
+func (n *Node) validateComplete() error {
+ if n.Incomplete() {
+ return errors.New("incomplete node")
+ }
+ if n.UDP == 0 {
+ return errors.New("missing UDP port")
+ }
+ if n.TCP == 0 {
+ return errors.New("missing TCP port")
+ }
+ if n.IP.IsMulticast() || n.IP.IsUnspecified() {
+ return errors.New("invalid IP (multicast/unspecified)")
+ }
+ _, err := n.ID.Pubkey() // validate the key (on curve, etc.)
+ return err
+}
+
// The string representation of a Node is a URL.
// Please see ParseNode for a description of the format.
func (n *Node) String() string {
- addr := net.TCPAddr{IP: n.IP, Port: int(n.TCP)}
- u := url.URL{
- Scheme: "enode",
- User: url.User(fmt.Sprintf("%x", n.ID[:])),
- Host: addr.String(),
- }
- if n.UDP != n.TCP {
- u.RawQuery = "discport=" + strconv.Itoa(int(n.UDP))
+ u := url.URL{Scheme: "enode"}
+ if n.Incomplete() {
+ u.Host = fmt.Sprintf("%x", n.ID[:])
+ } else {
+ addr := net.TCPAddr{IP: n.IP, Port: int(n.TCP)}
+ u.User = url.User(fmt.Sprintf("%x", n.ID[:]))
+ u.Host = addr.String()
+ if n.UDP != n.TCP {
+ u.RawQuery = "discport=" + strconv.Itoa(int(n.UDP))
+ }
}
return u.String()
}
-// ParseNode parses a node URL.
+var incompleteNodeURL = regexp.MustCompile("(?i)^(?:enode://)?([0-9a-f]+)$")
+
+// ParseNode parses a node designator.
+//
+// There are two basic forms of node designators
+// - incomplete nodes, which only have the public key (node ID)
+// - complete nodes, which contain the public key and IP/Port information
+//
+// For incomplete nodes, the designator must look like one of these
//
-// A node URL has scheme "enode".
+// enode://<hex node id>
+// <hex node id>
//
-// The hexadecimal node ID is encoded in the username portion of the
-// URL, separated from the host by an @ sign. The hostname can only be
-// given as an IP address, DNS domain names are not allowed. The port
-// in the host name section is the TCP listening port. If the TCP and
-// UDP (discovery) ports differ, the UDP port is specified as query
-// parameter "discport".
+// For complete nodes, the node ID is encoded in the username portion
+// of the URL, separated from the host by an @ sign. The hostname can
+// only be given as an IP address, DNS domain names are not allowed.
+// The port in the host name section is the TCP listening port. If the
+// TCP and UDP (discovery) ports differ, the UDP port is specified as
+// query parameter "discport".
//
// In the following example, the node URL describes
// a node with IP address 10.3.58.6, TCP listening port 30303
@@ -103,12 +141,26 @@ func (n *Node) String() string {
//
// enode://<hex node id>@10.3.58.6:30303?discport=30301
func ParseNode(rawurl string) (*Node, error) {
+ if m := incompleteNodeURL.FindStringSubmatch(rawurl); m != nil {
+ id, err := HexID(m[1])
+ if err != nil {
+ return nil, fmt.Errorf("invalid node ID (%v)", err)
+ }
+ return NewNode(id, nil, 0, 0), nil
+ }
+ return parseComplete(rawurl)
+}
+
+func parseComplete(rawurl string) (*Node, error) {
var (
id NodeID
ip net.IP
tcpPort, udpPort uint64
)
u, err := url.Parse(rawurl)
+ if err != nil {
+ return nil, err
+ }
if u.Scheme != "enode" {
return nil, errors.New("invalid URL scheme, want \"enode\"")
}
@@ -143,7 +195,7 @@ func ParseNode(rawurl string) (*Node, error) {
return nil, errors.New("invalid discport in query")
}
}
- return newNode(id, ip, uint16(udpPort), uint16(tcpPort)), nil
+ return NewNode(id, ip, uint16(udpPort), uint16(tcpPort)), nil
}
// MustParseNode parses a node URL. It panics if the URL is not valid.
@@ -180,7 +232,7 @@ func HexID(in string) (NodeID, error) {
if err != nil {
return id, err
} else if len(b) != len(id) {
- return id, fmt.Errorf("wrong length, need %d hex bytes", len(id))
+ return id, fmt.Errorf("wrong length, want %d hex chars", len(id)*2)
}
copy(id[:], b)
return id, nil
@@ -215,7 +267,7 @@ func (id NodeID) Pubkey() (*ecdsa.PublicKey, error) {
p.X.SetBytes(id[:half])
p.Y.SetBytes(id[half:])
if !p.Curve.IsOnCurve(p.X, p.Y) {
- return nil, errors.New("not a point on the S256 curve")
+ return nil, errors.New("id is invalid secp256k1 curve point")
}
return p, nil
}
diff --git a/p2p/discover/node_test.go b/p2p/discover/node_test.go
index e523e12d2..3d1662d0b 100644
--- a/p2p/discover/node_test.go
+++ b/p2p/discover/node_test.go
@@ -17,10 +17,12 @@
package discover
import (
+ "fmt"
"math/big"
"math/rand"
"net"
"reflect"
+ "strings"
"testing"
"testing/quick"
"time"
@@ -29,6 +31,27 @@ import (
"github.com/ethereum/go-ethereum/crypto"
)
+func ExampleNewNode() {
+ id := MustHexID("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439")
+
+ // Complete nodes contain UDP and TCP endpoints:
+ n1 := NewNode(id, net.ParseIP("2001:db8:3c4d:15::abcd:ef12"), 52150, 30303)
+ fmt.Println("n1:", n1)
+ fmt.Println("n1.Incomplete() ->", n1.Incomplete())
+
+ // An incomplete node can be created by passing zero values
+ // for all parameters except id.
+ n2 := NewNode(id, nil, 0, 0)
+ fmt.Println("n2:", n2)
+ fmt.Println("n2.Incomplete() ->", n2.Incomplete())
+
+ // Output:
+ // n1: enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:30303?discport=52150
+ // n1.Incomplete() -> false
+ // n2: enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439
+ // n2.Incomplete() -> true
+}
+
var parseNodeTests = []struct {
rawurl string
wantError string
@@ -39,13 +62,10 @@ var parseNodeTests = []struct {
wantError: `invalid URL scheme, want "enode"`,
},
{
- rawurl: "enode://foobar",
- wantError: `does not contain node ID`,
- },
- {
rawurl: "enode://01010101@123.124.125.126:3",
- wantError: `invalid node ID (wrong length, need 64 hex bytes)`,
+ wantError: `invalid node ID (wrong length, want 128 hex chars)`,
},
+ // Complete nodes with IP address.
{
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3",
wantError: `invalid IP address`,
@@ -60,7 +80,7 @@ var parseNodeTests = []struct {
},
{
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150",
- wantResult: newNode(
+ wantResult: NewNode(
MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{0x7f, 0x0, 0x0, 0x1},
52150,
@@ -69,7 +89,7 @@ var parseNodeTests = []struct {
},
{
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150",
- wantResult: newNode(
+ wantResult: NewNode(
MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.ParseIP("::"),
52150,
@@ -78,7 +98,7 @@ var parseNodeTests = []struct {
},
{
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150",
- wantResult: newNode(
+ wantResult: NewNode(
MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.ParseIP("2001:db8:3c4d:15::abcd:ef12"),
52150,
@@ -87,33 +107,62 @@ var parseNodeTests = []struct {
},
{
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334",
- wantResult: newNode(
+ wantResult: NewNode(
MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{0x7f, 0x0, 0x0, 0x1},
22334,
52150,
),
},
+ // Incomplete nodes with no address.
+ {
+ rawurl: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
+ wantResult: NewNode(
+ MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ nil, 0, 0,
+ ),
+ },
+ {
+ rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
+ wantResult: NewNode(
+ MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ nil, 0, 0,
+ ),
+ },
+ // Invalid URLs
+ {
+ rawurl: "01010101",
+ wantError: `invalid node ID (wrong length, want 128 hex chars)`,
+ },
+ {
+ rawurl: "enode://01010101",
+ wantError: `invalid node ID (wrong length, want 128 hex chars)`,
+ },
+ {
+ // This test checks that errors from url.Parse are handled.
+ rawurl: "://foo",
+ wantError: `parse ://foo: missing protocol scheme`,
+ },
}
func TestParseNode(t *testing.T) {
- for i, test := range parseNodeTests {
+ for _, test := range parseNodeTests {
n, err := ParseNode(test.rawurl)
if test.wantError != "" {
if err == nil {
- t.Errorf("test %d: got nil error, expected %#q", i, test.wantError)
+ t.Errorf("test %q:\n got nil error, expected %#q", test.rawurl, test.wantError)
continue
} else if err.Error() != test.wantError {
- t.Errorf("test %d: got error %#q, expected %#q", i, err.Error(), test.wantError)
+ t.Errorf("test %q:\n got error %#q, expected %#q", test.rawurl, err.Error(), test.wantError)
continue
}
} else {
if err != nil {
- t.Errorf("test %d: unexpected error: %v", i, err)
+ t.Errorf("test %q:\n unexpected error: %v", test.rawurl, err)
continue
}
if !reflect.DeepEqual(n, test.wantResult) {
- t.Errorf("test %d: result mismatch:\ngot: %#v, want: %#v", i, n, test.wantResult)
+ t.Errorf("test %q:\n result mismatch:\ngot: %#v, want: %#v", test.rawurl, n, test.wantResult)
}
}
}
@@ -121,12 +170,11 @@ func TestParseNode(t *testing.T) {
func TestNodeString(t *testing.T) {
for i, test := range parseNodeTests {
- if test.wantError != "" {
- continue
- }
- str := test.wantResult.String()
- if str != test.rawurl {
- t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl)
+ if test.wantError == "" && strings.HasPrefix(test.rawurl, "enode://") {
+ str := test.wantResult.String()
+ if str != test.rawurl {
+ t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl)
+ }
}
}
}
diff --git a/p2p/discover/table.go b/p2p/discover/table.go
index 298ba3fa6..abb7980f8 100644
--- a/p2p/discover/table.go
+++ b/p2p/discover/table.go
@@ -25,6 +25,7 @@ package discover
import (
"crypto/rand"
"encoding/binary"
+ "fmt"
"net"
"sort"
"sync"
@@ -56,7 +57,7 @@ type Table struct {
nursery []*Node // bootstrap nodes
db *nodeDB // database of known nodes
- refreshReq chan struct{}
+ refreshReq chan chan struct{}
closeReq chan struct{}
closed chan struct{}
@@ -99,10 +100,10 @@ func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, nodeDBPath string
tab := &Table{
net: t,
db: db,
- self: newNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)),
+ self: NewNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)),
bonding: make(map[NodeID]*bondproc),
bondslots: make(chan struct{}, maxBondingPingPongs),
- refreshReq: make(chan struct{}),
+ refreshReq: make(chan chan struct{}),
closeReq: make(chan struct{}),
closed: make(chan struct{}),
}
@@ -179,21 +180,49 @@ func (tab *Table) Close() {
}
}
-// Bootstrap sets the bootstrap nodes. These nodes are used to connect
-// to the network if the table is empty. Bootstrap will also attempt to
-// fill the table by performing random lookup operations on the
-// network.
-func (tab *Table) Bootstrap(nodes []*Node) {
+// SetFallbackNodes sets the initial points of contact. These nodes
+// are used to connect to the network if the table is empty and there
+// are no known nodes in the database.
+func (tab *Table) SetFallbackNodes(nodes []*Node) error {
+ for _, n := range nodes {
+ if err := n.validateComplete(); err != nil {
+ return fmt.Errorf("bad bootstrap/fallback node %q (%v)", n, err)
+ }
+ }
tab.mutex.Lock()
- // TODO: maybe filter nodes with bad fields (nil, etc.) to avoid strange crashes
tab.nursery = make([]*Node, 0, len(nodes))
for _, n := range nodes {
cpy := *n
+ // Recompute cpy.sha because the node might not have been
+ // created by NewNode or ParseNode.
cpy.sha = crypto.Sha3Hash(n.ID[:])
tab.nursery = append(tab.nursery, &cpy)
}
tab.mutex.Unlock()
- tab.requestRefresh()
+ tab.refresh()
+ return nil
+}
+
+// Resolve searches for a specific node with the given ID.
+// It returns nil if the node could not be found.
+func (tab *Table) Resolve(targetID NodeID) *Node {
+ // If the node is present in the local table, no
+ // network interaction is required.
+ hash := crypto.Sha3Hash(targetID[:])
+ tab.mutex.Lock()
+ cl := tab.closest(hash, 1)
+ tab.mutex.Unlock()
+ if len(cl.entries) > 0 && cl.entries[0].ID == targetID {
+ return cl.entries[0]
+ }
+ // Otherwise, do a network lookup.
+ result := tab.Lookup(targetID)
+ for _, n := range result {
+ if n.ID == targetID {
+ return n
+ }
+ }
+ return nil
}
// Lookup performs a network search for nodes close
@@ -202,26 +231,36 @@ func (tab *Table) Bootstrap(nodes []*Node) {
// The given target does not need to be an actual node
// identifier.
func (tab *Table) Lookup(targetID NodeID) []*Node {
+ return tab.lookup(targetID, true)
+}
+
+func (tab *Table) lookup(targetID NodeID, refreshIfEmpty bool) []*Node {
var (
target = crypto.Sha3Hash(targetID[:])
asked = make(map[NodeID]bool)
seen = make(map[NodeID]bool)
reply = make(chan []*Node, alpha)
pendingQueries = 0
+ result *nodesByDistance
)
// don't query further if we hit ourself.
// unlikely to happen often in practice.
asked[tab.self.ID] = true
- tab.mutex.Lock()
- // generate initial result set
- result := tab.closest(target, bucketSize)
- tab.mutex.Unlock()
-
- // If the result set is empty, all nodes were dropped, refresh.
- if len(result.entries) == 0 {
- tab.requestRefresh()
- return nil
+ for {
+ tab.mutex.Lock()
+ // generate initial result set
+ result = tab.closest(target, bucketSize)
+ tab.mutex.Unlock()
+ if len(result.entries) > 0 || !refreshIfEmpty {
+ break
+ }
+ // The result set is empty, all nodes were dropped, refresh.
+ // We actually wait for the refresh to complete here. The very
+ // first query will hit this case and run the bootstrapping
+ // logic.
+ <-tab.refresh()
+ refreshIfEmpty = false
}
for {
@@ -265,24 +304,24 @@ func (tab *Table) Lookup(targetID NodeID) []*Node {
return result.entries
}
-func (tab *Table) requestRefresh() {
+func (tab *Table) refresh() <-chan struct{} {
+ done := make(chan struct{})
select {
- case tab.refreshReq <- struct{}{}:
+ case tab.refreshReq <- done:
case <-tab.closed:
+ close(done)
}
+ return done
}
+// refreshLoop schedules doRefresh runs and coordinates shutdown.
func (tab *Table) refreshLoop() {
- defer func() {
- tab.db.close()
- if tab.net != nil {
- tab.net.close()
- }
- close(tab.closed)
- }()
-
- timer := time.NewTicker(autoRefreshInterval)
- var done chan struct{}
+ var (
+ timer = time.NewTicker(autoRefreshInterval)
+ waiting []chan struct{} // accumulates waiting callers while doRefresh runs
+ done chan struct{} // where doRefresh reports completion
+ )
+loop:
for {
select {
case <-timer.C:
@@ -290,20 +329,34 @@ func (tab *Table) refreshLoop() {
done = make(chan struct{})
go tab.doRefresh(done)
}
- case <-tab.refreshReq:
+ case req := <-tab.refreshReq:
+ waiting = append(waiting, req)
if done == nil {
done = make(chan struct{})
go tab.doRefresh(done)
}
case <-done:
+ for _, ch := range waiting {
+ close(ch)
+ }
+ waiting = nil
done = nil
case <-tab.closeReq:
- if done != nil {
- <-done
- }
- return
+ break loop
}
}
+
+ if tab.net != nil {
+ tab.net.close()
+ }
+ if done != nil {
+ <-done
+ }
+ for _, ch := range waiting {
+ close(ch)
+ }
+ tab.db.close()
+ close(tab.closed)
}
// doRefresh performs a lookup for a random target to keep buckets
@@ -320,7 +373,7 @@ func (tab *Table) doRefresh(done chan struct{}) {
// We perform a lookup with a random target instead.
var target NodeID
rand.Read(target[:])
- result := tab.Lookup(target)
+ result := tab.lookup(target, false)
if len(result) > 0 {
return
}
@@ -344,7 +397,7 @@ func (tab *Table) doRefresh(done chan struct{}) {
tab.mutex.Unlock()
// Finally, do a self lookup to fill up the buckets.
- tab.Lookup(tab.self.ID)
+ tab.lookup(tab.self.ID, false)
}
// closest returns the n nodes in the table that are closest to the
@@ -466,7 +519,7 @@ func (tab *Table) pingpong(w *bondproc, pinged bool, id NodeID, addr *net.UDPAdd
tab.net.waitping(id)
}
// Bonding succeeded, update the node database.
- w.n = newNode(id, addr.IP, uint16(addr.Port), tcpPort)
+ w.n = NewNode(id, addr.IP, uint16(addr.Port), tcpPort)
tab.db.updateNode(w.n)
close(w.done)
}
diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go
index 13effaed6..30a418f44 100644
--- a/p2p/discover/table_test.go
+++ b/p2p/discover/table_test.go
@@ -36,7 +36,7 @@ func TestTable_pingReplace(t *testing.T) {
transport := newPingRecorder()
tab, _ := newTable(transport, NodeID{}, &net.UDPAddr{}, "")
defer tab.Close()
- pingSender := newNode(MustHexID("a502af0f59b2aab7746995408c79e9ca312d2793cc997e44fc55eda62f0150bbb8c59a6f9269ba3a081518b62699ee807c7c19c20125ddfccca872608af9e370"), net.IP{}, 99, 99)
+ pingSender := NewNode(MustHexID("a502af0f59b2aab7746995408c79e9ca312d2793cc997e44fc55eda62f0150bbb8c59a6f9269ba3a081518b62699ee807c7c19c20125ddfccca872608af9e370"), net.IP{}, 99, 99)
// fill up the sender's bucket.
last := fillBucket(tab, 253)
@@ -287,7 +287,7 @@ func TestTable_Lookup(t *testing.T) {
t.Fatalf("lookup on empty table returned %d results: %#v", len(results), results)
}
// seed table with initial node (otherwise lookup will terminate immediately)
- seed := newNode(lookupTestnet.dists[256][0], net.IP{}, 256, 0)
+ seed := NewNode(lookupTestnet.dists[256][0], net.IP{}, 256, 0)
tab.stuff([]*Node{seed})
results := tab.Lookup(lookupTestnet.target)
@@ -517,7 +517,7 @@ func (tn *preminedTestnet) findnode(toid NodeID, toaddr *net.UDPAddr, target Nod
next := uint16(toaddr.Port) - 1
var result []*Node
for i, id := range tn.dists[toaddr.Port] {
- result = append(result, newNode(id, net.ParseIP("127.0.0.1"), next, uint16(i)))
+ result = append(result, NewNode(id, net.ParseIP("127.0.0.1"), next, uint16(i)))
}
return result, nil
}
diff --git a/p2p/discover/udp.go b/p2p/discover/udp.go
index fc7fa737c..72b2a45e5 100644
--- a/p2p/discover/udp.go
+++ b/p2p/discover/udp.go
@@ -114,13 +114,11 @@ func makeEndpoint(addr *net.UDPAddr, tcpPort uint16) rpcEndpoint {
return rpcEndpoint{IP: ip, UDP: uint16(addr.Port), TCP: tcpPort}
}
-func nodeFromRPC(rn rpcNode) (n *Node, valid bool) {
+func nodeFromRPC(rn rpcNode) (*Node, error) {
// TODO: don't accept localhost, LAN addresses from internet hosts
- // TODO: check public key is on secp256k1 curve
- if rn.IP.IsMulticast() || rn.IP.IsUnspecified() || rn.UDP == 0 {
- return nil, false
- }
- return newNode(rn.ID, rn.IP, rn.UDP, rn.TCP), true
+ n := NewNode(rn.ID, rn.IP, rn.UDP, rn.TCP)
+ err := n.validateComplete()
+ return n, err
}
func nodeToRPC(n *Node) rpcNode {
@@ -271,7 +269,7 @@ func (t *udp) findnode(toid NodeID, toaddr *net.UDPAddr, target NodeID) ([]*Node
reply := r.(*neighbors)
for _, rn := range reply.Nodes {
nreceived++
- if n, valid := nodeFromRPC(rn); valid {
+ if n, err := nodeFromRPC(rn); err == nil {
nodes = append(nodes, n)
}
}
diff --git a/p2p/discover/udp_test.go b/p2p/discover/udp_test.go
index 944e73d6e..8ed12b8ec 100644
--- a/p2p/discover/udp_test.go
+++ b/p2p/discover/udp_test.go
@@ -167,16 +167,17 @@ func TestUDP_responseTimeouts(t *testing.T) {
binary.BigEndian.PutUint64(p.from[:], uint64(i))
if p.ptype <= 128 {
p.errc = timeoutErr
+ test.udp.addpending <- p
nTimeouts++
} else {
p.errc = nilErr
+ test.udp.addpending <- p
time.AfterFunc(randomDuration(60*time.Millisecond), func() {
if !test.udp.handleReply(p.from, p.ptype, nil) {
t.Logf("not matched: %v", p)
}
})
}
- test.udp.addpending <- p
time.Sleep(randomDuration(30 * time.Millisecond))
}
@@ -243,7 +244,7 @@ func TestUDP_findnode(t *testing.T) {
// ensure there's a bond with the test node,
// findnode won't be accepted otherwise.
- test.table.db.updateNode(newNode(
+ test.table.db.updateNode(NewNode(
PubkeyID(&test.remotekey.PublicKey),
test.remoteaddr.IP,
uint16(test.remoteaddr.Port),