p2p/enode: improve IPv6 support, add ENR text representation (#19663)

* p2p/enr: add entries for for IPv4/IPv6 separation

This adds entry types for "ip6", "udp6", "tcp6" keys. The IP type stays
around because removing it would break a lot of code and force everyone
to care about the distinction.

* p2p/enode: track IPv4 and IPv6 address separately

LocalNode predicts the local node's UDP endpoint and updates the record.
This change makes it predict IPv4 and IPv6 endpoints separately since
they can now be in the record at the same time.

* p2p/enode: implement base64 text format
* all: switch to enode.Parse(...)

This allows passing base64-encoded node records to all the places that
previously accepted enode:// URLs. The URL format is still supported.

* cmd/bootnode, p2p: log node URL instead of ENR

...and return the base64 record in NodeInfo.
This commit is contained in:
Felix Lange 2019-06-07 15:31:00 +02:00 committed by GitHub
parent 896322bf88
commit e83c3ccc47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 464 additions and 220 deletions

View File

@ -143,7 +143,7 @@ func printNotice(nodeKey *ecdsa.PublicKey, addr net.UDPAddr) {
addr.IP = net.IP{127, 0, 0, 1} addr.IP = net.IP{127, 0, 0, 1}
} }
n := enode.NewV4(nodeKey, addr.IP, 0, addr.Port) 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("Note: you're using cmd/bootnode, a developer tool.")
fmt.Println("We recommend using a regular node as bootstrap node for production deployments.") fmt.Println("We recommend using a regular node as bootstrap node for production deployments.")
} }

View File

