diff --git a/cmd/bootnode/main.go b/cmd/bootnode/main.go
index a40b32b600654706e081c6a57812c13507d786ff..2f9bba111769211a36266c5840d1d6cdcb4a66db 100644
--- a/cmd/bootnode/main.go
+++ b/cmd/bootnode/main.go
@@ -143,7 +143,7 @@ func printNotice(nodeKey *ecdsa.PublicKey, addr net.UDPAddr) {
 		addr.IP = net.IP{127, 0, 0, 1}
 	}
 	n := enode.NewV4(nodeKey, addr.IP, 0, addr.Port)
-	fmt.Println(n.String())
+	fmt.Println(n.URLv4())
 	fmt.Println("Note: you're using cmd/bootnode, a developer tool.")
 	fmt.Println("We recommend using a regular node as bootstrap node for production deployments.")
 }
diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go
index 08b23d46cde2dadaad0c285ea47e2e917b1239ac..f8092084ad18eada413869e07a5acb8799ced50a 100644
--- a/cmd/faucet/faucet.go
+++ b/cmd/faucet/faucet.go
@@ -260,7 +260,7 @@ func newFaucet(genesis *core.Genesis, port int, enodes []*discv5.Node, network u
 		return nil, err
 	}
 	for _, boot := range enodes {
-		old, err := enode.ParseV4(boot.String())
+		old, err := enode.Parse(enode.ValidSchemes, boot.String())
 		if err == nil {
 			stack.Server().AddPeer(old)
 		}
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index 002d37f1642d48ba97784ef27859c111ef5327f8..973e47ea0cc57daebebb0d52df43f0bda86a3358 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -794,7 +794,7 @@ func setBootstrapNodes(ctx *cli.Context, cfg *p2p.Config) {
 	cfg.BootstrapNodes = make([]*enode.Node, 0, len(urls))
 	for _, url := range urls {
 		if url != "" {
-			node, err := enode.ParseV4(url)
+			node, err := enode.Parse(enode.ValidSchemes, url)
 			if err != nil {
 				log.Crit("Bootstrap URL invalid", "enode", url, "err", err)
 				continue
diff --git a/cmd/wnode/main.go b/cmd/wnode/main.go
index 97e5852013bf23a520b9c4aa5d2fb24330870386..99cf84ec1bfa1e5008ea1111065c90e2902be4d0 100644
--- a/cmd/wnode/main.go
+++ b/cmd/wnode/main.go
@@ -203,7 +203,7 @@ func initialize() {
 		if len(*argEnode) == 0 {
 			argEnode = scanLineA("Please enter the peer's enode: ")
 		}
-		peer := enode.MustParseV4(*argEnode)
+		peer := enode.MustParse(*argEnode)
 		peers = append(peers, peer)
 	}
 
@@ -747,9 +747,9 @@ func requestExpiredMessagesLoop() {
 }
 
 func extractIDFromEnode(s string) []byte {
-	n, err := enode.ParseV4(s)
+	n, err := enode.Parse(enode.ValidSchemes, s)
 	if err != nil {
-		utils.Fatalf("Failed to parse enode: %s", err)
+		utils.Fatalf("Failed to parse node: %s", err)
 	}
 	return n.ID().Bytes()
 }
diff --git a/les/serverpool.go b/les/serverpool.go
index 668f39c562230402bb86e027480e4f549acd014b..3e8cdee4104db5448e8a8eaecdcad07329eb40a4 100644
--- a/les/serverpool.go
+++ b/les/serverpool.go
@@ -505,7 +505,7 @@ func parseTrustedNodes(trustedNodes []string) map[enode.ID]*enode.Node {
 	nodes := make(map[enode.ID]*enode.Node)
 
 	for _, node := range trustedNodes {
-		node, err := enode.ParseV4(node)
+		node, err := enode.Parse(enode.ValidSchemes, node)
 		if err != nil {
 			log.Warn("Trusted node URL invalid", "enode", node, "err", err)
 			continue
diff --git a/les/ulc.go b/les/ulc.go
index c6d41555296e55ef2d99a9fa17dd76a5d766492d..8792f60d3cfa38f0adb6398610c1bcc9d5816cc4 100644
--- a/les/ulc.go
+++ b/les/ulc.go
@@ -34,7 +34,7 @@ func newULC(ulcConfig *eth.ULCConfig) *ulc {
 	}
 	m := make(map[string]struct{}, len(ulcConfig.TrustedServers))
 	for _, id := range ulcConfig.TrustedServers {
-		node, err := enode.ParseV4(id)
+		node, err := enode.Parse(enode.ValidSchemes, id)
 		if err != nil {
 			log.Debug("Failed to parse trusted server", "id", id, "err", err)
 			continue
diff --git a/node/api.go b/node/api.go
index cc80efabec50f5d449f308a6259637a5d019e33f..66cd1dde3354b5f1029b37c3a2dc2b662e56240d 100644
--- a/node/api.go
+++ b/node/api.go
@@ -49,7 +49,7 @@ func (api *PrivateAdminAPI) AddPeer(url string) (bool, error) {
 		return false, ErrNodeStopped
 	}
 	// Try to add the url as a static peer and return
-	node, err := enode.ParseV4(url)
+	node, err := enode.Parse(enode.ValidSchemes, url)
 	if err != nil {
 		return false, fmt.Errorf("invalid enode: %v", err)
 	}
@@ -65,7 +65,7 @@ func (api *PrivateAdminAPI) RemovePeer(url string) (bool, error) {
 		return false, ErrNodeStopped
 	}
 	// Try to remove the url as a static peer and return
-	node, err := enode.ParseV4(url)
+	node, err := enode.Parse(enode.ValidSchemes, url)
 	if err != nil {
 		return false, fmt.Errorf("invalid enode: %v", err)
 	}
@@ -80,7 +80,7 @@ func (api *PrivateAdminAPI) AddTrustedPeer(url string) (bool, error) {
 	if server == nil {
 		return false, ErrNodeStopped
 	}
-	node, err := enode.ParseV4(url)
+	node, err := enode.Parse(enode.ValidSchemes, url)
 	if err != nil {
 		return false, fmt.Errorf("invalid enode: %v", err)
 	}
@@ -96,7 +96,7 @@ func (api *PrivateAdminAPI) RemoveTrustedPeer(url string) (bool, error) {
 	if server == nil {
 		return false, ErrNodeStopped
 	}
-	node, err := enode.ParseV4(url)
+	node, err := enode.Parse(enode.ValidSchemes, url)
 	if err != nil {
 		return false, fmt.Errorf("invalid enode: %v", err)
 	}
diff --git a/node/config.go b/node/config.go
index 31d36f203f289df83e1d8a92efd3e9cca4240466..1905ac7fa59113379b63d56e09bd50ffa81135d5 100644
--- a/node/config.go
+++ b/node/config.go
@@ -425,7 +425,7 @@ func (c *Config) parsePersistentNodes(w *bool, path string) []*enode.Node {
 		if url == "" {
 			continue
 		}
-		node, err := enode.ParseV4(url)
+		node, err := enode.Parse(enode.ValidSchemes, url)
 		if err != nil {
 			log.Error(fmt.Sprintf("Node URL %s: %v\n", url, err))
 			continue
diff --git a/p2p/discover/v4_udp_test.go b/p2p/discover/v4_udp_test.go
index 9d7badea197452d02218988f64241e221f60ba9c..c4f5b5de0f44fedd139f5306df60e28525d7a197 100644
--- a/p2p/discover/v4_udp_test.go
+++ b/p2p/discover/v4_udp_test.go
@@ -342,10 +342,10 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) {
 
 	// send the reply as two packets.
 	list := []*node{
-		wrapNode(enode.MustParseV4("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304")),
-		wrapNode(enode.MustParseV4("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303")),
-		wrapNode(enode.MustParseV4("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17")),
-		wrapNode(enode.MustParseV4("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303")),
+		wrapNode(enode.MustParse("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304")),
+		wrapNode(enode.MustParse("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303")),
+		wrapNode(enode.MustParse("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17")),
+		wrapNode(enode.MustParse("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303")),
 	}
 	rpclist := make([]rpcNode, len(list))
 	for i := range list {
diff --git a/p2p/enode/localnode.go b/p2p/enode/localnode.go
index 623f8eae1847dff7cb41ce5e347c585af2a2ef8d..d8aa02a77e266b36514f5b015b428f9986a72f6a 100644
--- a/p2p/enode/localnode.go
+++ b/p2p/enode/localnode.go
@@ -48,23 +48,32 @@ type LocalNode struct {
 	db  *DB
 
 	// everything below is protected by a lock
-	mu          sync.Mutex
-	seq         uint64
-	entries     map[string]enr.Entry
-	udpTrack    *netutil.IPTracker // predicts external UDP endpoint
-	staticIP    net.IP
-	fallbackIP  net.IP
-	fallbackUDP int
+	mu        sync.Mutex
+	seq       uint64
+	entries   map[string]enr.Entry
+	endpoint4 lnEndpoint
+	endpoint6 lnEndpoint
+}
+
+type lnEndpoint struct {
+	track                *netutil.IPTracker
+	staticIP, fallbackIP net.IP
+	fallbackUDP          int
 }
 
 // NewLocalNode creates a local node.
 func NewLocalNode(db *DB, key *ecdsa.PrivateKey) *LocalNode {
 	ln := &LocalNode{
-		id:       PubkeyToIDV4(&key.PublicKey),
-		db:       db,
-		key:      key,
-		udpTrack: netutil.NewIPTracker(iptrackWindow, iptrackContactWindow, iptrackMinStatements),
-		entries:  make(map[string]enr.Entry),
+		id:      PubkeyToIDV4(&key.PublicKey),
+		db:      db,
+		key:     key,
+		entries: make(map[string]enr.Entry),
+		endpoint4: lnEndpoint{
+			track: netutil.NewIPTracker(iptrackWindow, iptrackContactWindow, iptrackMinStatements),
+		},
+		endpoint6: lnEndpoint{
+			track: netutil.NewIPTracker(iptrackWindow, iptrackContactWindow, iptrackMinStatements),
+		},
 	}
 	ln.seq = db.localSeq(ln.id)
 	ln.invalidate()
@@ -89,13 +98,22 @@ func (ln *LocalNode) Node() *Node {
 	return ln.cur.Load().(*Node)
 }
 
+// Seq returns the current sequence number of the local node record.
+func (ln *LocalNode) Seq() uint64 {
+	ln.mu.Lock()
+	defer ln.mu.Unlock()
+
+	return ln.seq
+}
+
 // ID returns the local node ID.
 func (ln *LocalNode) ID() ID {
 	return ln.id
 }
 
-// Set puts the given entry into the local record, overwriting
-// any existing value.
+// Set puts the given entry into the local record, overwriting any existing value.
+// Use Set*IP and SetFallbackUDP to set IP addresses and UDP port, otherwise they'll
+// be overwritten by the endpoint predictor.
 func (ln *LocalNode) Set(e enr.Entry) {
 	ln.mu.Lock()
 	defer ln.mu.Unlock()
@@ -127,13 +145,20 @@ func (ln *LocalNode) delete(e enr.Entry) {
 	}
 }
 
+func (ln *LocalNode) endpointForIP(ip net.IP) *lnEndpoint {
+	if ip.To4() != nil {
+		return &ln.endpoint4
+	}
+	return &ln.endpoint6
+}
+
 // SetStaticIP sets the local IP to the given one unconditionally.
 // This disables endpoint prediction.
 func (ln *LocalNode) SetStaticIP(ip net.IP) {
 	ln.mu.Lock()
 	defer ln.mu.Unlock()
 
-	ln.staticIP = ip
+	ln.endpointForIP(ip).staticIP = ip
 	ln.updateEndpoints()
 }
 
@@ -143,17 +168,18 @@ func (ln *LocalNode) SetFallbackIP(ip net.IP) {
 	ln.mu.Lock()
 	defer ln.mu.Unlock()
 
-	ln.fallbackIP = ip
+	ln.endpointForIP(ip).fallbackIP = ip
 	ln.updateEndpoints()
 }
 
-// SetFallbackUDP sets the last-resort UDP port. This port is used
+// SetFallbackUDP sets the last-resort UDP-on-IPv4 port. This port is used
 // if no endpoint prediction can be made.
 func (ln *LocalNode) SetFallbackUDP(port int) {
 	ln.mu.Lock()
 	defer ln.mu.Unlock()
 
-	ln.fallbackUDP = port
+	ln.endpoint4.fallbackUDP = port
+	ln.endpoint6.fallbackUDP = port
 	ln.updateEndpoints()
 }
 
@@ -163,7 +189,7 @@ func (ln *LocalNode) UDPEndpointStatement(fromaddr, endpoint *net.UDPAddr) {
 	ln.mu.Lock()
 	defer ln.mu.Unlock()
 
-	ln.udpTrack.AddStatement(fromaddr.String(), endpoint.String())
+	ln.endpointForIP(endpoint.IP).track.AddStatement(fromaddr.String(), endpoint.String())
 	ln.updateEndpoints()
 }
 
@@ -173,32 +199,50 @@ func (ln *LocalNode) UDPContact(toaddr *net.UDPAddr) {
 	ln.mu.Lock()
 	defer ln.mu.Unlock()
 
-	ln.udpTrack.AddContact(toaddr.String())
+	ln.endpointForIP(toaddr.IP).track.AddContact(toaddr.String())
 	ln.updateEndpoints()
 }
 
+// updateEndpoints updates the record with predicted endpoints.
 func (ln *LocalNode) updateEndpoints() {
-	// Determine the endpoints.
-	newIP := ln.fallbackIP
-	newUDP := ln.fallbackUDP
-	if ln.staticIP != nil {
-		newIP = ln.staticIP
-	} else if ip, port := predictAddr(ln.udpTrack); ip != nil {
-		newIP = ip
-		newUDP = port
-	}
+	ip4, udp4 := ln.endpoint4.get()
+	ip6, udp6 := ln.endpoint6.get()
 
-	// Update the record.
-	if newIP != nil && !newIP.IsUnspecified() {
-		ln.set(enr.IP(newIP))
-		if newUDP != 0 {
-			ln.set(enr.UDP(newUDP))
-		} else {
-			ln.delete(enr.UDP(0))
-		}
+	if ip4 != nil && !ip4.IsUnspecified() {
+		ln.set(enr.IPv4(ip4))
 	} else {
-		ln.delete(enr.IP{})
+		ln.delete(enr.IPv4{})
+	}
+	if ip6 != nil && !ip6.IsUnspecified() {
+		ln.set(enr.IPv6(ip6))
+	} else {
+		ln.delete(enr.IPv6{})
+	}
+	if udp4 != 0 {
+		ln.set(enr.UDP(udp4))
+	} else {
+		ln.delete(enr.UDP(0))
+	}
+	if udp6 != 0 && udp6 != udp4 {
+		ln.set(enr.UDP6(udp6))
+	} else {
+		ln.delete(enr.UDP6(0))
+	}
+}
+
+// get returns the endpoint with highest precedence.
+func (e *lnEndpoint) get() (newIP net.IP, newPort int) {
+	newPort = e.fallbackUDP
+	if e.fallbackIP != nil {
+		newIP = e.fallbackIP
+	}
+	if e.staticIP != nil {
+		newIP = e.staticIP
+	} else if ip, port := predictAddr(e.track); ip != nil {
+		newIP = ip
+		newPort = port
 	}
+	return newIP, newPort
 }
 
 // predictAddr wraps IPTracker.PredictEndpoint, converting from its string-based
diff --git a/p2p/enode/localnode_test.go b/p2p/enode/localnode_test.go
index f5e3496d6306979bceb14ef2f5c484748b2480ad..00746a8d277af60d95b772bae6a72fb2ee59917a 100644
--- a/p2p/enode/localnode_test.go
+++ b/p2p/enode/localnode_test.go
@@ -17,10 +17,13 @@
 package enode
 
 import (
+	"math/rand"
+	"net"
 	"testing"
 
 	"github.com/ethereum/go-ethereum/crypto"
 	"github.com/ethereum/go-ethereum/p2p/enr"
+	"github.com/stretchr/testify/assert"
 )
 
 func newLocalNodeForTesting() (*LocalNode, *DB) {
@@ -74,3 +77,46 @@ func TestLocalNodeSeqPersist(t *testing.T) {
 		t.Fatalf("wrong seq %d on instance with changed key, want 1", s)
 	}
 }
+
+// This test checks behavior of the endpoint predictor.
+func TestLocalNodeEndpoint(t *testing.T) {
+	var (
+		fallback  = &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: 80}
+		predicted = &net.UDPAddr{IP: net.IP{127, 0, 1, 2}, Port: 81}
+		staticIP  = net.IP{127, 0, 1, 2}
+	)
+	ln, db := newLocalNodeForTesting()
+	defer db.Close()
+
+	// Nothing is set initially.
+	assert.Equal(t, net.IP(nil), ln.Node().IP())
+	assert.Equal(t, 0, ln.Node().UDP())
+	assert.Equal(t, uint64(1), ln.Node().Seq())
+
+	// Set up fallback address.
+	ln.SetFallbackIP(fallback.IP)
+	ln.SetFallbackUDP(fallback.Port)
+	assert.Equal(t, fallback.IP, ln.Node().IP())
+	assert.Equal(t, fallback.Port, ln.Node().UDP())
+	assert.Equal(t, uint64(2), ln.Node().Seq())
+
+	// Add endpoint statements from random hosts.
+	for i := 0; i < iptrackMinStatements; i++ {
+		assert.Equal(t, fallback.IP, ln.Node().IP())
+		assert.Equal(t, fallback.Port, ln.Node().UDP())
+		assert.Equal(t, uint64(2), ln.Node().Seq())
+
+		from := &net.UDPAddr{IP: make(net.IP, 4), Port: 90}
+		rand.Read(from.IP)
+		ln.UDPEndpointStatement(from, predicted)
+	}
+	assert.Equal(t, predicted.IP, ln.Node().IP())
+	assert.Equal(t, predicted.Port, ln.Node().UDP())
+	assert.Equal(t, uint64(3), ln.Node().Seq())
+
+	// Static IP overrides prediction.
+	ln.SetStaticIP(staticIP)
+	assert.Equal(t, staticIP, ln.Node().IP())
+	assert.Equal(t, fallback.Port, ln.Node().UDP())
+	assert.Equal(t, uint64(4), ln.Node().Seq())
+}
diff --git a/p2p/enode/node.go b/p2p/enode/node.go
index b454ab2554d5feddbe3e641f7f7818f57235a659..9eb2544ffe1427a44a5cba51625571e7c976c953 100644
--- a/p2p/enode/node.go
+++ b/p2p/enode/node.go
@@ -18,6 +18,7 @@ package enode
 
 import (
 	"crypto/ecdsa"
+	"encoding/base64"
 	"encoding/hex"
 	"errors"
 	"fmt"
@@ -27,8 +28,11 @@ import (
 	"strings"
 
 	"github.com/ethereum/go-ethereum/p2p/enr"
+	"github.com/ethereum/go-ethereum/rlp"
 )
 
+var errMissingPrefix = errors.New("missing 'enr:' prefix for base64-encoded record")
+
 // Node represents a host on the network.
 type Node struct {
 	r  enr.Record
@@ -48,6 +52,34 @@ func New(validSchemes enr.IdentityScheme, r *enr.Record) (*Node, error) {
 	return node, nil
 }
 
+// MustParse parses a node record or enode:// URL. It panics if the input is invalid.
+func MustParse(rawurl string) *Node {
+	n, err := Parse(ValidSchemes, rawurl)
+	if err != nil {
+		panic("invalid node: " + err.Error())
+	}
+	return n
+}
+
+// Parse decodes and verifies a base64-encoded node record.
+func Parse(validSchemes enr.IdentityScheme, input string) (*Node, error) {
+	if strings.HasPrefix(input, "enode://") {
+		return ParseV4(input)
+	}
+	if !strings.HasPrefix(input, "enr:") {
+		return nil, errMissingPrefix
+	}
+	bin, err := base64.RawURLEncoding.DecodeString(input[4:])
+	if err != nil {
+		return nil, err
+	}
+	var r enr.Record
+	if err := rlp.DecodeBytes(bin, &r); err != nil {
+		return nil, err
+	}
+	return New(validSchemes, &r)
+}
+
 // ID returns the node identifier.
 func (n *Node) ID() ID {
 	return n.id
@@ -68,11 +100,19 @@ func (n *Node) Load(k enr.Entry) error {
 	return n.r.Load(k)
 }
 
-// IP returns the IP address of the node.
+// IP returns the IP address of the node. This prefers IPv4 addresses.
 func (n *Node) IP() net.IP {
-	var ip net.IP
-	n.Load((*enr.IP)(&ip))
-	return ip
+	var (
+		ip4 enr.IPv4
+		ip6 enr.IPv6
+	)
+	if n.Load(&ip4) == nil {
+		return net.IP(ip4)
+	}
+	if n.Load(&ip6) == nil {
+		return net.IP(ip6)
+	}
+	return nil
 }
 
 // UDP returns the UDP port of the node.
@@ -105,10 +145,11 @@ func (n *Node) Record() *enr.Record {
 	return &cpy
 }
 
-// checks whether n is a valid complete node.
+// ValidateComplete checks whether n has a valid IP and UDP port.
+// Deprecated: don't use this method.
 func (n *Node) ValidateComplete() error {
 	if n.Incomplete() {
-		return errors.New("incomplete node")
+		return errors.New("missing IP address")
 	}
 	if n.UDP() == 0 {
 		return errors.New("missing UDP port")
@@ -122,20 +163,24 @@ func (n *Node) ValidateComplete() error {
 	return n.Load(&key)
 }
 
-// The string representation of a Node is a URL.
-// Please see ParseNode for a description of the format.
+// String returns the text representation of the record.
 func (n *Node) String() string {
-	return n.v4URL()
+	if isNewV4(n) {
+		return n.URLv4() // backwards-compatibility glue for NewV4 nodes
+	}
+	enc, _ := rlp.EncodeToBytes(&n.r) // always succeeds because record is valid
+	b64 := base64.RawURLEncoding.EncodeToString(enc)
+	return "enr:" + b64
 }
 
 // MarshalText implements encoding.TextMarshaler.
 func (n *Node) MarshalText() ([]byte, error) {
-	return []byte(n.v4URL()), nil
+	return []byte(n.String()), nil
 }
 
 // UnmarshalText implements encoding.TextUnmarshaler.
 func (n *Node) UnmarshalText(text []byte) error {
-	dec, err := ParseV4(string(text))
+	dec, err := Parse(ValidSchemes, string(text))
 	if err == nil {
 		*n = *dec
 	}
diff --git a/p2p/enode/node_test.go b/p2p/enode/node_test.go
index 861a70bd64e9267cc708ebfd7db157fcfd72ca99..d15859c477a585f96a08a9e7bfd06b16eb016cbf 100644
--- a/p2p/enode/node_test.go
+++ b/p2p/enode/node_test.go
@@ -17,9 +17,12 @@
 package enode
 
 import (
+	"bytes"
 	"encoding/hex"
 	"fmt"
+	"math/big"
 	"testing"
+	"testing/quick"
 
 	"github.com/ethereum/go-ethereum/p2p/enr"
 	"github.com/ethereum/go-ethereum/rlp"
@@ -43,7 +46,7 @@ func TestPythonInterop(t *testing.T) {
 	var (
 		wantID  = HexID("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7")
 		wantSeq = uint64(1)
-		wantIP  = enr.IP{127, 0, 0, 1}
+		wantIP  = enr.IPv4{127, 0, 0, 1}
 		wantUDP = enr.UDP(30303)
 	)
 	if n.Seq() != wantSeq {
@@ -52,7 +55,7 @@ func TestPythonInterop(t *testing.T) {
 	if n.ID() != wantID {
 		t.Errorf("wrong id: got %x, want %x", n.ID(), wantID)
 	}
-	want := map[enr.Entry]interface{}{new(enr.IP): &wantIP, new(enr.UDP): &wantUDP}
+	want := map[enr.Entry]interface{}{new(enr.IPv4): &wantIP, new(enr.UDP): &wantUDP}
 	for k, v := range want {
 		desc := fmt.Sprintf("loading key %q", k.ENRKey())
 		if assert.NoError(t, n.Load(k), desc) {
@@ -60,3 +63,83 @@ func TestPythonInterop(t *testing.T) {
 		}
 	}
 }
+
+func TestHexID(t *testing.T) {
+	ref := ID{0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188}
+	id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
+	id2 := HexID("00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
+
+	if id1 != ref {
+		t.Errorf("wrong id1\ngot  %v\nwant %v", id1[:], ref[:])
+	}
+	if id2 != ref {
+		t.Errorf("wrong id2\ngot  %v\nwant %v", id2[:], ref[:])
+	}
+}
+
+func TestID_textEncoding(t *testing.T) {
+	ref := ID{
+		0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10,
+		0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20,
+		0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x30,
+		0x31, 0x32,
+	}
+	hex := "0102030405060708091011121314151617181920212223242526272829303132"
+
+	text, err := ref.MarshalText()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(text, []byte(hex)) {
+		t.Fatalf("text encoding did not match\nexpected: %s\ngot:      %s", hex, text)
+	}
+
+	id := new(ID)
+	if err := id.UnmarshalText(text); err != nil {
+		t.Fatal(err)
+	}
+	if *id != ref {
+		t.Fatalf("text decoding did not match\nexpected: %s\ngot:      %s", ref, id)
+	}
+}
+
+func TestID_distcmp(t *testing.T) {
+	distcmpBig := func(target, a, b ID) int {
+		tbig := new(big.Int).SetBytes(target[:])
+		abig := new(big.Int).SetBytes(a[:])
+		bbig := new(big.Int).SetBytes(b[:])
+		return new(big.Int).Xor(tbig, abig).Cmp(new(big.Int).Xor(tbig, bbig))
+	}
+	if err := quick.CheckEqual(DistCmp, distcmpBig, nil); err != nil {
+		t.Error(err)
+	}
+}
+
+// The random tests is likely to miss the case where a and b are equal,
+// this test checks it explicitly.
+func TestID_distcmpEqual(t *testing.T) {
+	base := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
+	x := ID{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
+	if DistCmp(base, x, x) != 0 {
+		t.Errorf("DistCmp(base, x, x) != 0")
+	}
+}
+
+func TestID_logdist(t *testing.T) {
+	logdistBig := func(a, b ID) int {
+		abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:])
+		return new(big.Int).Xor(abig, bbig).BitLen()
+	}
+	if err := quick.CheckEqual(LogDist, logdistBig, nil); err != nil {
+		t.Error(err)
+	}
+}
+
+// The random tests is likely to miss the case where a and b are equal,
+// this test checks it explicitly.
+func TestID_logdistEqual(t *testing.T) {
+	x := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
+	if LogDist(x, x) != 0 {
+		t.Errorf("LogDist(x, x) != 0")
+	}
+}
diff --git a/p2p/enode/urlv4.go b/p2p/enode/urlv4.go
index 50e9485d04c41705279f5f849cafdd1e7c7908a9..2372d4820b0a259d3caa5479519162330d2979ec 100644
--- a/p2p/enode/urlv4.go
+++ b/p2p/enode/urlv4.go
@@ -70,7 +70,7 @@ func ParseV4(rawurl string) (*Node, error) {
 	if m := incompleteNodeURL.FindStringSubmatch(rawurl); m != nil {
 		id, err := parsePubkey(m[1])
 		if err != nil {
-			return nil, fmt.Errorf("invalid node ID (%v)", err)
+			return nil, fmt.Errorf("invalid public key (%v)", err)
 		}
 		return NewV4(id, nil, 0, 0), nil
 	}
@@ -81,7 +81,7 @@ func ParseV4(rawurl string) (*Node, error) {
 // contained in the node has a zero-length signature.
 func NewV4(pubkey *ecdsa.PublicKey, ip net.IP, tcp, udp int) *Node {
 	var r enr.Record
-	if ip != nil {
+	if len(ip) > 0 {
 		r.Set(enr.IP(ip))
 	}
 	if udp != 0 {
@@ -98,6 +98,12 @@ func NewV4(pubkey *ecdsa.PublicKey, ip net.IP, tcp, udp int) *Node {
 	return n
 }
 
+// isNewV4 returns true for nodes created by NewV4.
+func isNewV4(n *Node) bool {
+	var k s256raw
+	return n.r.IdentityScheme() == "" && n.r.Load(&k) == nil && len(n.r.Signature()) == 0
+}
+
 func parseComplete(rawurl string) (*Node, error) {
 	var (
 		id               *ecdsa.PublicKey
@@ -116,7 +122,7 @@ func parseComplete(rawurl string) (*Node, error) {
 		return nil, errors.New("does not contain node ID")
 	}
 	if id, err = parsePubkey(u.User.String()); err != nil {
-		return nil, fmt.Errorf("invalid node ID (%v)", err)
+		return nil, fmt.Errorf("invalid public key (%v)", err)
 	}
 	// Parse the IP address.
 	host, port, err := net.SplitHostPort(u.Host)
@@ -126,10 +132,6 @@ func parseComplete(rawurl string) (*Node, error) {
 	if ip = net.ParseIP(host); ip == nil {
 		return nil, errors.New("invalid IP address")
 	}
-	// Ensure the IP is 4 bytes long for IPv4 addresses.
-	if ipv4 := ip.To4(); ipv4 != nil {
-		ip = ipv4
-	}
 	// Parse the port numbers.
 	if tcpPort, err = strconv.ParseUint(port, 10, 16); err != nil {
 		return nil, errors.New("invalid port")
@@ -157,7 +159,7 @@ func parsePubkey(in string) (*ecdsa.PublicKey, error) {
 	return crypto.UnmarshalPubkey(b)
 }
 
-func (n *Node) v4URL() string {
+func (n *Node) URLv4() string {
 	var (
 		scheme enr.ID
 		nodeid string
diff --git a/p2p/enode/urlv4_test.go b/p2p/enode/urlv4_test.go
index 3680ab6b78b654989c56330687686410acca408c..69ed1110213b1781da6569df565a82d3eb64c6be 100644
--- a/p2p/enode/urlv4_test.go
+++ b/p2p/enode/urlv4_test.go
@@ -17,44 +17,63 @@
 package enode
 
 import (
-	"bytes"
 	"crypto/ecdsa"
-	"math/big"
 	"net"
 	"reflect"
 	"strings"
 	"testing"
-	"testing/quick"
+
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/p2p/enr"
 )
 
 var parseNodeTests = []struct {
-	rawurl     string
+	input      string
 	wantError  string
 	wantResult *Node
 }{
+	// Records
+	{
+		input: "enr:-IS4QGrdq0ugARp5T2BZ41TrZOqLc_oKvZoPuZP5--anqWE_J-Tucc1xgkOL7qXl0puJgT7qc2KSvcupc4NCb0nr4tdjgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQM6UUF2Rm-oFe1IH_rQkRCi00T2ybeMHRSvw1HDpRvjPYN1ZHCCdl8",
+		wantResult: func() *Node {
+			testKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
+			var r enr.Record
+			r.Set(enr.IP{127, 0, 0, 1})
+			r.Set(enr.UDP(30303))
+			r.SetSeq(99)
+			SignV4(&r, testKey)
+			n, _ := New(ValidSchemes, &r)
+			return n
+		}(),
+	},
+	// Invalid Records
 	{
-		rawurl:    "http://foobar",
-		wantError: `invalid URL scheme, want "enode"`,
+		input:     "enr:",
+		wantError: "EOF", // could be nicer
 	},
 	{
-		rawurl:    "enode://01010101@123.124.125.126:3",
-		wantError: `invalid node ID (wrong length, want 128 hex chars)`,
+		input:     "enr:x",
+		wantError: "illegal base64 data at input byte 0",
 	},
-	// Complete nodes with IP address.
 	{
-		rawurl:    "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3",
+		input:     "enr:-EmGZm9vYmFyY4JpZIJ2NIJpcIR_AAABiXNlY3AyNTZrMaEDOlFBdkZvqBXtSB_60JEQotNE9sm3jB0Ur8NRw6Ub4z2DdWRwgnZf",
+		wantError: enr.ErrInvalidSig.Error(),
+	},
+	// Complete node URLs with IP address and ports
+	{
+		input:     "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3",
 		wantError: `invalid IP address`,
 	},
 	{
-		rawurl:    "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo",
+		input:     "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo",
 		wantError: `invalid port`,
 	},
 	{
-		rawurl:    "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo",
+		input:     "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo",
 		wantError: `invalid discport in query`,
 	},
 	{
-		rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150",
+		input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150",
 		wantResult: NewV4(
 			hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
 			net.IP{0x7f, 0x0, 0x0, 0x1},
@@ -63,7 +82,7 @@ var parseNodeTests = []struct {
 		),
 	},
 	{
-		rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150",
+		input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150",
 		wantResult: NewV4(
 			hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
 			net.ParseIP("::"),
@@ -72,7 +91,7 @@ var parseNodeTests = []struct {
 		),
 	},
 	{
-		rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150",
+		input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150",
 		wantResult: NewV4(
 			hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
 			net.ParseIP("2001:db8:3c4d:15::abcd:ef12"),
@@ -81,7 +100,7 @@ var parseNodeTests = []struct {
 		),
 	},
 	{
-		rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334",
+		input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334",
 		wantResult: NewV4(
 			hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
 			net.IP{0x7f, 0x0, 0x0, 0x1},
@@ -89,34 +108,42 @@ var parseNodeTests = []struct {
 			22334,
 		),
 	},
-	// Incomplete nodes with no address.
+	// Incomplete node URLs with no address
 	{
-		rawurl: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
+		input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
 		wantResult: NewV4(
 			hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
 			nil, 0, 0,
 		),
 	},
+	// Invalid URLs
 	{
-		rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
-		wantResult: NewV4(
-			hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
-			nil, 0, 0,
-		),
+		input:     "",
+		wantError: errMissingPrefix.Error(),
 	},
-	// Invalid URLs
 	{
-		rawurl:    "01010101",
-		wantError: `invalid node ID (wrong length, want 128 hex chars)`,
+		input:     "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
+		wantError: errMissingPrefix.Error(),
 	},
 	{
-		rawurl:    "enode://01010101",
-		wantError: `invalid node ID (wrong length, want 128 hex chars)`,
+		input:     "01010101",
+		wantError: errMissingPrefix.Error(),
 	},
 	{
-		// This test checks that errors from url.Parse are handled.
-		rawurl:    "://foo",
-		wantError: `parse ://foo: missing protocol scheme`,
+		input:     "enode://01010101@123.124.125.126:3",
+		wantError: `invalid public key (wrong length, want 128 hex chars)`,
+	},
+	{
+		input:     "enode://01010101",
+		wantError: `invalid public key (wrong length, want 128 hex chars)`,
+	},
+	{
+		input:     "http://foobar",
+		wantError: errMissingPrefix.Error(),
+	},
+	{
+		input:     "://foo",
+		wantError: errMissingPrefix.Error(),
 	},
 }
 
@@ -130,22 +157,22 @@ func hexPubkey(h string) *ecdsa.PublicKey {
 
 func TestParseNode(t *testing.T) {
 	for _, test := range parseNodeTests {
-		n, err := ParseV4(test.rawurl)
+		n, err := Parse(ValidSchemes, test.input)
 		if test.wantError != "" {
 			if err == nil {
-				t.Errorf("test %q:\n  got nil error, expected %#q", test.rawurl, test.wantError)
+				t.Errorf("test %q:\n  got nil error, expected %#q", test.input, test.wantError)
 				continue
 			} else if err.Error() != test.wantError {
-				t.Errorf("test %q:\n  got error %#q, expected %#q", test.rawurl, err.Error(), test.wantError)
+				t.Errorf("test %q:\n  got error %#q, expected %#q", test.input, err.Error(), test.wantError)
 				continue
 			}
 		} else {
 			if err != nil {
-				t.Errorf("test %q:\n  unexpected error: %v", test.rawurl, err)
+				t.Errorf("test %q:\n  unexpected error: %v", test.input, err)
 				continue
 			}
 			if !reflect.DeepEqual(n, test.wantResult) {
-				t.Errorf("test %q:\n  result mismatch:\ngot:  %#v\nwant: %#v", test.rawurl, n, test.wantResult)
+				t.Errorf("test %q:\n  result mismatch:\ngot:  %#v\nwant: %#v", test.input, n, test.wantResult)
 			}
 		}
 	}
@@ -153,91 +180,11 @@ func TestParseNode(t *testing.T) {
 
 func TestNodeString(t *testing.T) {
 	for i, test := range parseNodeTests {
-		if test.wantError == "" && strings.HasPrefix(test.rawurl, "enode://") {
+		if test.wantError == "" && strings.HasPrefix(test.input, "enode://") {
 			str := test.wantResult.String()
-			if str != test.rawurl {
-				t.Errorf("test %d: Node.String() mismatch:\ngot:  %s\nwant: %s", i, str, test.rawurl)
+			if str != test.input {
+				t.Errorf("test %d: Node.String() mismatch:\ngot:  %s\nwant: %s", i, str, test.input)
 			}
 		}
 	}
 }
-
-func TestHexID(t *testing.T) {
-	ref := ID{0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188}
-	id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
-	id2 := HexID("00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
-
-	if id1 != ref {
-		t.Errorf("wrong id1\ngot  %v\nwant %v", id1[:], ref[:])
-	}
-	if id2 != ref {
-		t.Errorf("wrong id2\ngot  %v\nwant %v", id2[:], ref[:])
-	}
-}
-
-func TestID_textEncoding(t *testing.T) {
-	ref := ID{
-		0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10,
-		0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20,
-		0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x30,
-		0x31, 0x32,
-	}
-	hex := "0102030405060708091011121314151617181920212223242526272829303132"
-
-	text, err := ref.MarshalText()
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !bytes.Equal(text, []byte(hex)) {
-		t.Fatalf("text encoding did not match\nexpected: %s\ngot:      %s", hex, text)
-	}
-
-	id := new(ID)
-	if err := id.UnmarshalText(text); err != nil {
-		t.Fatal(err)
-	}
-	if *id != ref {
-		t.Fatalf("text decoding did not match\nexpected: %s\ngot:      %s", ref, id)
-	}
-}
-
-func TestNodeID_distcmp(t *testing.T) {
-	distcmpBig := func(target, a, b ID) int {
-		tbig := new(big.Int).SetBytes(target[:])
-		abig := new(big.Int).SetBytes(a[:])
-		bbig := new(big.Int).SetBytes(b[:])
-		return new(big.Int).Xor(tbig, abig).Cmp(new(big.Int).Xor(tbig, bbig))
-	}
-	if err := quick.CheckEqual(DistCmp, distcmpBig, nil); err != nil {
-		t.Error(err)
-	}
-}
-
-// The random tests is likely to miss the case where a and b are equal,
-// this test checks it explicitly.
-func TestNodeID_distcmpEqual(t *testing.T) {
-	base := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
-	x := ID{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
-	if DistCmp(base, x, x) != 0 {
-		t.Errorf("DistCmp(base, x, x) != 0")
-	}
-}
-
-func TestNodeID_logdist(t *testing.T) {
-	logdistBig := func(a, b ID) int {
-		abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:])
-		return new(big.Int).Xor(abig, bbig).BitLen()
-	}
-	if err := quick.CheckEqual(LogDist, logdistBig, nil); err != nil {
-		t.Error(err)
-	}
-}
-
-// The random tests is likely to miss the case where a and b are equal,
-// this test checks it explicitly.
-func TestNodeID_logdistEqual(t *testing.T) {
-	x := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
-	if LogDist(x, x) != 0 {
-		t.Errorf("LogDist(x, x) != 0")
-	}
-}
diff --git a/p2p/enr/enr.go b/p2p/enr/enr.go
index 444820c15857c4664995f1c958fac118f2eeb1a4..c36ae9e3edeac8d35ddf81e7455efc0d3f2a4770 100644
--- a/p2p/enr/enr.go
+++ b/p2p/enr/enr.go
@@ -163,6 +163,16 @@ func (r *Record) invalidate() {
 	r.raw = nil
 }
 
+// Signature returns the signature of the record.
+func (r *Record) Signature() []byte {
+	if r.signature == nil {
+		return nil
+	}
+	cpy := make([]byte, len(r.signature))
+	copy(cpy, r.signature)
+	return cpy
+}
+
 // EncodeRLP implements rlp.Encoder. Encoding fails if
 // the record is unsigned.
 func (r Record) EncodeRLP(w io.Writer) error {
@@ -173,7 +183,7 @@ func (r Record) EncodeRLP(w io.Writer) error {
 	return err
 }
 
-// DecodeRLP implements rlp.Decoder. Decoding verifies the signature.
+// DecodeRLP implements rlp.Decoder. Decoding doesn't verify the signature.
 func (r *Record) DecodeRLP(s *rlp.Stream) error {
 	dec, raw, err := decodeRecord(s)
 	if err != nil {
diff --git a/p2p/enr/enr_test.go b/p2p/enr/enr_test.go
index 449c898a84795b91b40191f3ebf20a31723aca26..434685e0b66550ee3e0474dd57f58de23319055f 100644
--- a/p2p/enr/enr_test.go
+++ b/p2p/enr/enr_test.go
@@ -49,23 +49,23 @@ func TestGetSetID(t *testing.T) {
 }
 
 // TestGetSetIP4 tests encoding/decoding and setting/getting of the IP key.
-func TestGetSetIP4(t *testing.T) {
-	ip := IP{192, 168, 0, 3}
+func TestGetSetIPv4(t *testing.T) {
+	ip := IPv4{192, 168, 0, 3}
 	var r Record
 	r.Set(ip)
 
-	var ip2 IP
+	var ip2 IPv4
 	require.NoError(t, r.Load(&ip2))
 	assert.Equal(t, ip, ip2)
 }
 
-// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP key.
-func TestGetSetIP6(t *testing.T) {
-	ip := IP{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}
+// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP6 key.
+func TestGetSetIPv6(t *testing.T) {
+	ip := IPv6{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}
 	var r Record
 	r.Set(ip)
 
-	var ip2 IP
+	var ip2 IPv6
 	require.NoError(t, r.Load(&ip2))
 	assert.Equal(t, ip, ip2)
 }
@@ -83,7 +83,7 @@ func TestGetSetUDP(t *testing.T) {
 
 func TestLoadErrors(t *testing.T) {
 	var r Record
-	ip4 := IP{127, 0, 0, 1}
+	ip4 := IPv4{127, 0, 0, 1}
 	r.Set(ip4)
 
 	// Check error for missing keys.
@@ -185,13 +185,13 @@ func TestSeq(t *testing.T) {
 func TestGetSetOverwrite(t *testing.T) {
 	var r Record
 
-	ip := IP{192, 168, 0, 3}
+	ip := IPv4{192, 168, 0, 3}
 	r.Set(ip)
 
-	ip2 := IP{192, 168, 0, 4}
+	ip2 := IPv4{192, 168, 0, 4}
 	r.Set(ip2)
 
-	var ip3 IP
+	var ip3 IPv4
 	require.NoError(t, r.Load(&ip3))
 	assert.Equal(t, ip2, ip3)
 }
@@ -200,7 +200,7 @@ func TestGetSetOverwrite(t *testing.T) {
 func TestSignEncodeAndDecode(t *testing.T) {
 	var r Record
 	r.Set(UDP(30303))
-	r.Set(IP{127, 0, 0, 1})
+	r.Set(IPv4{127, 0, 0, 1})
 	require.NoError(t, signTest([]byte{5}, &r))
 
 	blob, err := rlp.EncodeToBytes(r)
diff --git a/p2p/enr/entries.go b/p2p/enr/entries.go
index 347990ab64fb4be74206dd80f91a53b5495a2996..f2118401afb85726faeeda115a0d7052d115c17d 100644
--- a/p2p/enr/entries.go
+++ b/p2p/enr/entries.go
@@ -60,11 +60,21 @@ type TCP uint16
 
 func (v TCP) ENRKey() string { return "tcp" }
 
+// UDP is the "udp" key, which holds the IPv6-specific UDP port of the node.
+type TCP6 uint16
+
+func (v TCP6) ENRKey() string { return "tcp6" }
+
 // UDP is the "udp" key, which holds the UDP port of the node.
 type UDP uint16
 
 func (v UDP) ENRKey() string { return "udp" }
 
+// UDP is the "udp" key, which holds the IPv6-specific UDP port of the node.
+type UDP6 uint16
+
+func (v UDP6) ENRKey() string { return "udp6" }
+
 // ID is the "id" key, which holds the name of the identity scheme.
 type ID string
 
@@ -72,17 +82,27 @@ const IDv4 = ID("v4") // the default identity scheme
 
 func (v ID) ENRKey() string { return "id" }
 
-// IP is the "ip" key, which holds the IP address of the node.
+// IP is either the "ip" or "ip6" key, depending on the value.
+// Use this value to encode IP addresses that can be either v4 or v6.
+// To load an address from a record use the IPv4 or IPv6 types.
 type IP net.IP
 
-func (v IP) ENRKey() string { return "ip" }
+func (v IP) ENRKey() string {
+	if net.IP(v).To4() == nil {
+		return "ip6"
+	}
+	return "ip"
+}
 
 // EncodeRLP implements rlp.Encoder.
 func (v IP) EncodeRLP(w io.Writer) error {
 	if ip4 := net.IP(v).To4(); ip4 != nil {
 		return rlp.Encode(w, ip4)
 	}
-	return rlp.Encode(w, net.IP(v))
+	if ip6 := net.IP(v).To16(); ip6 != nil {
+		return rlp.Encode(w, ip6)
+	}
+	return fmt.Errorf("invalid IP address: %v", net.IP(v))
 }
 
 // DecodeRLP implements rlp.Decoder.
@@ -96,6 +116,56 @@ func (v *IP) DecodeRLP(s *rlp.Stream) error {
 	return nil
 }
 
+// IPv4 is the "ip" key, which holds the IP address of the node.
+type IPv4 net.IP
+
+func (v IPv4) ENRKey() string { return "ip" }
+
+// EncodeRLP implements rlp.Encoder.
+func (v IPv4) EncodeRLP(w io.Writer) error {
+	ip4 := net.IP(v).To4()
+	if ip4 == nil {
+		return fmt.Errorf("invalid IPv4 address: %v", net.IP(v))
+	}
+	return rlp.Encode(w, ip4)
+}
+
+// DecodeRLP implements rlp.Decoder.
+func (v *IPv4) DecodeRLP(s *rlp.Stream) error {
+	if err := s.Decode((*net.IP)(v)); err != nil {
+		return err
+	}
+	if len(*v) != 4 {
+		return fmt.Errorf("invalid IPv4 address, want 4 bytes: %v", *v)
+	}
+	return nil
+}
+
+// IPv6 is the "ip6" key, which holds the IP address of the node.
+type IPv6 net.IP
+
+func (v IPv6) ENRKey() string { return "ip6" }
+
+// EncodeRLP implements rlp.Encoder.
+func (v IPv6) EncodeRLP(w io.Writer) error {
+	ip6 := net.IP(v).To16()
+	if ip6 == nil {
+		return fmt.Errorf("invalid IPv6 address: %v", net.IP(v))
+	}
+	return rlp.Encode(w, ip6)
+}
+
+// DecodeRLP implements rlp.Decoder.
+func (v *IPv6) DecodeRLP(s *rlp.Stream) error {
+	if err := s.Decode((*net.IP)(v)); err != nil {
+		return err
+	}
+	if len(*v) != 16 {
+		return fmt.Errorf("invalid IPv6 address, want 16 bytes: %v", *v)
+	}
+	return nil
+}
+
 // KeyError is an error related to a key.
 type KeyError struct {
 	Key string
diff --git a/p2p/server.go b/p2p/server.go
index 566f01ffc5ddfc1d8e22a782e27b28a5259849ee..f17ef2c2bfcf7a44953aa3eedab6abc85df3ec1e 100644
--- a/p2p/server.go
+++ b/p2p/server.go
@@ -39,7 +39,6 @@ import (
 	"github.com/ethereum/go-ethereum/p2p/enr"
 	"github.com/ethereum/go-ethereum/p2p/nat"
 	"github.com/ethereum/go-ethereum/p2p/netutil"
-	"github.com/ethereum/go-ethereum/rlp"
 )
 
 const (
@@ -602,7 +601,7 @@ type dialer interface {
 }
 
 func (srv *Server) run(dialstate dialer) {
-	srv.log.Info("Started P2P networking", "self", srv.localnode.Node())
+	srv.log.Info("Started P2P networking", "self", srv.localnode.Node().URLv4())
 	defer srv.loopWG.Done()
 	defer srv.nodedb.Close()
 
@@ -1034,7 +1033,7 @@ func (srv *Server) NodeInfo() *NodeInfo {
 	node := srv.Self()
 	info := &NodeInfo{
 		Name:       srv.Name,
-		Enode:      node.String(),
+		Enode:      node.URLv4(),
 		ID:         node.ID().String(),
 		IP:         node.IP().String(),
 		ListenAddr: srv.ListenAddr,
@@ -1042,9 +1041,7 @@ func (srv *Server) NodeInfo() *NodeInfo {
 	}
 	info.Ports.Discovery = node.UDP()
 	info.Ports.Listener = node.TCP()
-	if enc, err := rlp.EncodeToBytes(node.Record()); err == nil {
-		info.ENR = "0x" + hex.EncodeToString(enc)
-	}
+	info.ENR = node.String()
 
 	// Gather all the running protocol infos (only once per protocol type)
 	for _, proto := range srv.Protocols {
diff --git a/whisper/whisperv6/api.go b/whisper/whisperv6/api.go
index 7609a03c28e4e0c1feafc928556aaf2f0bcc9253..d6d4c8d3ded6afe1f9f3bdb4497d94435861b816 100644
--- a/whisper/whisperv6/api.go
+++ b/whisper/whisperv6/api.go
@@ -103,7 +103,7 @@ func (api *PublicWhisperAPI) SetBloomFilter(ctx context.Context, bloom hexutil.B
 // MarkTrustedPeer marks a peer trusted, which will allow it to send historic (expired) messages.
 // Note: This function is not adding new nodes, the node needs to exists as a peer.
 func (api *PublicWhisperAPI) MarkTrustedPeer(ctx context.Context, url string) (bool, error) {
-	n, err := enode.ParseV4(url)
+	n, err := enode.Parse(enode.ValidSchemes, url)
 	if err != nil {
 		return false, err
 	}
@@ -291,7 +291,7 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (hexutil.
 
 	// send to specific node (skip PoW check)
 	if len(req.TargetPeer) > 0 {
-		n, err := enode.ParseV4(req.TargetPeer)
+		n, err := enode.Parse(enode.ValidSchemes, req.TargetPeer)
 		if err != nil {
 			return nil, fmt.Errorf("failed to parse target peer: %s", err)
 		}