diff --git a/p2p/enr/enr.go b/p2p/enr/enr.go
new file mode 100644
index 0000000000..a6b04e5fd0
--- /dev/null
+++ b/p2p/enr/enr.go
@@ -0,0 +1,396 @@
+// 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 discover implements the Ethereum Node Record as per https://github.com/ethereum/EIPs/pull/778
+package enr
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "encoding/binary"
+ "errors"
+ "net"
+ "sort"
+
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/rlp"
+
+ "github.com/btcsuite/btcd/btcec"
+)
+
+// The maximum encoded size of a node record is 300 bytes. Implementations should reject records larger than this size.
+const (
+ RECORD_MAX_SIZE = 300
+ ID_SECP256k1_KECCAK = "secp256k1-keccak" // "secp256k1-keccak" identity scheme identifier
+)
+
+// Pseudo-const identifiers for pre-defined keys
+var (
+ ID = []byte(`id`) // name of identity scheme, e.g. "secp256k1-keccak"
+ SECP256K1 = []byte(`secp256k1`) // compressed secp256k1 public key
+ IP4 = []byte(`ip4`) // IPv4 address, 4 bytes
+ IP6 = []byte(`ip6`) // IPv6 address, 16 bytes
+ DISCV5 = []byte(`discv5`) // UDP port for discovery v5
+)
+
+type record struct {
+ k []byte
+ v []byte
+}
+
+type ENR struct {
+ seq uint32 // sequence number
+ signature []byte // record's signature
+ raw []byte // RLP encoded record
+ records []record // list of all key/value pairs, sorted prior to RLP encoding
+ dirty bool // keeps track if record was modified after it was signed and encoded
+}
+
+func NewENR() *ENR {
+ return &ENR{
+ dirty: true,
+ }
+}
+
+func (e *ENR) GetID() (string, error) {
+ for _, r := range e.records {
+ if bytes.Compare(ID, r.k) == 0 {
+ return string(r.v), nil
+ }
+ }
+
+ return "", errors.New("id record does not exist")
+}
+
+func (e *ENR) SetID(id string) {
+ e.dirty = true
+ e.records = append(e.records, record{ID, []byte(id)})
+}
+
+func (e *ENR) GetSecp256k1() ([]byte, error) {
+ for _, r := range e.records {
+ if bytes.Compare(SECP256K1, r.k) == 0 {
+ return r.v, nil
+ }
+ }
+
+ return nil, errors.New("secp256k1 record does not exist")
+}
+
+func (e *ENR) SetSecp256k1(pk []byte) {
+ e.dirty = true
+ e.records = append(e.records, record{SECP256K1, pk})
+}
+
+func (e *ENR) GetIPv4() (net.IP, error) {
+ for _, r := range e.records {
+ if bytes.Compare(IP4, r.k) == 0 {
+ if len(r.v) != net.IPv4len {
+ return nil, errors.New("wrong ipv4 record length")
+ }
+ return net.IP(r.v), nil
+ }
+ }
+
+ return nil, errors.New("ip4 record does not exist")
+}
+
+func (e *ENR) SetIPv4(ip net.IP) error {
+ e.dirty = true
+ ipv4 := ip.To4()
+ if ipv4 == nil {
+ return errors.New("param is not a valid ipv4 address")
+ }
+ e.records = append(e.records, record{IP4, ipv4})
+ return nil
+}
+
+func (e *ENR) GetIPv6() (net.IP, error) {
+ for _, r := range e.records {
+ if bytes.Compare(IP6, r.k) == 0 {
+ if len(r.v) != net.IPv6len {
+ return nil, errors.New("wrong ipv6 record length")
+ }
+ return net.IP(r.v), nil
+ }
+ }
+
+ return nil, errors.New("ip6 record does not exist")
+}
+
+func (e *ENR) SetIPv6(ip net.IP) error {
+ if len(ip) != net.IPv6len {
+ return errors.New("param length is not equal to 16 bytes")
+ }
+ e.dirty = true
+ e.records = append(e.records, record{IP6, ip})
+ return nil
+}
+
+func (e *ENR) GetDiscv5() (uint32, error) {
+ for _, r := range e.records {
+ if bytes.Compare(DISCV5, r.k) == 0 {
+ buf := bytes.NewBuffer(r.v)
+ var port uint32
+ err := binary.Read(buf, binary.BigEndian, &port)
+ if err != nil {
+ return 0, err
+ }
+
+ return port, nil
+ }
+ }
+
+ return 0, errors.New("secp256k1 record does not exist")
+}
+
+func (e *ENR) SetDiscv5(port uint32) error {
+ e.dirty = true
+ buf := new(bytes.Buffer)
+ err := binary.Write(buf, binary.BigEndian, port)
+ if err != nil {
+ return err
+ }
+
+ e.records = append(e.records, record{DISCV5, buf.Bytes()})
+
+ return nil
+}
+
+func (e *ENR) GetRaw(k []byte) ([]byte, error) {
+ for _, r := range e.records {
+ if bytes.Compare(r.k, k) == 0 {
+ return r.v, nil
+ }
+ }
+
+ return nil, errors.New("record does not exist")
+}
+
+func (e *ENR) SetRaw(k []byte, v []byte) {
+ e.dirty = true
+ e.records = append(e.records, record{k, v})
+}
+
+func (e *ENR) Encode() ([]byte, error) {
+ if e.dirty {
+ return nil, errors.New("record is not signed")
+ }
+ return e.raw, nil
+}
+
+func (e *ENR) NodeAddress() ([]byte, error) {
+ pk, err := e.GetSecp256k1()
+ if err != nil {
+ return nil, err
+ }
+
+ digest := crypto.Keccak256Hash(pk)
+
+ return digest.Bytes(), nil
+}
+
+func (e *ENR) Decode(data []byte) error {
+ if len(data) > RECORD_MAX_SIZE {
+ return errors.New("record is too big")
+ }
+
+ s := rlp.NewStream(bytes.NewReader(data), RECORD_MAX_SIZE)
+
+ signature, err := s.Bytes()
+ if err != nil {
+ return err
+ }
+
+ // consume the list prefix
+ _, err = s.List()
+ if err != nil {
+ return err
+ }
+
+ seq, err := s.Uint()
+ if err != nil {
+ return err
+ }
+
+ var records []record
+
+ // read key/value pairs until we reach rlp.EOL
+ for _, _, err = s.Kind(); err == nil; _, _, err = s.Kind() {
+ key, err2 := s.Bytes()
+ if err2 != nil {
+ return err2
+ }
+
+ value, err2 := s.Bytes()
+ if err2 != nil {
+ return err2
+ }
+
+ records = append(records, record{k: key, v: value})
+ }
+
+ if err != rlp.EOL {
+ return err
+ }
+
+ e.signature = signature
+ e.raw = data
+ e.seq = uint32(seq)
+ e.records = records
+
+ err = e.verifySignature()
+ if err != nil {
+ return err
+ }
+
+ e.dirty = false
+
+ return nil
+}
+
+func (e *ENR) Sign(privkey *ecdsa.PrivateKey) error {
+ e.seq = e.seq + 1
+
+ e.SetID(ID_SECP256k1_KECCAK)
+
+ pk := (*btcec.PublicKey)(&privkey.PublicKey).SerializeCompressed()
+ e.SetSecp256k1(pk)
+
+ var err error
+ e.signature, e.raw, err = e.SignAndEncode(privkey)
+ if err != nil {
+ return err
+ }
+
+ // mark record ready for encoding
+ e.dirty = false
+
+ return nil
+}
+
+func (e *ENR) SignAndEncode(privkey *ecdsa.PrivateKey) ([]byte, []byte, error) {
+ content, err := e.SerialisedContent()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ digest := crypto.Keccak256Hash(content)
+
+ signature, err := crypto.Sign(digest.Bytes(), privkey)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ blob, err := rlp.EncodeToBytes(signature)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ raw := append(blob, content...)
+
+ return signature, raw, nil
+}
+
+func (e *ENR) SerialisedContent() ([]byte, error) {
+ var buffer bytes.Buffer
+
+ blob, err := rlp.EncodeToBytes(e.seq)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = buffer.Write(blob)
+ if err != nil {
+ return nil, err
+ }
+
+ sort.Slice(e.records, func(i, j int) bool {
+ return bytes.Compare(e.records[i].k, e.records[j].k) < 0
+ })
+
+ for _, r := range e.records {
+ kk, err := rlp.EncodeToBytes(r.k)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = buffer.Write(kk)
+ if err != nil {
+ return nil, err
+ }
+
+ vv, err := rlp.EncodeToBytes(r.v)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = buffer.Write(vv)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return wrapList(buffer.Bytes()), nil
+}
+
+func (e *ENR) verifySignature() error {
+ id, err := e.GetID()
+ if err != nil {
+ return err
+ }
+
+ // currently "secp256k1-keccak" is the only known identity scheme
+ if id != ID_SECP256k1_KECCAK {
+ return errors.New("unknown identity scheme")
+ }
+
+ // get publickey from record
+ blob, err := e.GetSecp256k1()
+ if err != nil {
+ return err
+ }
+
+ pk, err := btcec.ParsePubKey(blob, btcec.S256())
+ if err != nil {
+ return err
+ }
+ pubkey1 := pk.SerializeUncompressed()
+
+ // get publickey from message and signature
+ content, err := e.SerialisedContent()
+ if err != nil {
+ return err
+ }
+
+ digest := crypto.Keccak256Hash(content)
+ pubkey2, err := crypto.Ecrecover(digest.Bytes(), e.signature)
+ if err != nil {
+ return err
+ }
+
+ if bytes.Compare(pubkey1, pubkey2) != 0 {
+ return errors.New("public key mismatch")
+ }
+
+ return nil
+}
+
+func wrapList(c []byte) []byte {
+ head := make([]byte, 9)
+ res := rlp.LengthPrefix(head, uint64(len(c)))
+ return append(res, c...)
+}
diff --git a/p2p/enr/enr_test.go b/p2p/enr/enr_test.go
new file mode 100644
index 0000000000..a059e902dd
--- /dev/null
+++ b/p2p/enr/enr_test.go
@@ -0,0 +1,218 @@
+// 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 enr
+
+import (
+ "bytes"
+ "encoding/hex"
+ "net"
+ "reflect"
+ "testing"
+
+ "github.com/btcsuite/btcd/btcec"
+ "github.com/ethereum/go-ethereum/crypto"
+)
+
+const (
+ privkeyHex = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"
+)
+
+func TestGetSetID(t *testing.T) {
+ id := "someid"
+ e := NewENR()
+ e.SetID(id)
+
+ got, err := e.GetID()
+ if err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ if got != id {
+ t.Fatalf("got %#v, expected %#v", got, id)
+ }
+}
+
+func TestGetSetIP4(t *testing.T) {
+ ip := net.IP{192, 168, 0, 3}
+ e := NewENR()
+ e.SetIPv4(ip)
+
+ got, err := e.GetIPv4()
+ if err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ if !got.Equal(ip) {
+ t.Fatalf("got %#v, expected %#v", got, ip)
+ }
+}
+
+func TestGetSetIP6(t *testing.T) {
+ ip := net.IP{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}
+ e := NewENR()
+ e.SetIPv6(ip)
+
+ got, err := e.GetIPv6()
+ if err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ if !got.Equal(ip) {
+ t.Fatalf("got %#v, expected %#v", got, ip)
+ }
+}
+
+func TestGetSetDiscv5(t *testing.T) {
+ port := uint32(30309)
+ e := NewENR()
+
+ err := e.SetDiscv5(port)
+ if err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ got, err := e.GetDiscv5()
+ if err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ if got != port {
+ t.Fatalf("got %#v, expected %#v", got, port)
+ }
+}
+
+func TestGetSetSecp256k1(t *testing.T) {
+ privkey, err := crypto.HexToECDSA(privkeyHex)
+ if err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ e := NewENR()
+
+ err = e.Sign(privkey)
+ if err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ got, err := e.GetSecp256k1()
+ if err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ expected := (*btcec.PublicKey)(&privkey.PublicKey).SerializeCompressed()
+ if bytes.Compare(got, expected) != 0 {
+ t.Fatalf("got %#v, expected %#v", got, expected)
+ }
+}
+
+func TestDirty(t *testing.T) {
+ privkey, err := crypto.HexToECDSA(privkeyHex)
+ if err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ e := NewENR()
+
+ err = e.Sign(privkey)
+ if err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ if _, err := e.Encode(); err != nil {
+ t.Fatalf("error: %#v", err)
+ }
+
+ e.SetRaw([]byte(`some key`), []byte(`some value`))
+
+ if _, err := e.Encode(); err == nil {
+ t.Fatal("expected err, got nil")
+ }
+}
+
+func TestSignEncodeAndDecode(t *testing.T) {
+ privkey, err := crypto.HexToECDSA(privkeyHex)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e := NewENR()
+ e.SetDiscv5(30303)
+ e.SetIPv4(net.ParseIP("127.0.0.1"))
+
+ err = e.Sign(privkey)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ record, err := e.Encode()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e2 := NewENR()
+
+ err = e2.Decode(record)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !reflect.DeepEqual(e, e2) {
+ t.Errorf("got\n%#v, expected\n%#v", e2, e)
+ }
+
+ expectedRecord := "b8415571f9a36b1e26c366745894656dd1565033cdeda18d330c9a9bac67dfc3e786556b0490e509372fa9db5abf418accd895467e8ff047bbdc147789bef71a2cc401f8560186646973637635840000765f82696490736563703235366b312d6b656363616b83697034847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138"
+
+ got := hex.EncodeToString(record)
+ if got != expectedRecord {
+ t.Errorf("got\n%#v, expected\n%#v", got, expectedRecord)
+ }
+
+ blob, err := e2.Encode()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ got = hex.EncodeToString(blob)
+ if got != expectedRecord {
+ t.Errorf("got\n%#v, expected\n%#v", got, expectedRecord)
+ }
+}
+
+func TestNodeAddress(t *testing.T) {
+ privkey, err := crypto.HexToECDSA(privkeyHex)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e := NewENR()
+
+ err = e.Sign(privkey)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ addr, err := e.NodeAddress()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expected := "caaa1485d83b18b32ed9ad666026151bf0cae8a0a88c857ae2d4c5be2daa6726"
+ got := hex.EncodeToString(addr)
+ if got != expected {
+ t.Errorf("got\n%#v, expected\n%#v", got, expected)
+ }
+}
diff --git a/rlp/encode.go b/rlp/encode.go
index 44592c2f53..841a1a85f6 100644
--- a/rlp/encode.go
+++ b/rlp/encode.go
@@ -136,6 +136,10 @@ func (head *listhead) encode(buf []byte) []byte {
return buf[:puthead(buf, 0xC0, 0xF7, uint64(head.size))]
}
+func LengthPrefix(buf []byte, size uint64) []byte {
+ return buf[:puthead(buf, 0xC0, 0xF7, size)]
+}
+
// headsize returns the size of a list or string header
// for a value of the given size.
func headsize(size uint64) int {