p2p/discover: support incomplete node URLs, add Resolve

This commit is contained in:
Felix Lange 2015-10-22 23:46:01 +02:00
parent 82a024d425
commit d1f507b7f1
7 changed files with 158 additions and 54 deletions

View File

@ -102,7 +102,7 @@ func TestNodeDBInt64(t *testing.T) {
} }
func TestNodeDBFetchStore(t *testing.T) { func TestNodeDBFetchStore(t *testing.T) {
node := newNode( node := NewNode(
MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{192, 168, 0, 1}, net.IP{192, 168, 0, 1},
30303, 30303,
@ -165,7 +165,7 @@ var nodeDBSeedQueryNodes = []struct {
// This one should not be in the result set because its last // This one should not be in the result set because its last
// pong time is too far in the past. // pong time is too far in the past.
{ {
node: newNode( node: NewNode(
MustHexID("0x84d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x84d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 3}, net.IP{127, 0, 0, 3},
30303, 30303,
@ -176,7 +176,7 @@ var nodeDBSeedQueryNodes = []struct {
// This one shouldn't be in in the result set because its // This one shouldn't be in in the result set because its
// nodeID is the local node's ID. // nodeID is the local node's ID.
{ {
node: newNode( node: NewNode(
MustHexID("0x57d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x57d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 3}, net.IP{127, 0, 0, 3},
30303, 30303,
@ -187,7 +187,7 @@ var nodeDBSeedQueryNodes = []struct {
// These should be in the result set. // These should be in the result set.
{ {
node: newNode( node: NewNode(
MustHexID("0x22d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x22d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 1}, net.IP{127, 0, 0, 1},
30303, 30303,
@ -196,7 +196,7 @@ var nodeDBSeedQueryNodes = []struct {
pong: time.Now().Add(-2 * time.Second), pong: time.Now().Add(-2 * time.Second),
}, },
{ {
node: newNode( node: NewNode(
MustHexID("0x44d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x44d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 2}, net.IP{127, 0, 0, 2},
30303, 30303,
@ -205,7 +205,7 @@ var nodeDBSeedQueryNodes = []struct {
pong: time.Now().Add(-3 * time.Second), pong: time.Now().Add(-3 * time.Second),
}, },
{ {
node: newNode( node: NewNode(
MustHexID("0xe2d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0xe2d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 3}, net.IP{127, 0, 0, 3},
30303, 30303,
@ -303,7 +303,7 @@ var nodeDBExpirationNodes = []struct {
exp bool exp bool
}{ }{
{ {
node: newNode( node: NewNode(
MustHexID("0x01d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x01d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 1}, net.IP{127, 0, 0, 1},
30303, 30303,
@ -312,7 +312,7 @@ var nodeDBExpirationNodes = []struct {
pong: time.Now().Add(-nodeDBNodeExpiration + time.Minute), pong: time.Now().Add(-nodeDBNodeExpiration + time.Minute),
exp: false, exp: false,
}, { }, {
node: newNode( node: NewNode(
MustHexID("0x02d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x02d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{127, 0, 0, 2}, net.IP{127, 0, 0, 2},
30303, 30303,

View File

@ -26,6 +26,7 @@ import (
"math/rand" "math/rand"
"net" "net"
"net/url" "net/url"
"regexp"
"strconv" "strconv"
"strings" "strings"
@ -37,6 +38,7 @@ import (
const nodeIDBits = 512 const nodeIDBits = 512
// Node represents a host on the network. // Node represents a host on the network.
// The fields of Node may not be modified.
type Node struct { type Node struct {
IP net.IP // len 4 for IPv4 or 16 for IPv6 IP net.IP // len 4 for IPv4 or 16 for IPv6
UDP, TCP uint16 // port numbers UDP, TCP uint16 // port numbers
@ -54,7 +56,9 @@ type Node struct {
contested bool 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 { if ipv4 := ip.To4(); ipv4 != nil {
ip = ipv4 ip = ipv4
} }
@ -71,31 +75,47 @@ func (n *Node) addr() *net.UDPAddr {
return &net.UDPAddr{IP: n.IP, Port: int(n.UDP)} 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
}
// The string representation of a Node is a URL. // The string representation of a Node is a URL.
// Please see ParseNode for a description of the format. // Please see ParseNode for a description of the format.
func (n *Node) String() string { func (n *Node) String() string {
addr := net.TCPAddr{IP: n.IP, Port: int(n.TCP)} u := url.URL{Scheme: "enode"}
u := url.URL{ if n.Incomplete() {
Scheme: "enode", u.Host = fmt.Sprintf("%x", n.ID[:])
User: url.User(fmt.Sprintf("%x", n.ID[:])), } else {
Host: addr.String(), addr := net.TCPAddr{IP: n.IP, Port: int(n.TCP)}
} u.User = url.User(fmt.Sprintf("%x", n.ID[:]))
if n.UDP != n.TCP { u.Host = addr.String()
u.RawQuery = "discport=" + strconv.Itoa(int(n.UDP)) if n.UDP != n.TCP {
u.RawQuery = "discport=" + strconv.Itoa(int(n.UDP))
}
} }
return u.String() return u.String()
} }
// ParseNode parses a node URL. var incompleteNodeURL = regexp.MustCompile("(?i)^(?:enode://)?([0-9a-f]+)$")
// ParseNode parses a node designator.
// //
// A node URL has scheme "enode". // 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
// //
// The hexadecimal node ID is encoded in the username portion of the // For incomplete nodes, the designator must look like one of these
// 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 // enode://<hex node id>
// in the host name section is the TCP listening port. If the TCP and // <hex node id>
// 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 // In the following example, the node URL describes
// a node with IP address 10.3.58.6, TCP listening port 30303 // a node with IP address 10.3.58.6, TCP listening port 30303
@ -103,12 +123,26 @@ func (n *Node) String() string {
// //
// enode://<hex node id>@10.3.58.6:30303?discport=30301 // enode://<hex node id>@10.3.58.6:30303?discport=30301
func ParseNode(rawurl string) (*Node, error) { 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 ( var (
id NodeID id NodeID
ip net.IP ip net.IP
tcpPort, udpPort uint64 tcpPort, udpPort uint64
) )
u, err := url.Parse(rawurl) u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
if u.Scheme != "enode" { if u.Scheme != "enode" {
return nil, errors.New("invalid URL scheme, want \"enode\"") return nil, errors.New("invalid URL scheme, want \"enode\"")
} }
@ -143,7 +177,7 @@ func ParseNode(rawurl string) (*Node, error) {
return nil, errors.New("invalid discport in query") 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. // MustParseNode parses a node URL. It panics if the URL is not valid.
@ -180,7 +214,7 @@ func HexID(in string) (NodeID, error) {
if err != nil { if err != nil {
return id, err return id, err
} else if len(b) != len(id) { } 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) copy(id[:], b)
return id, nil return id, nil

View File

@ -17,10 +17,12 @@
package discover package discover
import ( import (
"fmt"
"math/big" "math/big"
"math/rand" "math/rand"
"net" "net"
"reflect" "reflect"
"strings"
"testing" "testing"
"testing/quick" "testing/quick"
"time" "time"
@ -29,6 +31,27 @@ import (
"github.com/ethereum/go-ethereum/crypto" "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 { var parseNodeTests = []struct {
rawurl string rawurl string
wantError string wantError string
@ -38,14 +61,11 @@ var parseNodeTests = []struct {
rawurl: "http://foobar", rawurl: "http://foobar",
wantError: `invalid URL scheme, want "enode"`, wantError: `invalid URL scheme, want "enode"`,
}, },
{
rawurl: "enode://foobar",
wantError: `does not contain node ID`,
},
{ {
rawurl: "enode://01010101@123.124.125.126:3", 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", rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3",
wantError: `invalid IP address`, wantError: `invalid IP address`,
@ -60,7 +80,7 @@ var parseNodeTests = []struct {
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150", rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150",
wantResult: newNode( wantResult: NewNode(
MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{0x7f, 0x0, 0x0, 0x1}, net.IP{0x7f, 0x0, 0x0, 0x1},
52150, 52150,
@ -69,7 +89,7 @@ var parseNodeTests = []struct {
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150", rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150",
wantResult: newNode( wantResult: NewNode(
MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.ParseIP("::"), net.ParseIP("::"),
52150, 52150,
@ -78,7 +98,7 @@ var parseNodeTests = []struct {
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150", rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150",
wantResult: newNode( wantResult: NewNode(
MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.ParseIP("2001:db8:3c4d:15::abcd:ef12"), net.ParseIP("2001:db8:3c4d:15::abcd:ef12"),
52150, 52150,
@ -87,33 +107,62 @@ var parseNodeTests = []struct {
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334", rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334",
wantResult: newNode( wantResult: NewNode(
MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{0x7f, 0x0, 0x0, 0x1}, net.IP{0x7f, 0x0, 0x0, 0x1},
22334, 22334,
52150, 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) { func TestParseNode(t *testing.T) {
for i, test := range parseNodeTests { for _, test := range parseNodeTests {
n, err := ParseNode(test.rawurl) n, err := ParseNode(test.rawurl)
if test.wantError != "" { if test.wantError != "" {
if err == nil { 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 continue
} else if err.Error() != test.wantError { } 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 continue
} }
} else { } else {
if err != nil { if err != nil {
t.Errorf("test %d: unexpected error: %v", i, err) t.Errorf("test %q:\n unexpected error: %v", test.rawurl, err)
continue continue
} }
if !reflect.DeepEqual(n, test.wantResult) { 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) { func TestNodeString(t *testing.T) {
for i, test := range parseNodeTests { for i, test := range parseNodeTests {
if test.wantError != "" { if test.wantError == "" && strings.HasPrefix(test.rawurl, "enode://") {
continue str := test.wantResult.String()
} if str != test.rawurl {
str := test.wantResult.String() t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl)
if str != test.rawurl { }
t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl)
} }
} }
} }

View File

@ -99,7 +99,7 @@ func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, nodeDBPath string
tab := &Table{ tab := &Table{
net: t, net: t,
db: db, 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), bonding: make(map[NodeID]*bondproc),
bondslots: make(chan struct{}, maxBondingPingPongs), bondslots: make(chan struct{}, maxBondingPingPongs),
refreshReq: make(chan struct{}), refreshReq: make(chan struct{}),
@ -196,6 +196,28 @@ func (tab *Table) Bootstrap(nodes []*Node) {
tab.requestRefresh() tab.requestRefresh()
} }
// 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 // Lookup performs a network search for nodes close
// to the given target. It approaches the target by querying // to the given target. It approaches the target by querying
// nodes that are closer to it on each iteration. // nodes that are closer to it on each iteration.
@ -466,7 +488,7 @@ func (tab *Table) pingpong(w *bondproc, pinged bool, id NodeID, addr *net.UDPAdd
tab.net.waitping(id) tab.net.waitping(id)
} }
// Bonding succeeded, update the node database. // 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) tab.db.updateNode(w.n)
close(w.done) close(w.done)
} }

View File

@ -36,7 +36,7 @@ func TestTable_pingReplace(t *testing.T) {
transport := newPingRecorder() transport := newPingRecorder()
tab, _ := newTable(transport, NodeID{}, &net.UDPAddr{}, "") tab, _ := newTable(transport, NodeID{}, &net.UDPAddr{}, "")
defer tab.Close() 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. // fill up the sender's bucket.
last := fillBucket(tab, 253) 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) t.Fatalf("lookup on empty table returned %d results: %#v", len(results), results)
} }
// seed table with initial node (otherwise lookup will terminate immediately) // 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}) tab.stuff([]*Node{seed})
results := tab.Lookup(lookupTestnet.target) 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 next := uint16(toaddr.Port) - 1
var result []*Node var result []*Node
for i, id := range tn.dists[toaddr.Port] { 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 return result, nil
} }

View File

@ -120,7 +120,7 @@ func nodeFromRPC(rn rpcNode) (n *Node, valid bool) {
if rn.IP.IsMulticast() || rn.IP.IsUnspecified() || rn.UDP == 0 { if rn.IP.IsMulticast() || rn.IP.IsUnspecified() || rn.UDP == 0 {
return nil, false return nil, false
} }
return newNode(rn.ID, rn.IP, rn.UDP, rn.TCP), true return NewNode(rn.ID, rn.IP, rn.UDP, rn.TCP), true
} }
func nodeToRPC(n *Node) rpcNode { func nodeToRPC(n *Node) rpcNode {

View File

@ -243,7 +243,7 @@ func TestUDP_findnode(t *testing.T) {
// ensure there's a bond with the test node, // ensure there's a bond with the test node,
// findnode won't be accepted otherwise. // findnode won't be accepted otherwise.
test.table.db.updateNode(newNode( test.table.db.updateNode(NewNode(
PubkeyID(&test.remotekey.PublicKey), PubkeyID(&test.remotekey.PublicKey),
test.remoteaddr.IP, test.remoteaddr.IP,
uint16(test.remoteaddr.Port), uint16(test.remoteaddr.Port),