From d1f507b7f16e359dc2773195edb72a22357e5424 Mon Sep 17 00:00:00 2001
From: Felix Lange <fjl@twurst.com>
Date: Thu, 22 Oct 2015 23:46:01 +0200
Subject: [PATCH] p2p/discover: support incomplete node URLs, add Resolve

---
 p2p/discover/database_test.go | 16 +++----
 p2p/discover/node.go          | 72 ++++++++++++++++++++--------
 p2p/discover/node_test.go     | 88 +++++++++++++++++++++++++++--------
 p2p/discover/table.go         | 26 ++++++++++-
 p2p/discover/table_test.go    |  6 +--
 p2p/discover/udp.go           |  2 +-
 p2p/discover/udp_test.go      |  2 +-
 7 files changed, 158 insertions(+), 54 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..fac493f01 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,47 @@ 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
+}
+
 // 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 +123,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 +177,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 +214,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
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
@@ -38,14 +61,11 @@ var parseNodeTests = []struct {
 		rawurl:    "http://foobar",
 		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..efa6e8eea 100644
--- a/p2p/discover/table.go
+++ b/p2p/discover/table.go
@@ -99,7 +99,7 @@ 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{}),
@@ -196,6 +196,28 @@ func (tab *Table) Bootstrap(nodes []*Node) {
 	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
 // to the given target. It approaches the target by querying
 // 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)
 	}
 	// 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..e93949c56 100644
--- a/p2p/discover/udp.go
+++ b/p2p/discover/udp.go
@@ -120,7 +120,7 @@ func nodeFromRPC(rn rpcNode) (n *Node, valid bool) {
 	if rn.IP.IsMulticast() || rn.IP.IsUnspecified() || rn.UDP == 0 {
 		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 {
diff --git a/p2p/discover/udp_test.go b/p2p/discover/udp_test.go
index 944e73d6e..55d6d564a 100644
--- a/p2p/discover/udp_test.go
+++ b/p2p/discover/udp_test.go
@@ -243,7 +243,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),
-- 
GitLab