@ -260,7 +260,7 @@ func newFaucet(genesis *core.Genesis, port int, enodes []*discv5.Node, network u
return nil, err return nil, err
} }
for _, boot := range enodes { for _, boot := range enodes {
old, err := enode.ParseV4(boot.String()) old, err := enode.Parse(enode.ValidSchemes, boot.String())
if err == nil { if err == nil {
stack.Server().AddPeer(old) stack.Server().AddPeer(old)
} }

View File

@ -794,7 +794,7 @@ func setBootstrapNodes(ctx *cli.Context, cfg *p2p.Config) {
cfg.BootstrapNodes = make([]*enode.Node, 0, len(urls)) cfg.BootstrapNodes = make([]*enode.Node, 0, len(urls))
for _, url := range urls { for _, url := range urls {
if url != "" { if url != "" {
node, err := enode.ParseV4(url) node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil { if err != nil {
log.Crit("Bootstrap URL invalid", "enode", url, "err", err) log.Crit("Bootstrap URL invalid", "enode", url, "err", err)
continue continue

View File

@ -203,7 +203,7 @@ func initialize() {
if len(*argEnode) == 0 { if len(*argEnode) == 0 {
argEnode = scanLineA("Please enter the peer's enode: ") argEnode = scanLineA("Please enter the peer's enode: ")
} }
peer := enode.MustParseV4(*argEnode) peer := enode.MustParse(*argEnode)
peers = append(peers, peer) peers = append(peers, peer)
} }
@ -747,9 +747,9 @@ func requestExpiredMessagesLoop() {
} }
func extractIDFromEnode(s string) []byte { func extractIDFromEnode(s string) []byte {
n, err := enode.ParseV4(s) n, err := enode.Parse(enode.ValidSchemes, s)
if err != nil { if err != nil {
utils.Fatalf("Failed to parse enode: %s", err) utils.Fatalf("Failed to parse node: %s", err)
} }
return n.ID().Bytes() return n.ID().Bytes()
} }

View File

@ -505,7 +505,7 @@ func parseTrustedNodes(trustedNodes []string) map[enode.ID]*enode.Node {
nodes := make(map[enode.ID]*enode.Node) nodes := make(map[enode.ID]*enode.Node)
for _, node := range trustedNodes { for _, node := range trustedNodes {
node, err := enode.ParseV4(node) node, err := enode.Parse(enode.ValidSchemes, node)
if err != nil { if err != nil {
log.Warn("Trusted node URL invalid", "enode", node, "err", err) log.Warn("Trusted node URL invalid", "enode", node, "err", err)
continue continue

View File

@ -34,7 +34,7 @@ func newULC(ulcConfig *eth.ULCConfig) *ulc {
} }
m := make(map[string]struct{}, len(ulcConfig.TrustedServers)) m := make(map[string]struct{}, len(ulcConfig.TrustedServers))
for _, id := range ulcConfig.TrustedServers { for _, id := range ulcConfig.TrustedServers {
node, err := enode.ParseV4(id) node, err := enode.Parse(enode.ValidSchemes, id)
if err != nil { if err != nil {
log.Debug("Failed to parse trusted server", "id", id, "err", err) log.Debug("Failed to parse trusted server", "id", id, "err", err)
continue continue

View File

@ -49,7 +49,7 @@ func (api *PrivateAdminAPI) AddPeer(url string) (bool, error) {
return false, ErrNodeStopped return false, ErrNodeStopped
} }
// Try to add the url as a static peer and return // 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 { if err != nil {
return false, fmt.Errorf("invalid enode: %v", err) return false, fmt.Errorf("invalid enode: %v", err)
} }
@ -65,7 +65,7 @@ func (api *PrivateAdminAPI) RemovePeer(url string) (bool, error) {
return false, ErrNodeStopped return false, ErrNodeStopped
} }
// Try to remove the url as a static peer and return // 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 { if err != nil {
return false, fmt.Errorf("invalid enode: %v", err) return false, fmt.Errorf("invalid enode: %v", err)
} }
@ -80,7 +80,7 @@ func (api *PrivateAdminAPI) AddTrustedPeer(url string) (bool, error) {
if server == nil { if server == nil {
return false, ErrNodeStopped return false, ErrNodeStopped
} }
node, err := enode.ParseV4(url) node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid enode: %v", err) return false, fmt.Errorf("invalid enode: %v", err)
} }
@ -96,7 +96,7 @@ func (api *PrivateAdminAPI) RemoveTrustedPeer(url string) (bool, error) {
if server == nil { if server == nil {
return false, ErrNodeStopped return false, ErrNodeStopped
} }
node, err := enode.ParseV4(url) node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid enode: %v", err) return false, fmt.Errorf("invalid enode: %v", err)
} }

View File

@ -425,7 +425,7 @@ func (c *Config) parsePersistentNodes(w *bool, path string) []*enode.Node {
if url == "" { if url == "" {
continue continue
} }
node, err := enode.ParseV4(url) node, err := enode.Parse(enode.ValidSchemes, url)
if err != nil { if err != nil {
log.Error(fmt.Sprintf("Node URL %s: %v\n", url, err)) log.Error(fmt.Sprintf("Node URL %s: %v\n", url, err))
continue continue

View File

@ -342,10 +342,10 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) {
// send the reply as two packets. // send the reply as two packets.
list := []*node{ list := []*node{
wrapNode(enode.MustParseV4("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304")), wrapNode(enode.MustParse("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304")),
wrapNode(enode.MustParseV4("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303")), wrapNode(enode.MustParse("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303")),
wrapNode(enode.MustParseV4("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17")), wrapNode(enode.MustParse("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17")),
wrapNode(enode.MustParseV4("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303")), wrapNode(enode.MustParse("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303")),
} }
rpclist := make([]rpcNode, len(list)) rpclist := make([]rpcNode, len(list))
for i := range list { for i := range list {

View File

@ -48,23 +48,32 @@ type LocalNode struct {
db *DB db *DB
// everything below is protected by a lock // everything below is protected by a lock
mu sync.Mutex mu sync.Mutex
seq uint64 seq uint64
entries map[string]enr.Entry entries map[string]enr.Entry
udpTrack *netutil.IPTracker // predicts external UDP endpoint endpoint4 lnEndpoint
staticIP net.IP endpoint6 lnEndpoint
fallbackIP net.IP }
fallbackUDP int
type lnEndpoint struct {
track *netutil.IPTracker
staticIP, fallbackIP net.IP
fallbackUDP int
} }
// NewLocalNode creates a local node. // NewLocalNode creates a local node.
func NewLocalNode(db *DB, key *ecdsa.PrivateKey) *LocalNode { func NewLocalNode(db *DB, key *ecdsa.PrivateKey) *LocalNode {
ln := &LocalNode{ ln := &LocalNode{
id: PubkeyToIDV4(&key.PublicKey), id: PubkeyToIDV4(&key.PublicKey),
db: db, db: db,
key: key, key: key,
udpTrack: netutil.NewIPTracker(iptrackWindow, iptrackContactWindow, iptrackMinStatements), entries: make(map[string]enr.Entry),
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.seq = db.localSeq(ln.id)
ln.invalidate() ln.invalidate()
@ -89,13 +98,22 @@ func (ln *LocalNode) Node() *Node {
return ln.cur.Load().(*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. // ID returns the local node ID.
func (ln *LocalNode) ID() ID { func (ln *LocalNode) ID() ID {
return ln.id return ln.id
} }
// Set puts the given entry into the local record, overwriting // Set puts the given entry into the local record, overwriting any existing value.
// 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) { func (ln *LocalNode) Set(e enr.Entry) {
ln.mu.Lock() ln.mu.Lock()
defer ln.mu.Unlock() 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. // SetStaticIP sets the local IP to the given one unconditionally.
// This disables endpoint prediction. // This disables endpoint prediction.
func (ln *LocalNode) SetStaticIP(ip net.IP) { func (ln *LocalNode) SetStaticIP(ip net.IP) {
ln.mu.Lock() ln.mu.Lock()
defer ln.mu.Unlock() defer ln.mu.Unlock()
ln.staticIP = ip ln.endpointForIP(ip).staticIP = ip
ln.updateEndpoints() ln.updateEndpoints()
} }
@ -143,17 +168,18 @@ func (ln *LocalNode) SetFallbackIP(ip net.IP) {
ln.mu.Lock() ln.mu.Lock()
defer ln.mu.Unlock() defer ln.mu.Unlock()
ln.fallbackIP = ip ln.endpointForIP(ip).fallbackIP = ip
ln.updateEndpoints() 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. // if no endpoint prediction can be made.
func (ln *LocalNode) SetFallbackUDP(port int) { func (ln *LocalNode) SetFallbackUDP(port int) {
ln.mu.Lock() ln.mu.Lock()
defer ln.mu.Unlock() defer ln.mu.Unlock()
ln.fallbackUDP = port ln.endpoint4.fallbackUDP = port
ln.endpoint6.fallbackUDP = port
ln.updateEndpoints() ln.updateEndpoints()
} }
@ -163,7 +189,7 @@ func (ln *LocalNode) UDPEndpointStatement(fromaddr, endpoint *net.UDPAddr) {
ln.mu.Lock() ln.mu.Lock()
defer ln.mu.Unlock() defer ln.mu.Unlock()
ln.udpTrack.AddStatement(fromaddr.String(), endpoint.String()) ln.endpointForIP(endpoint.IP).track.AddStatement(fromaddr.String(), endpoint.String())
ln.updateEndpoints() ln.updateEndpoints()
} }
@ -173,32 +199,50 @@ func (ln *LocalNode) UDPContact(toaddr *net.UDPAddr) {
ln.mu.Lock() ln.mu.Lock()
defer ln.mu.Unlock() defer ln.mu.Unlock()
ln.udpTrack.AddContact(toaddr.String()) ln.endpointForIP(toaddr.IP).track.AddContact(toaddr.String())
ln.updateEndpoints() ln.updateEndpoints()
} }
// updateEndpoints updates the record with predicted endpoints.
func (ln *LocalNode) updateEndpoints() { func (ln *LocalNode) updateEndpoints() {
// Determine the endpoints. ip4, udp4 := ln.endpoint4.get()
newIP := ln.fallbackIP ip6, udp6 := ln.endpoint6.get()
newUDP := ln.fallbackUDP
if ln.staticIP != nil {
newIP = ln.staticIP
} else if ip, port := predictAddr(ln.udpTrack); ip != nil {
newIP = ip
newUDP = port
}
// Update the record. if ip4 != nil && !ip4.IsUnspecified() {
if newIP != nil && !newIP.IsUnspecified() { ln.set(enr.IPv4(ip4))
ln.set(enr.IP(newIP))
if newUDP != 0 {
ln.set(enr.UDP(newUDP))
} else {
ln.delete(enr.UDP(0))
}
} else { } 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 // predictAddr wraps IPTracker.PredictEndpoint, converting from its string-based

View File

@ -17,10 +17,13 @@
package enode package enode
import ( import (
"math/rand"
"net"
"testing" "testing"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/enr"
"github.com/stretchr/testify/assert"
) )
func newLocalNodeForTesting() (*LocalNode, *DB) { 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) 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())
}

View File

@ -18,6 +18,7 @@ package enode
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"encoding/base64"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
@ -27,8 +28,11 @@ import (
"strings" "strings"
"github.com/ethereum/go-ethereum/p2p/enr" "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. // Node represents a host on the network.
type Node struct { type Node struct {
r enr.Record r enr.Record
@ -48,6 +52,34 @@ func New(validSchemes enr.IdentityScheme, r *enr.Record) (*Node, error) {
return node, nil 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. // ID returns the node identifier.
func (n *Node) ID() ID { func (n *Node) ID() ID {
return n.id return n.id
@ -68,11 +100,19 @@ func (n *Node) Load(k enr.Entry) error {
return n.r.Load(k) 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 { func (n *Node) IP() net.IP {
var ip net.IP var (
n.Load((*enr.IP)(&ip)) ip4 enr.IPv4
return ip 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. // UDP returns the UDP port of the node.
@ -105,10 +145,11 @@ func (n *Node) Record() *enr.Record {
return &cpy 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 { func (n *Node) ValidateComplete() error {
if n.Incomplete() { if n.Incomplete() {
return errors.New("incomplete node") return errors.New("missing IP address")
} }
if n.UDP() == 0 { if n.UDP() == 0 {
return errors.New("missing UDP port") return errors.New("missing UDP port")
@ -122,20 +163,24 @@ func (n *Node) ValidateComplete() error {
return n.Load(&key) return n.Load(&key)
} }
// The string representation of a Node is a URL. // String returns the text representation of the record.
// Please see ParseNode for a description of the format.
func (n *Node) String() string { 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. // MarshalText implements encoding.TextMarshaler.
func (n *Node) MarshalText() ([]byte, error) { func (n *Node) MarshalText() ([]byte, error) {
return []byte(n.v4URL()), nil return []byte(n.String()), nil
} }
// UnmarshalText implements encoding.TextUnmarshaler. // UnmarshalText implements encoding.TextUnmarshaler.
func (n *Node) UnmarshalText(text []byte) error { func (n *Node) UnmarshalText(text []byte) error {
dec, err := ParseV4(string(text)) dec, err := Parse(ValidSchemes, string(text))
if err == nil { if err == nil {
*n = *dec *n = *dec
} }

View File

@ -17,9 +17,12 @@
package enode package enode
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math/big"
"testing" "testing"
"testing/quick"
"github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
@ -43,7 +46,7 @@ func TestPythonInterop(t *testing.T) {
var ( var (
wantID = HexID("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7") wantID = HexID("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7")
wantSeq = uint64(1) wantSeq = uint64(1)
wantIP = enr.IP{127, 0, 0, 1} wantIP = enr.IPv4{127, 0, 0, 1}
wantUDP = enr.UDP(30303) wantUDP = enr.UDP(30303)
) )
if n.Seq() != wantSeq { if n.Seq() != wantSeq {
@ -52,7 +55,7 @@ func TestPythonInterop(t *testing.T) {
if n.ID() != wantID { if n.ID() != wantID {
t.Errorf("wrong id: got %x, want %x", 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 { for k, v := range want {
desc := fmt.Sprintf("loading key %q", k.ENRKey()) desc := fmt.Sprintf("loading key %q", k.ENRKey())
if assert.NoError(t, n.Load(k), desc) { 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")
}
}

View File

@ -70,7 +70,7 @@ func ParseV4(rawurl string) (*Node, error) {
if m := incompleteNodeURL.FindStringSubmatch(rawurl); m != nil { if m := incompleteNodeURL.FindStringSubmatch(rawurl); m != nil {
id, err := parsePubkey(m[1]) id, err := parsePubkey(m[1])
if err != nil { 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 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. // contained in the node has a zero-length signature.
func NewV4(pubkey *ecdsa.PublicKey, ip net.IP, tcp, udp int) *Node { func NewV4(pubkey *ecdsa.PublicKey, ip net.IP, tcp, udp int) *Node {
var r enr.Record var r enr.Record
if ip != nil { if len(ip) > 0 {
r.Set(enr.IP(ip)) r.Set(enr.IP(ip))
} }
if udp != 0 { if udp != 0 {
@ -98,6 +98,12 @@ func NewV4(pubkey *ecdsa.PublicKey, ip net.IP, tcp, udp int) *Node {
return n 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) { func parseComplete(rawurl string) (*Node, error) {
var ( var (
id *ecdsa.PublicKey id *ecdsa.PublicKey
@ -116,7 +122,7 @@ func parseComplete(rawurl string) (*Node, error) {
return nil, errors.New("does not contain node ID") return nil, errors.New("does not contain node ID")
} }
if id, err = parsePubkey(u.User.String()); err != nil { 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. // Parse the IP address.
host, port, err := net.SplitHostPort(u.Host) host, port, err := net.SplitHostPort(u.Host)
@ -126,10 +132,6 @@ func parseComplete(rawurl string) (*Node, error) {
if ip = net.ParseIP(host); ip == nil { if ip = net.ParseIP(host); ip == nil {
return nil, errors.New("invalid IP address") 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. // Parse the port numbers.
if tcpPort, err = strconv.ParseUint(port, 10, 16); err != nil { if tcpPort, err = strconv.ParseUint(port, 10, 16); err != nil {
return nil, errors.New("invalid port") return nil, errors.New("invalid port")
@ -157,7 +159,7 @@ func parsePubkey(in string) (*ecdsa.PublicKey, error) {
return crypto.UnmarshalPubkey(b) return crypto.UnmarshalPubkey(b)
} }
func (n *Node) v4URL() string { func (n *Node) URLv4() string {
var ( var (
scheme enr.ID scheme enr.ID
nodeid string nodeid string

View File

@ -17,44 +17,63 @@
package enode package enode
import ( import (
"bytes"
"crypto/ecdsa" "crypto/ecdsa"
"math/big"
"net" "net"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"testing/quick"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enr"
) )
var parseNodeTests = []struct { var parseNodeTests = []struct {
rawurl string input string
wantError string wantError string
wantResult *Node wantResult *Node
}{ }{
// Records
{ {
rawurl: "http://foobar", input: "enr:-IS4QGrdq0ugARp5T2BZ41TrZOqLc_oKvZoPuZP5--anqWE_J-Tucc1xgkOL7qXl0puJgT7qc2KSvcupc4NCb0nr4tdjgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQM6UUF2Rm-oFe1IH_rQkRCi00T2ybeMHRSvw1HDpRvjPYN1ZHCCdl8",
wantError: `invalid URL scheme, want "enode"`, 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
{
input: "enr:",
wantError: "EOF", // could be nicer
}, },
{ {
rawurl: "enode://01010101@123.124.125.126:3", input: "enr:x",
wantError: `invalid node ID (wrong length, want 128 hex chars)`, 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`, wantError: `invalid IP address`,
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo",
wantError: `invalid port`, 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`, wantError: `invalid discport in query`,
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150",
wantResult: NewV4( wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{0x7f, 0x0, 0x0, 0x1}, net.IP{0x7f, 0x0, 0x0, 0x1},
@ -63,7 +82,7 @@ var parseNodeTests = []struct {
), ),
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150",
wantResult: NewV4( wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.ParseIP("::"), 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( wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.ParseIP("2001:db8:3c4d:15::abcd:ef12"), 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( wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{0x7f, 0x0, 0x0, 0x1}, net.IP{0x7f, 0x0, 0x0, 0x1},
@ -89,16 +108,9 @@ var parseNodeTests = []struct {
22334, 22334,
), ),
}, },
// Incomplete nodes with no address. // Incomplete node URLs with no address
{ {
rawurl: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
nil, 0, 0,
),
},
{
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
wantResult: NewV4( wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
nil, 0, 0, nil, 0, 0,
@ -106,17 +118,32 @@ var parseNodeTests = []struct {
}, },
// Invalid URLs // Invalid URLs
{ {
rawurl: "01010101", input: "",
wantError: `invalid node ID (wrong length, want 128 hex chars)`, wantError: errMissingPrefix.Error(),
}, },
{ {
rawurl: "enode://01010101", input: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
wantError: `invalid node ID (wrong length, want 128 hex chars)`, wantError: errMissingPrefix.Error(),
}, },
{ {
// This test checks that errors from url.Parse are handled. input: "01010101",
rawurl: "://foo", wantError: errMissingPrefix.Error(),
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) { func TestParseNode(t *testing.T) {
for _, test := range parseNodeTests { for _, test := range parseNodeTests {
n, err := ParseV4(test.rawurl) n, err := Parse(ValidSchemes, test.input)
if test.wantError != "" { if test.wantError != "" {
if err == nil { 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 continue
} else if err.Error() != test.wantError { } 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 continue
} }
} else { } else {
if err != nil { 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 continue
} }
if !reflect.DeepEqual(n, test.wantResult) { 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) { func TestNodeString(t *testing.T) {
for i, test := range parseNodeTests { 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() str := test.wantResult.String()
if str != test.rawurl { if str != test.input {
t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl) 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")
}
}

View File

@ -163,6 +163,16 @@ func (r *Record) invalidate() {
r.raw = nil 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 // EncodeRLP implements rlp.Encoder. Encoding fails if
// the record is unsigned. // the record is unsigned.
func (r Record) EncodeRLP(w io.Writer) error { func (r Record) EncodeRLP(w io.Writer) error {
@ -173,7 +183,7 @@ func (r Record) EncodeRLP(w io.Writer) error {
return err 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 { func (r *Record) DecodeRLP(s *rlp.Stream) error {
dec, raw, err := decodeRecord(s) dec, raw, err := decodeRecord(s)
if err != nil { if err != nil {

View File

@ -49,23 +49,23 @@ func TestGetSetID(t *testing.T) {
} }
// TestGetSetIP4 tests encoding/decoding and setting/getting of the IP key. // TestGetSetIP4 tests encoding/decoding and setting/getting of the IP key.
func TestGetSetIP4(t *testing.T) { func TestGetSetIPv4(t *testing.T) {
ip := IP{192, 168, 0, 3} ip := IPv4{192, 168, 0, 3}
var r Record var r Record
r.Set(ip) r.Set(ip)
var ip2 IP var ip2 IPv4
require.NoError(t, r.Load(&ip2)) require.NoError(t, r.Load(&ip2))
assert.Equal(t, ip, ip2) assert.Equal(t, ip, ip2)
} }
// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP key. // TestGetSetIP6 tests encoding/decoding and setting/getting of the IP6 key.
func TestGetSetIP6(t *testing.T) { func TestGetSetIPv6(t *testing.T) {
ip := IP{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68} ip := IPv6{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}
var r Record var r Record
r.Set(ip) r.Set(ip)
var ip2 IP var ip2 IPv6
require.NoError(t, r.Load(&ip2)) require.NoError(t, r.Load(&ip2))
assert.Equal(t, ip, ip2) assert.Equal(t, ip, ip2)
} }
@ -83,7 +83,7 @@ func TestGetSetUDP(t *testing.T) {
func TestLoadErrors(t *testing.T) { func TestLoadErrors(t *testing.T) {
var r Record var r Record
ip4 := IP{127, 0, 0, 1} ip4 := IPv4{127, 0, 0, 1}
r.Set(ip4) r.Set(ip4)
// Check error for missing keys. // Check error for missing keys.
@ -185,13 +185,13 @@ func TestSeq(t *testing.T) {
func TestGetSetOverwrite(t *testing.T) { func TestGetSetOverwrite(t *testing.T) {
var r Record var r Record
ip := IP{192, 168, 0, 3} ip := IPv4{192, 168, 0, 3}
r.Set(ip) r.Set(ip)
ip2 := IP{192, 168, 0, 4} ip2 := IPv4{192, 168, 0, 4}
r.Set(ip2) r.Set(ip2)
var ip3 IP var ip3 IPv4
require.NoError(t, r.Load(&ip3)) require.NoError(t, r.Load(&ip3))
assert.Equal(t, ip2, ip3) assert.Equal(t, ip2, ip3)
} }
@ -200,7 +200,7 @@ func TestGetSetOverwrite(t *testing.T) {
func TestSignEncodeAndDecode(t *testing.T) { func TestSignEncodeAndDecode(t *testing.T) {
var r Record var r Record
r.Set(UDP(30303)) 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)) require.NoError(t, signTest([]byte{5}, &r))
blob, err := rlp.EncodeToBytes(r) blob, err := rlp.EncodeToBytes(r)

View File

@ -60,11 +60,21 @@ type TCP uint16
func (v TCP) ENRKey() string { return "tcp" } 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. // UDP is the "udp" key, which holds the UDP port of the node.
type UDP uint16 type UDP uint16
func (v UDP) ENRKey() string { return "udp" } 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. // ID is the "id" key, which holds the name of the identity scheme.
type ID string type ID string
@ -72,17 +82,27 @@ const IDv4 = ID("v4") // the default identity scheme
func (v ID) ENRKey() string { return "id" } 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 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. // EncodeRLP implements rlp.Encoder.
func (v IP) EncodeRLP(w io.Writer) error { func (v IP) EncodeRLP(w io.Writer) error {
if ip4 := net.IP(v).To4(); ip4 != nil { if ip4 := net.IP(v).To4(); ip4 != nil {
return rlp.Encode(w, ip4) 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. // DecodeRLP implements rlp.Decoder.
@ -96,6 +116,56 @@ func (v *IP) DecodeRLP(s *rlp.Stream) error {
return nil 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. // KeyError is an error related to a key.
type KeyError struct { type KeyError struct {
Key string Key string

View File

@ -39,7 +39,6 @@ import (
"github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/p2p/nat"
"github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/p2p/netutil"
"github.com/ethereum/go-ethereum/rlp"
) )
const ( const (
@ -602,7 +601,7 @@ type dialer interface {
} }
func (srv *Server) run(dialstate dialer) { 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.loopWG.Done()
defer srv.nodedb.Close() defer srv.nodedb.Close()
@ -1034,7 +1033,7 @@ func (srv *Server) NodeInfo() *NodeInfo {
node := srv.Self() node := srv.Self()
info := &NodeInfo{ info := &NodeInfo{
Name: srv.Name, Name: srv.Name,
Enode: node.String(), Enode: node.URLv4(),
ID: node.ID().String(), ID: node.ID().String(),
IP: node.IP().String(), IP: node.IP().String(),
ListenAddr: srv.ListenAddr, ListenAddr: srv.ListenAddr,
@ -1042,9 +1041,7 @@ func (srv *Server) NodeInfo() *NodeInfo {
} }
info.Ports.Discovery = node.UDP() info.Ports.Discovery = node.UDP()
info.Ports.Listener = node.TCP() info.Ports.Listener = node.TCP()
if enc, err := rlp.EncodeToBytes(node.Record()); err == nil { info.ENR = node.String()
info.ENR = "0x" + hex.EncodeToString(enc)
}
// Gather all the running protocol infos (only once per protocol type) // Gather all the running protocol infos (only once per protocol type)
for _, proto := range srv.Protocols { for _, proto := range srv.Protocols {

View File

@ -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. // 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. // 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) { 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 { if err != nil {
return false, err return false, err
} }
@ -291,7 +291,7 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (hexutil.
// send to specific node (skip PoW check) // send to specific node (skip PoW check)
if len(req.TargetPeer) > 0 { if len(req.TargetPeer) > 0 {
n, err := enode.ParseV4(req.TargetPeer) n, err := enode.Parse(enode.ValidSchemes, req.TargetPeer)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse target peer: %s", err) return nil, fmt.Errorf("failed to parse target peer: %s", err)
} }