aboutsummaryrefslogtreecommitdiffstats
path: root/p2p/enode
diff options
context:
space:
mode:
Diffstat (limited to 'p2p/enode')
-rw-r--r--p2p/enode/idscheme.go160
-rw-r--r--p2p/enode/idscheme_test.go74
-rw-r--r--p2p/enode/node.go248
-rw-r--r--p2p/enode/node_test.go62
-rw-r--r--p2p/enode/nodedb.go349
-rw-r--r--p2p/enode/nodedb_test.go372
-rw-r--r--p2p/enode/urlv4.go194
-rw-r--r--p2p/enode/urlv4_test.go243
8 files changed, 1702 insertions, 0 deletions
diff --git a/p2p/enode/idscheme.go b/p2p/enode/idscheme.go
new file mode 100644
index 000000000..9b495fd4f
--- /dev/null
+++ b/p2p/enode/idscheme.go
@@ -0,0 +1,160 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package enode
+
+import (
+ "crypto/ecdsa"
+ "fmt"
+ "io"
+
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/crypto/sha3"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// List of known secure identity schemes.
+var ValidSchemes = enr.SchemeMap{
+ "v4": V4ID{},
+}
+
+var ValidSchemesForTesting = enr.SchemeMap{
+ "v4": V4ID{},
+ "null": NullID{},
+}
+
+// v4ID is the "v4" identity scheme.
+type V4ID struct{}
+
+// SignV4 signs a record using the v4 scheme.
+func SignV4(r *enr.Record, privkey *ecdsa.PrivateKey) error {
+ // Copy r to avoid modifying it if signing fails.
+ cpy := *r
+ cpy.Set(enr.ID("v4"))
+ cpy.Set(Secp256k1(privkey.PublicKey))
+
+ h := sha3.NewKeccak256()
+ rlp.Encode(h, cpy.AppendElements(nil))
+ sig, err := crypto.Sign(h.Sum(nil), privkey)
+ if err != nil {
+ return err
+ }
+ sig = sig[:len(sig)-1] // remove v
+ if err = cpy.SetSig(V4ID{}, sig); err == nil {
+ *r = cpy
+ }
+ return err
+}
+
+func (V4ID) Verify(r *enr.Record, sig []byte) error {
+ var entry s256raw
+ if err := r.Load(&entry); err != nil {
+ return err
+ } else if len(entry) != 33 {
+ return fmt.Errorf("invalid public key")
+ }
+
+ h := sha3.NewKeccak256()
+ rlp.Encode(h, r.AppendElements(nil))
+ if !crypto.VerifySignature(entry, h.Sum(nil), sig) {
+ return enr.ErrInvalidSig
+ }
+ return nil
+}
+
+func (V4ID) NodeAddr(r *enr.Record) []byte {
+ var pubkey Secp256k1
+ err := r.Load(&pubkey)
+ if err != nil {
+ return nil
+ }
+ buf := make([]byte, 64)
+ math.ReadBits(pubkey.X, buf[:32])
+ math.ReadBits(pubkey.Y, buf[32:])
+ return crypto.Keccak256(buf)
+}
+
+// Secp256k1 is the "secp256k1" key, which holds a public key.
+type Secp256k1 ecdsa.PublicKey
+
+func (v Secp256k1) ENRKey() string { return "secp256k1" }
+
+// EncodeRLP implements rlp.Encoder.
+func (v Secp256k1) EncodeRLP(w io.Writer) error {
+ return rlp.Encode(w, crypto.CompressPubkey((*ecdsa.PublicKey)(&v)))
+}
+
+// DecodeRLP implements rlp.Decoder.
+func (v *Secp256k1) DecodeRLP(s *rlp.Stream) error {
+ buf, err := s.Bytes()
+ if err != nil {
+ return err
+ }
+ pk, err := crypto.DecompressPubkey(buf)
+ if err != nil {
+ return err
+ }
+ *v = (Secp256k1)(*pk)
+ return nil
+}
+
+// s256raw is an unparsed secp256k1 public key entry.
+type s256raw []byte
+
+func (s256raw) ENRKey() string { return "secp256k1" }
+
+// v4CompatID is a weaker and insecure version of the "v4" scheme which only checks for the
+// presence of a secp256k1 public key, but doesn't verify the signature.
+type v4CompatID struct {
+ V4ID
+}
+
+func (v4CompatID) Verify(r *enr.Record, sig []byte) error {
+ var pubkey Secp256k1
+ return r.Load(&pubkey)
+}
+
+func signV4Compat(r *enr.Record, pubkey *ecdsa.PublicKey) {
+ r.Set((*Secp256k1)(pubkey))
+ if err := r.SetSig(v4CompatID{}, []byte{}); err != nil {
+ panic(err)
+ }
+}
+
+// NullID is the "null" ENR identity scheme. This scheme stores the node
+// ID in the record without any signature.
+type NullID struct{}
+
+func (NullID) Verify(r *enr.Record, sig []byte) error {
+ return nil
+}
+
+func (NullID) NodeAddr(r *enr.Record) []byte {
+ var id ID
+ r.Load(enr.WithEntry("nulladdr", &id))
+ return id[:]
+}
+
+func SignNull(r *enr.Record, id ID) *Node {
+ r.Set(enr.ID("null"))
+ r.Set(enr.WithEntry("nulladdr", id))
+ if err := r.SetSig(NullID{}, []byte{}); err != nil {
+ panic(err)
+ }
+ return &Node{r: *r, id: id}
+}
diff --git a/p2p/enode/idscheme_test.go b/p2p/enode/idscheme_test.go
new file mode 100644
index 000000000..0910e6e83
--- /dev/null
+++ b/p2p/enode/idscheme_test.go
@@ -0,0 +1,74 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package enode
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "encoding/hex"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ privkey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ pubkey = &privkey.PublicKey
+)
+
+func TestEmptyNodeID(t *testing.T) {
+ var r enr.Record
+ if addr := ValidSchemes.NodeAddr(&r); addr != nil {
+ t.Errorf("wrong address on empty record: got %v, want %v", addr, nil)
+ }
+
+ require.NoError(t, SignV4(&r, privkey))
+ expected := "a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7"
+ assert.Equal(t, expected, hex.EncodeToString(ValidSchemes.NodeAddr(&r)))
+}
+
+// Checks that failure to sign leaves the record unmodified.
+func TestSignError(t *testing.T) {
+ invalidKey := &ecdsa.PrivateKey{D: new(big.Int), PublicKey: *pubkey}
+
+ var r enr.Record
+ emptyEnc, _ := rlp.EncodeToBytes(&r)
+ if err := SignV4(&r, invalidKey); err == nil {
+ t.Fatal("expected error from SignV4")
+ }
+ newEnc, _ := rlp.EncodeToBytes(&r)
+ if !bytes.Equal(newEnc, emptyEnc) {
+ t.Fatal("record modified even though signing failed")
+ }
+}
+
+// TestGetSetSecp256k1 tests encoding/decoding and setting/getting of the Secp256k1 key.
+func TestGetSetSecp256k1(t *testing.T) {
+ var r enr.Record
+ if err := SignV4(&r, privkey); err != nil {
+ t.Fatal(err)
+ }
+
+ var pk Secp256k1
+ require.NoError(t, r.Load(&pk))
+ assert.EqualValues(t, pubkey, &pk)
+}
diff --git a/p2p/enode/node.go b/p2p/enode/node.go
new file mode 100644
index 000000000..84088fcd2
--- /dev/null
+++ b/p2p/enode/node.go
@@ -0,0 +1,248 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package enode
+
+import (
+ "crypto/ecdsa"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "math/bits"
+ "math/rand"
+ "net"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/p2p/enr"
+)
+
+// Node represents a host on the network.
+type Node struct {
+ r enr.Record
+ id ID
+}
+
+// New wraps a node record. The record must be valid according to the given
+// identity scheme.
+func New(validSchemes enr.IdentityScheme, r *enr.Record) (*Node, error) {
+ if err := r.VerifySignature(validSchemes); err != nil {
+ return nil, err
+ }
+ node := &Node{r: *r}
+ if n := copy(node.id[:], validSchemes.NodeAddr(&node.r)); n != len(ID{}) {
+ return nil, fmt.Errorf("invalid node ID length %d, need %d", n, len(ID{}))
+ }
+ return node, nil
+}
+
+// ID returns the node identifier.
+func (n *Node) ID() ID {
+ return n.id
+}
+
+// Seq returns the sequence number of the underlying record.
+func (n *Node) Seq() uint64 {
+ return n.r.Seq()
+}
+
+// Incomplete returns true for nodes with no IP address.
+func (n *Node) Incomplete() bool {
+ return n.IP() == nil
+}
+
+// Load retrieves an entry from the underlying record.
+func (n *Node) Load(k enr.Entry) error {
+ return n.r.Load(k)
+}
+
+// IP returns the IP address of the node.
+func (n *Node) IP() net.IP {
+ var ip net.IP
+ n.Load((*enr.IP)(&ip))
+ return ip
+}
+
+// UDP returns the UDP port of the node.
+func (n *Node) UDP() int {
+ var port enr.UDP
+ n.Load(&port)
+ return int(port)
+}
+
+// UDP returns the TCP port of the node.
+func (n *Node) TCP() int {
+ var port enr.TCP
+ n.Load(&port)
+ return int(port)
+}
+
+// Pubkey returns the secp256k1 public key of the node, if present.
+func (n *Node) Pubkey() *ecdsa.PublicKey {
+ var key ecdsa.PublicKey
+ if n.Load((*Secp256k1)(&key)) != nil {
+ return nil
+ }
+ return &key
+}
+
+// checks whether n is a valid complete node.
+func (n *Node) ValidateComplete() error {
+ if n.Incomplete() {
+ return errors.New("incomplete node")
+ }
+ if n.UDP() == 0 {
+ return errors.New("missing UDP port")
+ }
+ ip := n.IP()
+ if ip.IsMulticast() || ip.IsUnspecified() {
+ return errors.New("invalid IP (multicast/unspecified)")
+ }
+ // Validate the node key (on curve, etc.).
+ var key Secp256k1
+ return n.Load(&key)
+}
+
+// The string representation of a Node is a URL.
+// Please see ParseNode for a description of the format.
+func (n *Node) String() string {
+ return n.v4URL()
+}
+
+// MarshalText implements encoding.TextMarshaler.
+func (n *Node) MarshalText() ([]byte, error) {
+ return []byte(n.v4URL()), nil
+}
+
+// UnmarshalText implements encoding.TextUnmarshaler.
+func (n *Node) UnmarshalText(text []byte) error {
+ dec, err := ParseV4(string(text))
+ if err == nil {
+ *n = *dec
+ }
+ return err
+}
+
+// ID is a unique identifier for each node.
+type ID [32]byte
+
+// Bytes returns a byte slice representation of the ID
+func (n ID) Bytes() []byte {
+ return n[:]
+}
+
+// ID prints as a long hexadecimal number.
+func (n ID) String() string {
+ return fmt.Sprintf("%x", n[:])
+}
+
+// The Go syntax representation of a ID is a call to HexID.
+func (n ID) GoString() string {
+ return fmt.Sprintf("enode.HexID(\"%x\")", n[:])
+}
+
+// TerminalString returns a shortened hex string for terminal logging.
+func (n ID) TerminalString() string {
+ return hex.EncodeToString(n[:8])
+}
+
+// MarshalText implements the encoding.TextMarshaler interface.
+func (n ID) MarshalText() ([]byte, error) {
+ return []byte(hex.EncodeToString(n[:])), nil
+}
+
+// UnmarshalText implements the encoding.TextUnmarshaler interface.
+func (n *ID) UnmarshalText(text []byte) error {
+ id, err := parseID(string(text))
+ if err != nil {
+ return err
+ }
+ *n = id
+ return nil
+}
+
+// HexID converts a hex string to an ID.
+// The string may be prefixed with 0x.
+// It panics if the string is not a valid ID.
+func HexID(in string) ID {
+ id, err := parseID(in)
+ if err != nil {
+ panic(err)
+ }
+ return id
+}
+
+func parseID(in string) (ID, error) {
+ var id ID
+ b, err := hex.DecodeString(strings.TrimPrefix(in, "0x"))
+ if err != nil {
+ return id, err
+ } else if len(b) != len(id) {
+ return id, fmt.Errorf("wrong length, want %d hex chars", len(id)*2)
+ }
+ copy(id[:], b)
+ return id, nil
+}
+
+// DistCmp compares the distances a->target and b->target.
+// Returns -1 if a is closer to target, 1 if b is closer to target
+// and 0 if they are equal.
+func DistCmp(target, a, b ID) int {
+ for i := range target {
+ da := a[i] ^ target[i]
+ db := b[i] ^ target[i]
+ if da > db {
+ return 1
+ } else if da < db {
+ return -1
+ }
+ }
+ return 0
+}
+
+// LogDist returns the logarithmic distance between a and b, log2(a ^ b).
+func LogDist(a, b ID) int {
+ lz := 0
+ for i := range a {
+ x := a[i] ^ b[i]
+ if x == 0 {
+ lz += 8
+ } else {
+ lz += bits.LeadingZeros8(x)
+ break
+ }
+ }
+ return len(a)*8 - lz
+}
+
+// RandomID returns a random ID b such that logdist(a, b) == n.
+func RandomID(a ID, n int) (b ID) {
+ if n == 0 {
+ return a
+ }
+ // flip bit at position n, fill the rest with random bits
+ b = a
+ pos := len(a) - n/8 - 1
+ bit := byte(0x01) << (byte(n%8) - 1)
+ if bit == 0 {
+ pos++
+ bit = 0x80
+ }
+ b[pos] = a[pos]&^bit | ^a[pos]&bit // TODO: randomize end bits
+ for i := pos + 1; i < len(a); i++ {
+ b[i] = byte(rand.Intn(255))
+ }
+ return b
+}
diff --git a/p2p/enode/node_test.go b/p2p/enode/node_test.go
new file mode 100644
index 000000000..861a70bd6
--- /dev/null
+++ b/p2p/enode/node_test.go
@@ -0,0 +1,62 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package enode
+
+import (
+ "encoding/hex"
+ "fmt"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/stretchr/testify/assert"
+)
+
+var pyRecord, _ = hex.DecodeString("f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f")
+
+// TestPythonInterop checks that we can decode and verify a record produced by the Python
+// implementation.
+func TestPythonInterop(t *testing.T) {
+ var r enr.Record
+ if err := rlp.DecodeBytes(pyRecord, &r); err != nil {
+ t.Fatalf("can't decode: %v", err)
+ }
+ n, err := New(ValidSchemes, &r)
+ if err != nil {
+ t.Fatalf("can't verify record: %v", err)
+ }
+
+ var (
+ wantID = HexID("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7")
+ wantSeq = uint64(1)
+ wantIP = enr.IP{127, 0, 0, 1}
+ wantUDP = enr.UDP(30303)
+ )
+ if n.Seq() != wantSeq {
+ t.Errorf("wrong seq: got %d, want %d", n.Seq(), wantSeq)
+ }
+ 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}
+ for k, v := range want {
+ desc := fmt.Sprintf("loading key %q", k.ENRKey())
+ if assert.NoError(t, n.Load(k), desc) {
+ assert.Equal(t, k, v, desc)
+ }
+ }
+}
diff --git a/p2p/enode/nodedb.go b/p2p/enode/nodedb.go
new file mode 100644
index 000000000..a929b75d7
--- /dev/null
+++ b/p2p/enode/nodedb.go
@@ -0,0 +1,349 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package enode
+
+import (
+ "bytes"
+ "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/syndtr/goleveldb/leveldb"
+ "github.com/syndtr/goleveldb/leveldb/errors"
+ "github.com/syndtr/goleveldb/leveldb/iterator"
+ "github.com/syndtr/goleveldb/leveldb/opt"
+ "github.com/syndtr/goleveldb/leveldb/storage"
+ "github.com/syndtr/goleveldb/leveldb/util"
+)
+
+var (
+ nodeDBNilID = ID{} // Special node ID to use as a nil element.
+ nodeDBNodeExpiration = 24 * time.Hour // Time after which an unseen node should be dropped.
+ nodeDBCleanupCycle = time.Hour // Time period for running the expiration task.
+ nodeDBVersion = 6
+)
+
+// DB is the node database, storing previously seen nodes and any collected metadata about
+// them for QoS purposes.
+type DB struct {
+ lvl *leveldb.DB // Interface to the database itself
+ runner sync.Once // Ensures we can start at most one expirer
+ quit chan struct{} // Channel to signal the expiring thread to stop
+}
+
+// Schema layout for the node database
+var (
+ nodeDBVersionKey = []byte("version") // Version of the database to flush if changes
+ nodeDBItemPrefix = []byte("n:") // Identifier to prefix node entries with
+
+ nodeDBDiscoverRoot = ":discover"
+ nodeDBDiscoverPing = nodeDBDiscoverRoot + ":lastping"
+ nodeDBDiscoverPong = nodeDBDiscoverRoot + ":lastpong"
+ nodeDBDiscoverFindFails = nodeDBDiscoverRoot + ":findfail"
+)
+
+// OpenDB opens a node database for storing and retrieving infos about known peers in the
+// network. If no path is given an in-memory, temporary database is constructed.
+func OpenDB(path string) (*DB, error) {
+ if path == "" {
+ return newMemoryDB()
+ }
+ return newPersistentDB(path)
+}
+
+// newMemoryNodeDB creates a new in-memory node database without a persistent backend.
+func newMemoryDB() (*DB, error) {
+ db, err := leveldb.Open(storage.NewMemStorage(), nil)
+ if err != nil {
+ return nil, err
+ }
+ return &DB{lvl: db, quit: make(chan struct{})}, nil
+}
+
+// newPersistentNodeDB creates/opens a leveldb backed persistent node database,
+// also flushing its contents in case of a version mismatch.
+func newPersistentDB(path string) (*DB, error) {
+ opts := &opt.Options{OpenFilesCacheCapacity: 5}
+ db, err := leveldb.OpenFile(path, opts)
+ if _, iscorrupted := err.(*errors.ErrCorrupted); iscorrupted {
+ db, err = leveldb.RecoverFile(path, nil)
+ }
+ if err != nil {
+ return nil, err
+ }
+ // The nodes contained in the cache correspond to a certain protocol version.
+ // Flush all nodes if the version doesn't match.
+ currentVer := make([]byte, binary.MaxVarintLen64)
+ currentVer = currentVer[:binary.PutVarint(currentVer, int64(nodeDBVersion))]
+
+ blob, err := db.Get(nodeDBVersionKey, nil)
+ switch err {
+ case leveldb.ErrNotFound:
+ // Version not found (i.e. empty cache), insert it
+ if err := db.Put(nodeDBVersionKey, currentVer, nil); err != nil {
+ db.Close()
+ return nil, err
+ }
+
+ case nil:
+ // Version present, flush if different
+ if !bytes.Equal(blob, currentVer) {
+ db.Close()
+ if err = os.RemoveAll(path); err != nil {
+ return nil, err
+ }
+ return newPersistentDB(path)
+ }
+ }
+ return &DB{lvl: db, quit: make(chan struct{})}, nil
+}
+
+// makeKey generates the leveldb key-blob from a node id and its particular
+// field of interest.
+func makeKey(id ID, field string) []byte {
+ if bytes.Equal(id[:], nodeDBNilID[:]) {
+ return []byte(field)
+ }
+ return append(nodeDBItemPrefix, append(id[:], field...)...)
+}
+
+// splitKey tries to split a database key into a node id and a field part.
+func splitKey(key []byte) (id ID, field string) {
+ // If the key is not of a node, return it plainly
+ if !bytes.HasPrefix(key, nodeDBItemPrefix) {
+ return ID{}, string(key)
+ }
+ // Otherwise split the id and field
+ item := key[len(nodeDBItemPrefix):]
+ copy(id[:], item[:len(id)])
+ field = string(item[len(id):])
+
+ return id, field
+}
+
+// fetchInt64 retrieves an integer instance associated with a particular
+// database key.
+func (db *DB) fetchInt64(key []byte) int64 {
+ blob, err := db.lvl.Get(key, nil)
+ if err != nil {
+ return 0
+ }
+ val, read := binary.Varint(blob)
+ if read <= 0 {
+ return 0
+ }
+ return val
+}
+
+// storeInt64 update a specific database entry to the current time instance as a
+// unix timestamp.
+func (db *DB) storeInt64(key []byte, n int64) error {
+ blob := make([]byte, binary.MaxVarintLen64)
+ blob = blob[:binary.PutVarint(blob, n)]
+
+ return db.lvl.Put(key, blob, nil)
+}
+
+// Node retrieves a node with a given id from the database.
+func (db *DB) Node(id ID) *Node {
+ blob, err := db.lvl.Get(makeKey(id, nodeDBDiscoverRoot), nil)
+ if err != nil {
+ return nil
+ }
+ return mustDecodeNode(id[:], blob)
+}
+
+func mustDecodeNode(id, data []byte) *Node {
+ node := new(Node)
+ if err := rlp.DecodeBytes(data, &node.r); err != nil {
+ panic(fmt.Errorf("p2p/enode: can't decode node %x in DB: %v", id, err))
+ }
+ // Restore node id cache.
+ copy(node.id[:], id)
+ return node
+}
+
+// UpdateNode inserts - potentially overwriting - a node into the peer database.
+func (db *DB) UpdateNode(node *Node) error {
+ blob, err := rlp.EncodeToBytes(&node.r)
+ if err != nil {
+ return err
+ }
+ return db.lvl.Put(makeKey(node.ID(), nodeDBDiscoverRoot), blob, nil)
+}
+
+// DeleteNode deletes all information/keys associated with a node.
+func (db *DB) DeleteNode(id ID) error {
+ deleter := db.lvl.NewIterator(util.BytesPrefix(makeKey(id, "")), nil)
+ for deleter.Next() {
+ if err := db.lvl.Delete(deleter.Key(), nil); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// ensureExpirer is a small helper method ensuring that the data expiration
+// mechanism is running. If the expiration goroutine is already running, this
+// method simply returns.
+//
+// The goal is to start the data evacuation only after the network successfully
+// bootstrapped itself (to prevent dumping potentially useful seed nodes). Since
+// it would require significant overhead to exactly trace the first successful
+// convergence, it's simpler to "ensure" the correct state when an appropriate
+// condition occurs (i.e. a successful bonding), and discard further events.
+func (db *DB) ensureExpirer() {
+ db.runner.Do(func() { go db.expirer() })
+}
+
+// expirer should be started in a go routine, and is responsible for looping ad
+// infinitum and dropping stale data from the database.
+func (db *DB) expirer() {
+ tick := time.NewTicker(nodeDBCleanupCycle)
+ defer tick.Stop()
+ for {
+ select {
+ case <-tick.C:
+ if err := db.expireNodes(); err != nil {
+ log.Error("Failed to expire nodedb items", "err", err)
+ }
+ case <-db.quit:
+ return
+ }
+ }
+}
+
+// expireNodes iterates over the database and deletes all nodes that have not
+// been seen (i.e. received a pong from) for some allotted time.
+func (db *DB) expireNodes() error {
+ threshold := time.Now().Add(-nodeDBNodeExpiration)
+
+ // Find discovered nodes that are older than the allowance
+ it := db.lvl.NewIterator(nil, nil)
+ defer it.Release()
+
+ for it.Next() {
+ // Skip the item if not a discovery node
+ id, field := splitKey(it.Key())
+ if field != nodeDBDiscoverRoot {
+ continue
+ }
+ // Skip the node if not expired yet (and not self)
+ if seen := db.LastPongReceived(id); seen.After(threshold) {
+ continue
+ }
+ // Otherwise delete all associated information
+ db.DeleteNode(id)
+ }
+ return nil
+}
+
+// LastPingReceived retrieves the time of the last ping packet received from
+// a remote node.
+func (db *DB) LastPingReceived(id ID) time.Time {
+ return time.Unix(db.fetchInt64(makeKey(id, nodeDBDiscoverPing)), 0)
+}
+
+// UpdateLastPingReceived updates the last time we tried contacting a remote node.
+func (db *DB) UpdateLastPingReceived(id ID, instance time.Time) error {
+ return db.storeInt64(makeKey(id, nodeDBDiscoverPing), instance.Unix())
+}
+
+// LastPongReceived retrieves the time of the last successful pong from remote node.
+func (db *DB) LastPongReceived(id ID) time.Time {
+ // Launch expirer
+ db.ensureExpirer()
+ return time.Unix(db.fetchInt64(makeKey(id, nodeDBDiscoverPong)), 0)
+}
+
+// UpdateLastPongReceived updates the last pong time of a node.
+func (db *DB) UpdateLastPongReceived(id ID, instance time.Time) error {
+ return db.storeInt64(makeKey(id, nodeDBDiscoverPong), instance.Unix())
+}
+
+// FindFails retrieves the number of findnode failures since bonding.
+func (db *DB) FindFails(id ID) int {
+ return int(db.fetchInt64(makeKey(id, nodeDBDiscoverFindFails)))
+}
+
+// UpdateFindFails updates the number of findnode failures since bonding.
+func (db *DB) UpdateFindFails(id ID, fails int) error {
+ return db.storeInt64(makeKey(id, nodeDBDiscoverFindFails), int64(fails))
+}
+
+// QuerySeeds retrieves random nodes to be used as potential seed nodes
+// for bootstrapping.
+func (db *DB) QuerySeeds(n int, maxAge time.Duration) []*Node {
+ var (
+ now = time.Now()
+ nodes = make([]*Node, 0, n)
+ it = db.lvl.NewIterator(nil, nil)
+ id ID
+ )
+ defer it.Release()
+
+seek:
+ for seeks := 0; len(nodes) < n && seeks < n*5; seeks++ {
+ // Seek to a random entry. The first byte is incremented by a
+ // random amount each time in order to increase the likelihood
+ // of hitting all existing nodes in very small databases.
+ ctr := id[0]
+ rand.Read(id[:])
+ id[0] = ctr + id[0]%16
+ it.Seek(makeKey(id, nodeDBDiscoverRoot))
+
+ n := nextNode(it)
+ if n == nil {
+ id[0] = 0
+ continue seek // iterator exhausted
+ }
+ if now.Sub(db.LastPongReceived(n.ID())) > maxAge {
+ continue seek
+ }
+ for i := range nodes {
+ if nodes[i].ID() == n.ID() {
+ continue seek // duplicate
+ }
+ }
+ nodes = append(nodes, n)
+ }
+ return nodes
+}
+
+// reads the next node record from the iterator, skipping over other
+// database entries.
+func nextNode(it iterator.Iterator) *Node {
+ for end := false; !end; end = !it.Next() {
+ id, field := splitKey(it.Key())
+ if field != nodeDBDiscoverRoot {
+ continue
+ }
+ return mustDecodeNode(id[:], it.Value())
+ }
+ return nil
+}
+
+// close flushes and closes the database files.
+func (db *DB) Close() {
+ close(db.quit)
+ db.lvl.Close()
+}
diff --git a/p2p/enode/nodedb_test.go b/p2p/enode/nodedb_test.go
new file mode 100644
index 000000000..57a0424d8
--- /dev/null
+++ b/p2p/enode/nodedb_test.go
@@ -0,0 +1,372 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package enode
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+ "time"
+)
+
+var nodeDBKeyTests = []struct {
+ id ID
+ field string
+ key []byte
+}{
+ {
+ id: ID{},
+ field: "version",
+ key: []byte{0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e}, // field
+ },
+ {
+ id: HexID("51232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ field: ":discover",
+ key: []byte{
+ 0x6e, 0x3a, // prefix
+ 0x51, 0x23, 0x2b, 0x8d, 0x78, 0x21, 0x61, 0x7d, // node id
+ 0x2b, 0x29, 0xb5, 0x4b, 0x81, 0xcd, 0xef, 0xb9, //
+ 0xb3, 0xe9, 0xc3, 0x7d, 0x7f, 0xd5, 0xf6, 0x32, //
+ 0x70, 0xbc, 0xc9, 0xe1, 0xa6, 0xf6, 0xa4, 0x39, //
+ 0x3a, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, // field
+ },
+ },
+}
+
+func TestDBKeys(t *testing.T) {
+ for i, tt := range nodeDBKeyTests {
+ if key := makeKey(tt.id, tt.field); !bytes.Equal(key, tt.key) {
+ t.Errorf("make test %d: key mismatch: have 0x%x, want 0x%x", i, key, tt.key)
+ }
+ id, field := splitKey(tt.key)
+ if !bytes.Equal(id[:], tt.id[:]) {
+ t.Errorf("split test %d: id mismatch: have 0x%x, want 0x%x", i, id, tt.id)
+ }
+ if field != tt.field {
+ t.Errorf("split test %d: field mismatch: have 0x%x, want 0x%x", i, field, tt.field)
+ }
+ }
+}
+
+var nodeDBInt64Tests = []struct {
+ key []byte
+ value int64
+}{
+ {key: []byte{0x01}, value: 1},
+ {key: []byte{0x02}, value: 2},
+ {key: []byte{0x03}, value: 3},
+}
+
+func TestDBInt64(t *testing.T) {
+ db, _ := OpenDB("")
+ defer db.Close()
+
+ tests := nodeDBInt64Tests
+ for i := 0; i < len(tests); i++ {
+ // Insert the next value
+ if err := db.storeInt64(tests[i].key, tests[i].value); err != nil {
+ t.Errorf("test %d: failed to store value: %v", i, err)
+ }
+ // Check all existing and non existing values
+ for j := 0; j < len(tests); j++ {
+ num := db.fetchInt64(tests[j].key)
+ switch {
+ case j <= i && num != tests[j].value:
+ t.Errorf("test %d, item %d: value mismatch: have %v, want %v", i, j, num, tests[j].value)
+ case j > i && num != 0:
+ t.Errorf("test %d, item %d: value mismatch: have %v, want %v", i, j, num, 0)
+ }
+ }
+ }
+}
+
+func TestDBFetchStore(t *testing.T) {
+ node := NewV4(
+ hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ net.IP{192, 168, 0, 1},
+ 30303,
+ 30303,
+ )
+ inst := time.Now()
+ num := 314
+
+ db, _ := OpenDB("")
+ defer db.Close()
+
+ // Check fetch/store operations on a node ping object
+ if stored := db.LastPingReceived(node.ID()); stored.Unix() != 0 {
+ t.Errorf("ping: non-existing object: %v", stored)
+ }
+ if err := db.UpdateLastPingReceived(node.ID(), inst); err != nil {
+ t.Errorf("ping: failed to update: %v", err)
+ }
+ if stored := db.LastPingReceived(node.ID()); stored.Unix() != inst.Unix() {
+ t.Errorf("ping: value mismatch: have %v, want %v", stored, inst)
+ }
+ // Check fetch/store operations on a node pong object
+ if stored := db.LastPongReceived(node.ID()); stored.Unix() != 0 {
+ t.Errorf("pong: non-existing object: %v", stored)
+ }
+ if err := db.UpdateLastPongReceived(node.ID(), inst); err != nil {
+ t.Errorf("pong: failed to update: %v", err)
+ }
+ if stored := db.LastPongReceived(node.ID()); stored.Unix() != inst.Unix() {
+ t.Errorf("pong: value mismatch: have %v, want %v", stored, inst)
+ }
+ // Check fetch/store operations on a node findnode-failure object
+ if stored := db.FindFails(node.ID()); stored != 0 {
+ t.Errorf("find-node fails: non-existing object: %v", stored)
+ }
+ if err := db.UpdateFindFails(node.ID(), num); err != nil {
+ t.Errorf("find-node fails: failed to update: %v", err)
+ }
+ if stored := db.FindFails(node.ID()); stored != num {
+ t.Errorf("find-node fails: value mismatch: have %v, want %v", stored, num)
+ }
+ // Check fetch/store operations on an actual node object
+ if stored := db.Node(node.ID()); stored != nil {
+ t.Errorf("node: non-existing object: %v", stored)
+ }
+ if err := db.UpdateNode(node); err != nil {
+ t.Errorf("node: failed to update: %v", err)
+ }
+ if stored := db.Node(node.ID()); stored == nil {
+ t.Errorf("node: not found")
+ } else if !reflect.DeepEqual(stored, node) {
+ t.Errorf("node: data mismatch: have %v, want %v", stored, node)
+ }
+}
+
+var nodeDBSeedQueryNodes = []struct {
+ node *Node
+ pong time.Time
+}{
+ // This one should not be in the result set because its last
+ // pong time is too far in the past.
+ {
+ node: NewV4(
+ hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ net.IP{127, 0, 0, 3},
+ 30303,
+ 30303,
+ ),
+ pong: time.Now().Add(-3 * time.Hour),
+ },
+ // This one shouldn't be in in the result set because its
+ // nodeID is the local node's ID.
+ {
+ node: NewV4(
+ hexPubkey("ff93ff820abacd4351b0f14e47b324bc82ff014c226f3f66a53535734a3c150e7e38ca03ef0964ba55acddc768f5e99cd59dea95ddd4defbab1339c92fa319b2"),
+ net.IP{127, 0, 0, 3},
+ 30303,
+ 30303,
+ ),
+ pong: time.Now().Add(-4 * time.Second),
+ },
+
+ // These should be in the result set.
+ {
+ node: NewV4(
+ hexPubkey("c2b5eb3f5dde05f815b63777809ee3e7e0cbb20035a6b00ce327191e6eaa8f26a8d461c9112b7ab94698e7361fa19fd647e603e73239002946d76085b6f928d6"),
+ net.IP{127, 0, 0, 1},
+ 30303,
+ 30303,
+ ),
+ pong: time.Now().Add(-2 * time.Second),
+ },
+ {
+ node: NewV4(
+ hexPubkey("6ca1d400c8ddf8acc94bcb0dd254911ad71a57bed5e0ae5aa205beed59b28c2339908e97990c493499613cff8ecf6c3dc7112a8ead220cdcd00d8847ca3db755"),
+ net.IP{127, 0, 0, 2},
+ 30303,
+ 30303,
+ ),
+ pong: time.Now().Add(-3 * time.Second),
+ },
+ {
+ node: NewV4(
+ hexPubkey("234dc63fe4d131212b38236c4c3411288d7bec61cbf7b120ff12c43dc60c96182882f4291d209db66f8a38e986c9c010ff59231a67f9515c7d1668b86b221a47"),
+ net.IP{127, 0, 0, 3},
+ 30303,
+ 30303,
+ ),
+ pong: time.Now().Add(-1 * time.Second),
+ },
+ {
+ node: NewV4(
+ hexPubkey("c013a50b4d1ebce5c377d8af8cb7114fd933ffc9627f96ad56d90fef5b7253ec736fd07ef9a81dc2955a997e54b7bf50afd0aa9f110595e2bec5bb7ce1657004"),
+ net.IP{127, 0, 0, 3},
+ 30303,
+ 30303,
+ ),
+ pong: time.Now().Add(-2 * time.Second),
+ },
+ {
+ node: NewV4(
+ hexPubkey("f141087e3e08af1aeec261ff75f48b5b1637f594ea9ad670e50051646b0416daa3b134c28788cbe98af26992a47652889cd8577ccc108ac02c6a664db2dc1283"),
+ net.IP{127, 0, 0, 3},
+ 30303,
+ 30303,
+ ),
+ pong: time.Now().Add(-2 * time.Second),
+ },
+}
+
+func TestDBSeedQuery(t *testing.T) {
+ // Querying seeds uses seeks an might not find all nodes
+ // every time when the database is small. Run the test multiple
+ // times to avoid flakes.
+ const attempts = 15
+ var err error
+ for i := 0; i < attempts; i++ {
+ if err = testSeedQuery(); err == nil {
+ return
+ }
+ }
+ if err != nil {
+ t.Errorf("no successful run in %d attempts: %v", attempts, err)
+ }
+}
+
+func testSeedQuery() error {
+ db, _ := OpenDB("")
+ defer db.Close()
+
+ // Insert a batch of nodes for querying
+ for i, seed := range nodeDBSeedQueryNodes {
+ if err := db.UpdateNode(seed.node); err != nil {
+ return fmt.Errorf("node %d: failed to insert: %v", i, err)
+ }
+ if err := db.UpdateLastPongReceived(seed.node.ID(), seed.pong); err != nil {
+ return fmt.Errorf("node %d: failed to insert bondTime: %v", i, err)
+ }
+ }
+
+ // Retrieve the entire batch and check for duplicates
+ seeds := db.QuerySeeds(len(nodeDBSeedQueryNodes)*2, time.Hour)
+ have := make(map[ID]struct{})
+ for _, seed := range seeds {
+ have[seed.ID()] = struct{}{}
+ }
+ want := make(map[ID]struct{})
+ for _, seed := range nodeDBSeedQueryNodes[1:] {
+ want[seed.node.ID()] = struct{}{}
+ }
+ if len(seeds) != len(want) {
+ return fmt.Errorf("seed count mismatch: have %v, want %v", len(seeds), len(want))
+ }
+ for id := range have {
+ if _, ok := want[id]; !ok {
+ return fmt.Errorf("extra seed: %v", id)
+ }
+ }
+ for id := range want {
+ if _, ok := have[id]; !ok {
+ return fmt.Errorf("missing seed: %v", id)
+ }
+ }
+ return nil
+}
+
+func TestDBPersistency(t *testing.T) {
+ root, err := ioutil.TempDir("", "nodedb-")
+ if err != nil {
+ t.Fatalf("failed to create temporary data folder: %v", err)
+ }
+ defer os.RemoveAll(root)
+
+ var (
+ testKey = []byte("somekey")
+ testInt = int64(314)
+ )
+
+ // Create a persistent database and store some values
+ db, err := OpenDB(filepath.Join(root, "database"))
+ if err != nil {
+ t.Fatalf("failed to create persistent database: %v", err)
+ }
+ if err := db.storeInt64(testKey, testInt); err != nil {
+ t.Fatalf("failed to store value: %v.", err)
+ }
+ db.Close()
+
+ // Reopen the database and check the value
+ db, err = OpenDB(filepath.Join(root, "database"))
+ if err != nil {
+ t.Fatalf("failed to open persistent database: %v", err)
+ }
+ if val := db.fetchInt64(testKey); val != testInt {
+ t.Fatalf("value mismatch: have %v, want %v", val, testInt)
+ }
+ db.Close()
+}
+
+var nodeDBExpirationNodes = []struct {
+ node *Node
+ pong time.Time
+ exp bool
+}{
+ {
+ node: NewV4(
+ hexPubkey("8d110e2ed4b446d9b5fb50f117e5f37fb7597af455e1dab0e6f045a6eeaa786a6781141659020d38bdc5e698ed3d4d2bafa8b5061810dfa63e8ac038db2e9b67"),
+ net.IP{127, 0, 0, 1},
+ 30303,
+ 30303,
+ ),
+ pong: time.Now().Add(-nodeDBNodeExpiration + time.Minute),
+ exp: false,
+ }, {
+ node: NewV4(
+ hexPubkey("913a205579c32425b220dfba999d215066e5bdbf900226b11da1907eae5e93eb40616d47412cf819664e9eacbdfcca6b0c6e07e09847a38472d4be46ab0c3672"),
+ net.IP{127, 0, 0, 2},
+ 30303,
+ 30303,
+ ),
+ pong: time.Now().Add(-nodeDBNodeExpiration - time.Minute),
+ exp: true,
+ },
+}
+
+func TestDBExpiration(t *testing.T) {
+ db, _ := OpenDB("")
+ defer db.Close()
+
+ // Add all the test nodes and set their last pong time
+ for i, seed := range nodeDBExpirationNodes {
+ if err := db.UpdateNode(seed.node); err != nil {
+ t.Fatalf("node %d: failed to insert: %v", i, err)
+ }
+ if err := db.UpdateLastPongReceived(seed.node.ID(), seed.pong); err != nil {
+ t.Fatalf("node %d: failed to update bondTime: %v", i, err)
+ }
+ }
+ // Expire some of them, and check the rest
+ if err := db.expireNodes(); err != nil {
+ t.Fatalf("failed to expire nodes: %v", err)
+ }
+ for i, seed := range nodeDBExpirationNodes {
+ node := db.Node(seed.node.ID())
+ if (node == nil && !seed.exp) || (node != nil && seed.exp) {
+ t.Errorf("node %d: expiration mismatch: have %v, want %v", i, node, seed.exp)
+ }
+ }
+}
diff --git a/p2p/enode/urlv4.go b/p2p/enode/urlv4.go
new file mode 100644
index 000000000..50e9485d0
--- /dev/null
+++ b/p2p/enode/urlv4.go
@@ -0,0 +1,194 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package enode
+
+import (
+ "crypto/ecdsa"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "net"
+ "net/url"
+ "regexp"
+ "strconv"
+
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+)
+
+var incompleteNodeURL = regexp.MustCompile("(?i)^(?:enode://)?([0-9a-f]+)$")
+
+// MustParseV4 parses a node URL. It panics if the URL is not valid.
+func MustParseV4(rawurl string) *Node {
+ n, err := ParseV4(rawurl)
+ if err != nil {
+ panic("invalid node URL: " + err.Error())
+ }
+ return n
+}
+
+// ParseV4 parses a node URL.
+//
+// There are two basic forms of node URLs:
+//
+// - incomplete nodes, which only have the public key (node ID)
+// - complete nodes, which contain the public key and IP/Port information
+//
+// For incomplete nodes, the designator must look like one of these
+//
+// enode://<hex node id>
+// <hex node id>
+//
+// For complete nodes, the node ID is encoded in the username portion
+// of the URL, separated from the host by an @ sign. The hostname can
+// only be given as an IP address, DNS domain names are not allowed.
+// The port in the host name section is the TCP listening port. If the
+// TCP and UDP (discovery) ports differ, the UDP port is specified as
+// query parameter "discport".
+//
+// In the following example, the node URL describes
+// a node with IP address 10.3.58.6, TCP listening port 30303
+// and UDP discovery port 30301.
+//
+// enode://<hex node id>@10.3.58.6:30303?discport=30301
+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 NewV4(id, nil, 0, 0), nil
+ }
+ return parseComplete(rawurl)
+}
+
+// NewV4 creates a node from discovery v4 node information. The record
+// 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 {
+ r.Set(enr.IP(ip))
+ }
+ if udp != 0 {
+ r.Set(enr.UDP(udp))
+ }
+ if tcp != 0 {
+ r.Set(enr.TCP(tcp))
+ }
+ signV4Compat(&r, pubkey)
+ n, err := New(v4CompatID{}, &r)
+ if err != nil {
+ panic(err)
+ }
+ return n
+}
+
+func parseComplete(rawurl string) (*Node, error) {
+ var (
+ id *ecdsa.PublicKey
+ ip net.IP
+ tcpPort, udpPort uint64
+ )
+ u, err := url.Parse(rawurl)
+ if err != nil {
+ return nil, err
+ }
+ if u.Scheme != "enode" {
+ return nil, errors.New("invalid URL scheme, want \"enode\"")
+ }
+ // Parse the Node ID from the user portion.
+ if u.User == nil {
+ 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)
+ }
+ // Parse the IP address.
+ host, port, err := net.SplitHostPort(u.Host)
+ if err != nil {
+ return nil, fmt.Errorf("invalid host: %v", err)
+ }
+ 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")
+ }
+ udpPort = tcpPort
+ qv := u.Query()
+ if qv.Get("discport") != "" {
+ udpPort, err = strconv.ParseUint(qv.Get("discport"), 10, 16)
+ if err != nil {
+ return nil, errors.New("invalid discport in query")
+ }
+ }
+ return NewV4(id, ip, int(tcpPort), int(udpPort)), nil
+}
+
+// parsePubkey parses a hex-encoded secp256k1 public key.
+func parsePubkey(in string) (*ecdsa.PublicKey, error) {
+ b, err := hex.DecodeString(in)
+ if err != nil {
+ return nil, err
+ } else if len(b) != 64 {
+ return nil, fmt.Errorf("wrong length, want %d hex chars", 128)
+ }
+ b = append([]byte{0x4}, b...)
+ return crypto.UnmarshalPubkey(b)
+}
+
+func (n *Node) v4URL() string {
+ var (
+ scheme enr.ID
+ nodeid string
+ key ecdsa.PublicKey
+ )
+ n.Load(&scheme)
+ n.Load((*Secp256k1)(&key))
+ switch {
+ case scheme == "v4" || key != ecdsa.PublicKey{}:
+ nodeid = fmt.Sprintf("%x", crypto.FromECDSAPub(&key)[1:])
+ default:
+ nodeid = fmt.Sprintf("%s.%x", scheme, n.id[:])
+ }
+ u := url.URL{Scheme: "enode"}
+ if n.Incomplete() {
+ u.Host = nodeid
+ } else {
+ addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()}
+ u.User = url.User(nodeid)
+ u.Host = addr.String()
+ if n.UDP() != n.TCP() {
+ u.RawQuery = "discport=" + strconv.Itoa(n.UDP())
+ }
+ }
+ return u.String()
+}
+
+// PubkeyToIDV4 derives the v4 node address from the given public key.
+func PubkeyToIDV4(key *ecdsa.PublicKey) ID {
+ e := make([]byte, 64)
+ math.ReadBits(key.X, e[:len(e)/2])
+ math.ReadBits(key.Y, e[len(e)/2:])
+ return ID(crypto.Keccak256Hash(e))
+}
diff --git a/p2p/enode/urlv4_test.go b/p2p/enode/urlv4_test.go
new file mode 100644
index 000000000..3680ab6b7
--- /dev/null
+++ b/p2p/enode/urlv4_test.go
@@ -0,0 +1,243 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package enode
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "math/big"
+ "net"
+ "reflect"
+ "strings"
+ "testing"
+ "testing/quick"
+)
+
+var parseNodeTests = []struct {
+ rawurl string
+ wantError string
+ wantResult *Node
+}{
+ {
+ rawurl: "http://foobar",
+ wantError: `invalid URL scheme, want "enode"`,
+ },
+ {
+ rawurl: "enode://01010101@123.124.125.126:3",
+ wantError: `invalid node ID (wrong length, want 128 hex chars)`,
+ },
+ // Complete nodes with IP address.
+ {
+ rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3",
+ wantError: `invalid IP address`,
+ },
+ {
+ rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo",
+ wantError: `invalid port`,
+ },
+ {
+ rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo",
+ wantError: `invalid discport in query`,
+ },
+ {
+ rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150",
+ wantResult: NewV4(
+ hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ net.IP{0x7f, 0x0, 0x0, 0x1},
+ 52150,
+ 52150,
+ ),
+ },
+ {
+ rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150",
+ wantResult: NewV4(
+ hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ net.ParseIP("::"),
+ 52150,
+ 52150,
+ ),
+ },
+ {
+ rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150",
+ wantResult: NewV4(
+ hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ net.ParseIP("2001:db8:3c4d:15::abcd:ef12"),
+ 52150,
+ 52150,
+ ),
+ },
+ {
+ rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334",
+ wantResult: NewV4(
+ hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ net.IP{0x7f, 0x0, 0x0, 0x1},
+ 52150,
+ 22334,
+ ),
+ },
+ // Incomplete nodes with no address.
+ {
+ rawurl: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
+ wantResult: NewV4(
+ hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ nil, 0, 0,
+ ),
+ },
+ {
+ rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
+ wantResult: NewV4(
+ hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
+ nil, 0, 0,
+ ),
+ },
+ // Invalid URLs
+ {
+ rawurl: "01010101",
+ wantError: `invalid node ID (wrong length, want 128 hex chars)`,
+ },
+ {
+ rawurl: "enode://01010101",
+ wantError: `invalid node ID (wrong length, want 128 hex chars)`,
+ },
+ {
+ // This test checks that errors from url.Parse are handled.
+ rawurl: "://foo",
+ wantError: `parse ://foo: missing protocol scheme`,
+ },
+}
+
+func hexPubkey(h string) *ecdsa.PublicKey {
+ k, err := parsePubkey(h)
+ if err != nil {
+ panic(err)
+ }
+ return k
+}
+
+func TestParseNode(t *testing.T) {
+ for _, test := range parseNodeTests {
+ n, err := ParseV4(test.rawurl)
+ if test.wantError != "" {
+ if err == nil {
+ t.Errorf("test %q:\n got nil error, expected %#q", test.rawurl, 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)
+ continue
+ }
+ } else {
+ if err != nil {
+ t.Errorf("test %q:\n unexpected error: %v", test.rawurl, err)
+ continue
+ }
+ if !reflect.DeepEqual(n, test.wantResult) {
+ t.Errorf("test %q:\n result mismatch:\ngot: %#v\nwant: %#v", test.rawurl, n, test.wantResult)
+ }
+ }
+ }
+}
+
+func TestNodeString(t *testing.T) {
+ for i, test := range parseNodeTests {
+ if test.wantError == "" && strings.HasPrefix(test.rawurl, "enode://") {
+ str := test.wantResult.String()
+ if str != test.rawurl {
+ t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl)
+ }
+ }
+ }
+}
+
+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")
+ }
+}