diff --git a/p2p/enr/idscheme.go b/p2p/enode/idscheme.go
similarity index 50%
rename from p2p/enr/idscheme.go
rename to p2p/enode/idscheme.go
index efaf68041d..9b495fd4ff 100644
--- a/p2p/enr/idscheme.go
+++ b/p2p/enode/idscheme.go
@@ -14,57 +14,38 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
-package enr
+package enode
import (
"crypto/ecdsa"
"fmt"
- "sync"
+ "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"
)
-// Registry of known identity schemes.
-var schemes sync.Map
-
-// An IdentityScheme is capable of verifying record signatures and
-// deriving node addresses.
-type IdentityScheme interface {
- Verify(r *Record, sig []byte) error
- NodeAddr(r *Record) []byte
+// List of known secure identity schemes.
+var ValidSchemes = enr.SchemeMap{
+ "v4": V4ID{},
}
-// RegisterIdentityScheme adds an identity scheme to the global registry.
-func RegisterIdentityScheme(name string, scheme IdentityScheme) {
- if _, loaded := schemes.LoadOrStore(name, scheme); loaded {
- panic("identity scheme " + name + " already registered")
- }
-}
-
-// FindIdentityScheme resolves name to an identity scheme in the global registry.
-func FindIdentityScheme(name string) IdentityScheme {
- s, ok := schemes.Load(name)
- if !ok {
- return nil
- }
- return s.(IdentityScheme)
+var ValidSchemesForTesting = enr.SchemeMap{
+ "v4": V4ID{},
+ "null": NullID{},
}
// v4ID is the "v4" identity scheme.
-type v4ID struct{}
-
-func init() {
- RegisterIdentityScheme("v4", v4ID{})
-}
+type V4ID struct{}
// SignV4 signs a record using the v4 scheme.
-func SignV4(r *Record, privkey *ecdsa.PrivateKey) error {
+func SignV4(r *enr.Record, privkey *ecdsa.PrivateKey) error {
// Copy r to avoid modifying it if signing fails.
cpy := *r
- cpy.Set(ID("v4"))
+ cpy.Set(enr.ID("v4"))
cpy.Set(Secp256k1(privkey.PublicKey))
h := sha3.NewKeccak256()
@@ -74,18 +55,13 @@ func SignV4(r *Record, privkey *ecdsa.PrivateKey) error {
return err
}
sig = sig[:len(sig)-1] // remove v
- if err = cpy.SetSig("v4", sig); err == nil {
+ if err = cpy.SetSig(V4ID{}, sig); err == nil {
*r = cpy
}
return err
}
-// s256raw is an unparsed secp256k1 public key entry.
-type s256raw []byte
-
-func (s256raw) ENRKey() string { return "secp256k1" }
-
-func (v4ID) Verify(r *Record, sig []byte) error {
+func (V4ID) Verify(r *enr.Record, sig []byte) error {
var entry s256raw
if err := r.Load(&entry); err != nil {
return err
@@ -96,12 +72,12 @@ func (v4ID) Verify(r *Record, sig []byte) error {
h := sha3.NewKeccak256()
rlp.Encode(h, r.AppendElements(nil))
if !crypto.VerifySignature(entry, h.Sum(nil), sig) {
- return errInvalidSig
+ return enr.ErrInvalidSig
}
return nil
}
-func (v4ID) NodeAddr(r *Record) []byte {
+func (V4ID) NodeAddr(r *enr.Record) []byte {
var pubkey Secp256k1
err := r.Load(&pubkey)
if err != nil {
@@ -112,3 +88,73 @@ func (v4ID) NodeAddr(r *Record) []byte {
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 0000000000..0910e6e83f
--- /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 .
+
+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 0000000000..84088fcd26
--- /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 .
+
+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 0000000000..861a70bd64
--- /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 .
+
+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 0000000000..a929b75d7b
--- /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 .
+
+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 0000000000..57a0424d8d
--- /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 .
+
+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 0000000000..50e9485d04
--- /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 .
+
+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://
+//
+//
+// 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://@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 0000000000..3680ab6b78
--- /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 .
+
+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")
+ }
+}
diff --git a/p2p/enr/enr.go b/p2p/enr/enr.go
index 48683471d2..251caf4585 100644
--- a/p2p/enr/enr.go
+++ b/p2p/enr/enr.go
@@ -15,14 +15,20 @@
// along with the go-ethereum library. If not, see .
// Package enr implements Ethereum Node Records as defined in EIP-778. A node record holds
-// arbitrary information about a node on the peer-to-peer network.
-//
-// Records contain named keys. To store and retrieve key/values in a record, use the Entry
+// arbitrary information about a node on the peer-to-peer network. Node information is
+// stored in key/value pairs. To store and retrieve key/values in a record, use the Entry
// interface.
//
-// Records must be signed before transmitting them to another node. Decoding a record verifies
-// its signature. When creating a record, set the entries you want, then call Sign to add the
-// signature. Modifying a record invalidates the signature.
+// Signature Handling
+//
+// Records must be signed before transmitting them to another node.
+//
+// Decoding a record doesn't check its signature. Code working with records from an
+// untrusted source must always verify two things: that the record uses an identity scheme
+// deemed secure, and that the signature is valid according to the declared scheme.
+//
+// When creating a record, set the entries you want and use a signing function provided by
+// the identity scheme to add the signature. Modifying a record invalidates the signature.
//
// Package enr supports the "secp256k1-keccak" identity scheme.
package enr
@@ -40,8 +46,7 @@ import (
const SizeLimit = 300 // maximum encoded size of a node record in bytes
var (
- errNoID = errors.New("unknown or unspecified identity scheme")
- errInvalidSig = errors.New("invalid signature")
+ ErrInvalidSig = errors.New("invalid signature on node record")
errNotSorted = errors.New("record key/value pairs are not sorted by key")
errDuplicateKey = errors.New("record contains duplicate key")
errIncompletePair = errors.New("record contains incomplete k/v pair")
@@ -50,6 +55,32 @@ var (
errNotFound = errors.New("no such key in record")
)
+// An IdentityScheme is capable of verifying record signatures and
+// deriving node addresses.
+type IdentityScheme interface {
+ Verify(r *Record, sig []byte) error
+ NodeAddr(r *Record) []byte
+}
+
+// SchemeMap is a registry of named identity schemes.
+type SchemeMap map[string]IdentityScheme
+
+func (m SchemeMap) Verify(r *Record, sig []byte) error {
+ s := m[r.IdentityScheme()]
+ if s == nil {
+ return ErrInvalidSig
+ }
+ return s.Verify(r, sig)
+}
+
+func (m SchemeMap) NodeAddr(r *Record) []byte {
+ s := m[r.IdentityScheme()]
+ if s == nil {
+ return nil
+ }
+ return s.NodeAddr(r)
+}
+
// Record represents a node record. The zero value is an empty record.
type Record struct {
seq uint64 // sequence number
@@ -64,11 +95,6 @@ type pair struct {
v rlp.RawValue
}
-// Signed reports whether the record has a valid signature.
-func (r *Record) Signed() bool {
- return r.signature != nil
-}
-
// Seq returns the sequence number.
func (r *Record) Seq() uint64 {
return r.seq
@@ -140,7 +166,7 @@ func (r *Record) invalidate() {
// EncodeRLP implements rlp.Encoder. Encoding fails if
// the record is unsigned.
func (r Record) EncodeRLP(w io.Writer) error {
- if !r.Signed() {
+ if r.signature == nil {
return errEncodeUnsigned
}
_, err := w.Write(r.raw)
@@ -149,25 +175,34 @@ func (r Record) EncodeRLP(w io.Writer) error {
// DecodeRLP implements rlp.Decoder. Decoding verifies the signature.
func (r *Record) DecodeRLP(s *rlp.Stream) error {
- raw, err := s.Raw()
+ dec, raw, err := decodeRecord(s)
if err != nil {
return err
}
+ *r = dec
+ r.raw = raw
+ return nil
+}
+
+func decodeRecord(s *rlp.Stream) (dec Record, raw []byte, err error) {
+ raw, err = s.Raw()
+ if err != nil {
+ return dec, raw, err
+ }
if len(raw) > SizeLimit {
- return errTooBig
+ return dec, raw, errTooBig
}
// Decode the RLP container.
- dec := Record{raw: raw}
s = rlp.NewStream(bytes.NewReader(raw), 0)
if _, err := s.List(); err != nil {
- return err
+ return dec, raw, err
}
if err = s.Decode(&dec.signature); err != nil {
- return err
+ return dec, raw, err
}
if err = s.Decode(&dec.seq); err != nil {
- return err
+ return dec, raw, err
}
// The rest of the record contains sorted k/v pairs.
var prevkey string
@@ -177,73 +212,68 @@ func (r *Record) DecodeRLP(s *rlp.Stream) error {
if err == rlp.EOL {
break
}
- return err
+ return dec, raw, err
}
if err := s.Decode(&kv.v); err != nil {
if err == rlp.EOL {
- return errIncompletePair
+ return dec, raw, errIncompletePair
}
- return err
+ return dec, raw, err
}
if i > 0 {
if kv.k == prevkey {
- return errDuplicateKey
+ return dec, raw, errDuplicateKey
}
if kv.k < prevkey {
- return errNotSorted
+ return dec, raw, errNotSorted
}
}
dec.pairs = append(dec.pairs, kv)
prevkey = kv.k
}
- if err := s.ListEnd(); err != nil {
- return err
- }
-
- _, scheme := dec.idScheme()
- if scheme == nil {
- return errNoID
- }
- if err := scheme.Verify(&dec, dec.signature); err != nil {
- return err
- }
- *r = dec
- return nil
+ return dec, raw, s.ListEnd()
}
-// NodeAddr returns the node address. The return value will be nil if the record is
-// unsigned or uses an unknown identity scheme.
-func (r *Record) NodeAddr() []byte {
- _, scheme := r.idScheme()
- if scheme == nil {
- return nil
- }
- return scheme.NodeAddr(r)
+// IdentityScheme returns the name of the identity scheme in the record.
+func (r *Record) IdentityScheme() string {
+ var id ID
+ r.Load(&id)
+ return string(id)
+}
+
+// VerifySignature checks whether the record is signed using the given identity scheme.
+func (r *Record) VerifySignature(s IdentityScheme) error {
+ return s.Verify(r, r.signature)
}
// SetSig sets the record signature. It returns an error if the encoded record is larger
// than the size limit or if the signature is invalid according to the passed scheme.
-func (r *Record) SetSig(idscheme string, sig []byte) error {
- // Check that "id" is set and matches the given scheme. This panics because
- // inconsitencies here are always implementation bugs in the signing function calling
- // this method.
- id, s := r.idScheme()
- if s == nil {
- panic(errNoID)
+//
+// You can also use SetSig to remove the signature explicitly by passing a nil scheme
+// and signature.
+//
+// SetSig panics when either the scheme or the signature (but not both) are nil.
+func (r *Record) SetSig(s IdentityScheme, sig []byte) error {
+ switch {
+ // Prevent storing invalid data.
+ case s == nil && sig != nil:
+ panic("enr: invalid call to SetSig with non-nil signature but nil scheme")
+ case s != nil && sig == nil:
+ panic("enr: invalid call to SetSig with nil signature but non-nil scheme")
+ // Verify if we have a scheme.
+ case s != nil:
+ if err := s.Verify(r, sig); err != nil {
+ return err
+ }
+ raw, err := r.encode(sig)
+ if err != nil {
+ return err
+ }
+ r.signature, r.raw = sig, raw
+ // Reset otherwise.
+ default:
+ r.signature, r.raw = nil, nil
}
- if id != idscheme {
- panic(fmt.Errorf("identity scheme mismatch in Sign: record has %s, want %s", id, idscheme))
- }
-
- // Verify against the scheme.
- if err := s.Verify(r, sig); err != nil {
- return err
- }
- raw, err := r.encode(sig)
- if err != nil {
- return err
- }
- r.signature, r.raw = sig, raw
return nil
}
@@ -268,11 +298,3 @@ func (r *Record) encode(sig []byte) (raw []byte, err error) {
}
return raw, nil
}
-
-func (r *Record) idScheme() (string, IdentityScheme) {
- var id ID
- if err := r.Load(&id); err != nil {
- return "", nil
- }
- return string(id), FindIdentityScheme(string(id))
-}
diff --git a/p2p/enr/enr_test.go b/p2p/enr/enr_test.go
index d1d0887563..9bf22478dd 100644
--- a/p2p/enr/enr_test.go
+++ b/p2p/enr/enr_test.go
@@ -18,23 +18,17 @@ package enr
import (
"bytes"
- "encoding/hex"
+ "encoding/binary"
"fmt"
"math/rand"
"testing"
"time"
- "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-var (
- privkey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
- pubkey = &privkey.PublicKey
-)
-
var rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
func randomString(strlen int) string {
@@ -87,18 +81,6 @@ func TestGetSetUDP(t *testing.T) {
assert.Equal(t, port, port2)
}
-// TestGetSetSecp256k1 tests encoding/decoding and setting/getting of the Secp256k1 key.
-func TestGetSetSecp256k1(t *testing.T) {
- var r 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)
-}
-
func TestLoadErrors(t *testing.T) {
var r Record
ip4 := IP{127, 0, 0, 1}
@@ -167,23 +149,20 @@ func TestSortedGetAndSet(t *testing.T) {
func TestDirty(t *testing.T) {
var r Record
- if r.Signed() {
- t.Error("Signed returned true for zero record")
- }
if _, err := rlp.EncodeToBytes(r); err != errEncodeUnsigned {
t.Errorf("expected errEncodeUnsigned, got %#v", err)
}
- require.NoError(t, SignV4(&r, privkey))
- if !r.Signed() {
- t.Error("Signed return false for signed record")
+ require.NoError(t, signTest([]byte{5}, &r))
+ if len(r.signature) == 0 {
+ t.Error("record is not signed")
}
_, err := rlp.EncodeToBytes(r)
assert.NoError(t, err)
r.SetSeq(3)
- if r.Signed() {
- t.Error("Signed returned true for modified record")
+ if len(r.signature) != 0 {
+ t.Error("signature still set after modification")
}
if _, err := rlp.EncodeToBytes(r); err != errEncodeUnsigned {
t.Errorf("expected errEncodeUnsigned, got %#v", err)
@@ -210,7 +189,7 @@ func TestSignEncodeAndDecode(t *testing.T) {
var r Record
r.Set(UDP(30303))
r.Set(IP{127, 0, 0, 1})
- require.NoError(t, SignV4(&r, privkey))
+ require.NoError(t, signTest([]byte{5}, &r))
blob, err := rlp.EncodeToBytes(r)
require.NoError(t, err)
@@ -224,48 +203,6 @@ func TestSignEncodeAndDecode(t *testing.T) {
assert.Equal(t, blob, blob2)
}
-func TestNodeAddr(t *testing.T) {
- var r Record
- if addr := r.NodeAddr(); 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(r.NodeAddr()))
-}
-
-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 Record
- if err := rlp.DecodeBytes(pyRecord, &r); err != nil {
- t.Fatalf("can't decode: %v", err)
- }
-
- var (
- wantAddr, _ = hex.DecodeString("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7")
- wantSeq = uint64(1)
- wantIP = IP{127, 0, 0, 1}
- wantUDP = UDP(30303)
- )
- if r.Seq() != wantSeq {
- t.Errorf("wrong seq: got %d, want %d", r.Seq(), wantSeq)
- }
- if addr := r.NodeAddr(); !bytes.Equal(addr, wantAddr) {
- t.Errorf("wrong addr: got %x, want %x", addr, wantAddr)
- }
- want := map[Entry]interface{}{new(IP): &wantIP, new(UDP): &wantUDP}
- for k, v := range want {
- desc := fmt.Sprintf("loading key %q", k.ENRKey())
- if assert.NoError(t, r.Load(k), desc) {
- assert.Equal(t, k, v, desc)
- }
- }
-}
-
// TestRecordTooBig tests that records bigger than SizeLimit bytes cannot be signed.
func TestRecordTooBig(t *testing.T) {
var r Record
@@ -273,13 +210,13 @@ func TestRecordTooBig(t *testing.T) {
// set a big value for random key, expect error
r.Set(WithEntry(key, randomString(SizeLimit)))
- if err := SignV4(&r, privkey); err != errTooBig {
+ if err := signTest([]byte{5}, &r); err != errTooBig {
t.Fatalf("expected to get errTooBig, got %#v", err)
}
// set an acceptable value for random key, expect no error
r.Set(WithEntry(key, randomString(100)))
- require.NoError(t, SignV4(&r, privkey))
+ require.NoError(t, signTest([]byte{5}, &r))
}
// TestSignEncodeAndDecodeRandom tests encoding/decoding of records containing random key/value pairs.
@@ -295,7 +232,7 @@ func TestSignEncodeAndDecodeRandom(t *testing.T) {
r.Set(WithEntry(key, &value))
}
- require.NoError(t, SignV4(&r, privkey))
+ require.NoError(t, signTest([]byte{5}, &r))
_, err := rlp.EncodeToBytes(r)
require.NoError(t, err)
@@ -308,11 +245,40 @@ func TestSignEncodeAndDecodeRandom(t *testing.T) {
}
}
-func BenchmarkDecode(b *testing.B) {
- var r Record
- for i := 0; i < b.N; i++ {
- rlp.DecodeBytes(pyRecord, &r)
- }
- b.StopTimer()
- r.NodeAddr()
+type testSig struct{}
+
+type testID []byte
+
+func (id testID) ENRKey() string { return "testid" }
+
+func signTest(id []byte, r *Record) error {
+ r.Set(ID("test"))
+ r.Set(testID(id))
+ return r.SetSig(testSig{}, makeTestSig(id, r.Seq()))
+}
+
+func makeTestSig(id []byte, seq uint64) []byte {
+ sig := make([]byte, 8, len(id)+8)
+ binary.BigEndian.PutUint64(sig[:8], seq)
+ sig = append(sig, id...)
+ return sig
+}
+
+func (testSig) Verify(r *Record, sig []byte) error {
+ var id []byte
+ if err := r.Load((*testID)(&id)); err != nil {
+ return err
+ }
+ if !bytes.Equal(sig, makeTestSig(id, r.Seq())) {
+ return ErrInvalidSig
+ }
+ return nil
+}
+
+func (testSig) NodeAddr(r *Record) []byte {
+ var id []byte
+ if err := r.Load((*testID)(&id)); err != nil {
+ return nil
+ }
+ return id
}
diff --git a/p2p/enr/entries.go b/p2p/enr/entries.go
index 71c7653a28..347990ab64 100644
--- a/p2p/enr/entries.go
+++ b/p2p/enr/entries.go
@@ -17,12 +17,10 @@
package enr
import (
- "crypto/ecdsa"
"fmt"
"io"
"net"
- "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
)
@@ -98,30 +96,6 @@ func (v *IP) DecodeRLP(s *rlp.Stream) error {
return nil
}
-// 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
-}
-
// KeyError is an error related to a key.
type KeyError struct {
Key string
diff --git a/p2p/enr/idscheme_test.go b/p2p/enr/idscheme_test.go
deleted file mode 100644
index d790e12f14..0000000000
--- a/p2p/enr/idscheme_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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 .
-
-package enr
-
-import (
- "crypto/ecdsa"
- "math/big"
- "testing"
-)
-
-// 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 Record
- if err := SignV4(&r, invalidKey); err == nil {
- t.Fatal("expected error from SignV4")
- }
- if len(r.pairs) > 0 {
- t.Fatal("expected empty record, have", r.pairs)
- }
-}