diff --git a/p2p/enr/enr.go b/p2p/enr/enr.go index b84f3ca880..b84d5c0bfe 100644 --- a/p2p/enr/enr.go +++ b/p2p/enr/enr.go @@ -14,7 +14,17 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -// Package enr implements the Ethereum Node Record as per https://github.com/ethereum/EIPs/pull/778 +// 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 keys in a record, use the Key +// interface. +// +// Records must signed before transmitting them to another node. Decoding a record verifies +// its signature. When creating a record, set the keys you want, then call Sign to add the +// signature. Modifying a record invalidates the signature. +// +// Package enr supports the "secp256k1-keccak" identity scheme. package enr import ( @@ -35,6 +45,8 @@ import ( const SizeLimit = 300 // maximum encoded size of a node record in bytes +const ID_SECP256k1_KECCAK = ID("secp256k1-keccak") // the default identity scheme + var ( errNoID = errors.New("unknown or unspecified identity scheme") errInvalidSigsize = errors.New("invalid signature size") @@ -43,15 +55,16 @@ var ( errDuplicateKey = errors.New("record contains duplicate key") errIncompletePair = errors.New("record contains incomplete k/v pair") errTooBig = fmt.Errorf("record bigger than %d bytes", SizeLimit) + errEncodeUnsigned = errors.New("can't encode unsigned record") + errNotFound = errors.New("no such key in record") ) -// Key is implemented by known node record key types. -// -// To define a new key that is to be included in a node record, -// create a Go type that satisfies this interface. The type should -// also implement rlp.Decoder if additional checks are needed on the value. -type Key interface { - ENRKey() string +// Record represents a node record. The zero value is an empty record. +type Record struct { + seq uint32 // sequence number + signature []byte // the signature + raw []byte // RLP encoded record + pairs []pair // sorted list of all key/value pairs } // pair is a key/value pair in a record. @@ -60,36 +73,38 @@ type pair struct { v rlp.RawValue } -// Record represents Ethereum Node Record -type Record struct { - seq uint32 // sequence number - signature []byte // record's signature - raw []byte // RLP encoded record - pairs []pair // sorted list of all key/value pairs +// Signed reports whether the record has a valid signature. +func (r *Record) Signed() bool { + return r.signature != nil } -// Seq return record's sequence number -func (r Record) Seq() uint32 { +// Seq returns the sequence number. +func (r *Record) Seq() uint32 { return r.seq } -// SetSeq update record's sequence number. Nodes should increase the number whenever the record changes. +// SetSeq updates the record sequence number. This invalidates any signature on the record. +// Calling SetSeq is usually not required because signing the redord increments the +// sequence number. func (r *Record) SetSeq(s uint32) { r.signature = nil r.seq = s } -// Load is loading a key/value pair based on provided key from the record. -// It returns false if such key cannot be found. -// It returns an error if there is a problem with RLP decoding of the pair. -func (r *Record) Load(k Key) (bool, error) { +// Load retrieves the valud of a key/value pair. The given Key must be a pointer and will +// be set to the value of the key in the record. +// +// Errors returned by Load are wrapped in KeyError. You can distinguish decoding errors +// from missing keys using the IsNotFound function. +func (r *Record) Load(k Key) error { i := sort.Search(len(r.pairs), func(i int) bool { return r.pairs[i].k >= k.ENRKey() }) - if i < len(r.pairs) && r.pairs[i].k == k.ENRKey() { - return true, rlp.DecodeBytes(r.pairs[i].v, k) + if err := rlp.DecodeBytes(r.pairs[i].v, k); err != nil { + return &KeyError{Key: k.ENRKey(), Err: err} + } + return nil } - - return false, errors.New("record does not exist") + return &KeyError{Key: k.ENRKey(), Err: errNotFound} } // Set adds or updates the given key in the record. @@ -120,17 +135,17 @@ func (r *Record) Set(k Key) { r.pairs = append(r.pairs, pair{k.ENRKey(), blob}) } -// EncodeRLP implements rlp.Encoder. -// Sign must be called prior to calling rlp.Encode +// EncodeRLP implements rlp.Encoder. Encoding fails if +// the record is unsigned. func (r Record) EncodeRLP(w io.Writer) error { - if r.signature == nil { - return errors.New("record is not signed") + if !r.Signed() { + return errEncodeUnsigned } _, err := w.Write(r.raw) return err } -// DecodeRLP implements rlp.Decoder. +// DecodeRLP implements rlp.Decoder. Decoding verifies the signature. func (r *Record) DecodeRLP(s *rlp.Stream) error { raw, err := s.Raw() if err != nil { @@ -191,29 +206,24 @@ func (r *Record) DecodeRLP(s *rlp.Stream) error { return nil } -// NodeAddr returns node's address - keccak256 hash of the public key. -func (r *Record) NodeAddr() ([]byte, error) { +// NodeAddr returns the node address. The return value will be nil if the record is +// unsigned. +func (r *Record) NodeAddr() []byte { var secp256k1 Secp256k1 - - _, err := r.Load(&secp256k1) - if err != nil { - return nil, err + if r.Load(&secp256k1) != nil { + return nil } - pk := btcec.PublicKey(secp256k1) - - digest := crypto.Keccak256Hash(pk.SerializeCompressed()) - - return digest.Bytes(), nil + return crypto.Keccak256(pk.SerializeCompressed()) } -// Sign signs the record with the provided private key. -// It updates record's identity scheme and public key. -// It returns an error if signed record is bigger than SizeLimit bytes. +// Sign signs the record with the given private key. It updates the record's identity +// scheme, public key and increments the sequence number. Sign returns an error if the +// encoded record is larger than the size limit. func (r *Record) Sign(privkey *ecdsa.PrivateKey) error { pk := (*btcec.PublicKey)(&privkey.PublicKey) r.seq = r.seq + 1 - r.Set(ID(ID_SECP256k1_KECCAK)) + r.Set(ID_SECP256k1_KECCAK) r.Set(Secp256k1(*pk)) return r.signAndEncode(privkey) } @@ -246,11 +256,9 @@ func (r *Record) signAndEncode(privkey *ecdsa.PrivateKey) error { if err != nil { return err } - if len(r.raw) > SizeLimit { return errTooBig } - return nil } @@ -258,15 +266,13 @@ func (r *Record) verifySignature() error { // Get identity scheme, public key, signature. var id ID var secp256k1 Secp256k1 - if _, err := r.Load(&id); err != nil { + if err := r.Load(&id); err != nil { return err } else if id != ID_SECP256k1_KECCAK { return errNoID } - if ok, err := r.Load(&secp256k1); err != nil { + if err := r.Load(&secp256k1); err != nil { return err - } else if !ok { - return fmt.Errorf("can't verify signature: missing %q key", secp256k1.ENRKey()) } sig, err := parseCompactSignature(r.signature) if err != nil { diff --git a/p2p/enr/enr_test.go b/p2p/enr/enr_test.go index d6bafd4b65..324167ec6d 100644 --- a/p2p/enr/enr_test.go +++ b/p2p/enr/enr_test.go @@ -19,60 +19,108 @@ package enr import ( "bytes" "encoding/hex" + "fmt" "math/rand" "net" - "reflect" "testing" "time" "github.com/btcsuite/btcd/btcec" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -const ( - privkeyHex = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" +var ( + privkeyHex = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" + privkey, _ = crypto.HexToECDSA(privkeyHex) + pubkeyBytes, _ = hex.DecodeString("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138") + pubkey, _ = btcec.ParsePubKey(pubkeyBytes, btcec.S256()) ) -var rnd *rand.Rand +var rnd = rand.New(rand.NewSource(time.Now().UnixNano())) -func init() { - rnd = rand.New(rand.NewSource(time.Now().UnixNano())) -} - -// TestGetSetID tests encoding/decoding and setting/getting of the enr.ID type +// TestGetSetID tests encoding/decoding and setting/getting of the ID key. func TestGetSetID(t *testing.T) { id := ID("someid") var r Record r.Set(id) var id2 ID - - _, err := r.Load(&id2) - if err != nil { - t.Fatal(err) - } - - if id != id2 { - t.Fatalf("got %#v, expected %#v", id2, id) - } + require.NoError(t, r.Load(&id2)) + assert.Equal(t, id, id2) } -// TestGetSetIP4 tests encoding/decoding and setting/getting of the enr.IP4 type +// TestGetSetIP4 tests encoding/decoding and setting/getting of the IP4 key. func TestGetSetIP4(t *testing.T) { - ip := IP4(net.IP{192, 168, 0, 3}) + ip := IP4{192, 168, 0, 3} var r Record r.Set(ip) var ip2 IP4 + require.NoError(t, r.Load(&ip2)) + assert.Equal(t, ip, ip2) +} - _, err := r.Load(&ip2) - if err != nil { +// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP6 key. +func TestGetSetIP6(t *testing.T) { + ip := IP6{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68} + var r Record + r.Set(ip) + + var ip2 IP6 + require.NoError(t, r.Load(&ip2)) + assert.Equal(t, ip, ip2) +} + +// TestGetSetDiscPort tests encoding/decoding and setting/getting of the DiscPort key. +func TestGetSetDiscPort(t *testing.T) { + port := DiscPort(30309) + var r Record + r.Set(port) + + var port2 DiscPort + require.NoError(t, r.Load(&port2)) + 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 := r.Sign(privkey); err != nil { t.Fatal(err) } - if bytes.Compare(ip, ip2) != 0 { - t.Fatalf("got %#v, expected %#v", ip2, ip) + var pk Secp256k1 + require.NoError(t, r.Load(&pk)) + assert.EqualValues(t, pubkey, &pk) +} + +func TestLoadErrors(t *testing.T) { + var r Record + ip4 := IP4{127, 0, 0, 1} + r.Set(ip4) + + // Check error for missing keys. + var ip6 IP6 + err := r.Load(&ip6) + if !IsNotFound(err) { + t.Error("IsNotFound should return true for missing key") + } + assert.Equal(t, &KeyError{Key: ip6.ENRKey(), Err: errNotFound}, err) + + // Check error for invalid keys. + var list []uint + err = r.Load(WithKey(IP4{}.ENRKey(), &list)) + kerr, ok := err.(*KeyError) + if !ok { + t.Fatal("expected KeyError, got %T", err) + } + assert.Equal(t, kerr.Key, ip4.ENRKey()) + assert.Error(t, kerr.Err) + if IsNotFound(err) { + t.Error("IsNotFound should return false for decoding errors") } } @@ -107,103 +155,36 @@ func TestSortedGetAndSet(t *testing.T) { for i, w := range tt.want { // set got's key from r.pair[i], so that we preserve order of pairs got := pair{k: r.pairs[i].k} - if ok, err := r.Load(WithKey(w.k, &got.v)); !ok || err != nil { - t.Fatal(err) - } - - if got != w { - t.Fatalf("expected %#v, got %#v", w, got) - } + assert.NoError(t, r.Load(WithKey(w.k, &got.v))) + assert.Equal(t, w, got) } } } -// TestGetSetIP6 tests encoding/decoding and setting/getting of the enr.IP6 type -func TestGetSetIP6(t *testing.T) { - ip := IP6(net.IP{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}) - var r Record - r.Set(ip) - - var ip2 IP6 - - _, err := r.Load(&ip2) - if err != nil { - t.Fatal(err) - } - - if bytes.Compare(ip, ip2) != 0 { - t.Fatalf("got %#v, expected %#v", ip2, ip) - } -} - -// TestGetSetDiscPort tests encoding/decoding and setting/getting of the enr.DiscPort type -func TestGetSetDiscPort(t *testing.T) { - port := DiscPort(30309) - var r Record - r.Set(port) - - var port2 DiscPort - - _, err := r.Load(&port2) - if err != nil { - t.Fatal(err) - } - - if port != port2 { - t.Fatalf("got %#v, expected %#v", port2, port) - } -} - -// TestGetSetSecp256k1 tests encoding/decoding and setting/getting of the enr.Secp256k1 type -func TestGetSetSecp256k1(t *testing.T) { - privkey, err := crypto.HexToECDSA(privkeyHex) - if err != nil { - t.Fatal(err) - } - - var r Record - - err = r.Sign(privkey) - if err != nil { - t.Fatal(err) - } - - var pk Secp256k1 - - _, err = r.Load(&pk) - if err != nil { - t.Fatal(err) - } - - got := (*btcec.PublicKey)(&pk).SerializeCompressed() - expected := (*btcec.PublicKey)(&privkey.PublicKey).SerializeCompressed() - if bytes.Compare(got, expected) != 0 { - t.Fatalf("got %#v, expected %#v", got, expected) - } -} - // TestDirty tests record signature removal on setting of new key/value pair in record. func TestDirty(t *testing.T) { - privkey, err := crypto.HexToECDSA(privkeyHex) - if err != nil { - t.Fatal(err) - } - var r Record - err = r.Sign(privkey) - if err != nil { - t.Fatal(err) + 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) } - if _, err := rlp.EncodeToBytes(r); err != nil { - t.Fatal(err) + require.NoError(t, r.Sign(privkey)) + if !r.Signed() { + t.Error("Signed return false for signed record") } + _, err := rlp.EncodeToBytes(r) + assert.NoError(t, err) r.SetSeq(3) - - if _, err := rlp.EncodeToBytes(r); err == nil { - t.Fatal("expected err, got nil") + if r.Signed() { + t.Error("Signed returned true for modified record") + } + if _, err := rlp.EncodeToBytes(r); err != errEncodeUnsigned { + t.Errorf("expected errEncodeUnsigned, got %#v") } } @@ -218,88 +199,45 @@ func TestGetSetOverwrite(t *testing.T) { r.Set(ip2) var ip3 IP4 - - _, err := r.Load(&ip3) - if err != nil { - t.Fatal(err) - } - - if bytes.Compare(ip2, ip3) != 0 { - t.Fatalf("got %#v, expected %#v", ip2, ip3) - } + require.NoError(t, r.Load(&ip3)) + assert.Equal(t, ip2, ip3) } // TestSignEncodeAndDecode tests signing, RLP encoding and RLP decoding of a record. func TestSignEncodeAndDecode(t *testing.T) { - privkey, err := crypto.HexToECDSA(privkeyHex) - if err != nil { - t.Fatal(err) - } - var r Record - port := DiscPort(30303) - r.Set(port) - - ipv4 := IP4(net.ParseIP("127.0.0.1")) - r.Set(ipv4) - - err = r.Sign(privkey) - if err != nil { - t.Fatal(err) - } - + r.Set(DiscPort(30303)) + r.Set(IP4(net.ParseIP("127.0.0.1"))) + require.NoError(t, r.Sign(privkey)) blob, err := rlp.EncodeToBytes(r) if err != nil { t.Fatal(err) } var r2 Record - err = rlp.DecodeBytes(blob, &r2) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(r, r2) { - t.Errorf("records not deep equal ; got\n%#v, expected\n%#v", r2, r) - } + require.NoError(t, rlp.DecodeBytes(blob, &r2)) + assert.Equal(t, r, r2) blob2, err := rlp.EncodeToBytes(r2) if err != nil { t.Fatal(err) } - - if bytes.Compare(blob, blob2) != 0 { - t.Errorf("serialised records not equal ; got\n%#v, expected\n%#v", blob2, blob) - } + assert.Equal(t, blob, blob2) } -// TestNodeAddress tests that record returns correct node address - keccak256 hash of the public key. -func TestNodeAddress(t *testing.T) { - privkey, err := crypto.HexToECDSA(privkeyHex) - if err != nil { - t.Fatal(err) - } - +func TestNodeAddr(t *testing.T) { var r Record - - err = r.Sign(privkey) - if err != nil { - t.Fatal(err) - } - - addr, err := r.NodeAddr() - if err != nil { - t.Fatal(err) + if addr := r.NodeAddr(); addr != nil { + t.Errorf("wrong address on empty record: got %v, want %v", addr, nil) } + require.NoError(t, r.Sign(privkey)) expected := "caaa1485d83b18b32ed9ad666026151bf0cae8a0a88c857ae2d4c5be2daa6726" - got := hex.EncodeToString(addr) - if got != expected { - t.Errorf("got\n%#v, expected\n%#v", got, expected) - } + assert.Equal(t, expected, hex.EncodeToString(r.NodeAddr())) } -// TestPythonInterop tests that Go implementation can successfully RLP decode a record produced by Python implementation. +// TestPythonInterop checks that we can decode and verify a record produced by the Python +// implementation. func TestPythonInterop(t *testing.T) { enc, _ := hex.DecodeString("f896b840638a54215d80a6713c8d523a6adc4e6e73652d859103a36b700851cb0e61b66b8ebfc1a610c57d732ec6e0a8f06a9a7a28df5051ece514702ff9cdff0b11f454018664697363763582765f82696490736563703235366b312d6b656363616b83697034847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138") var r Record @@ -316,95 +254,62 @@ func TestPythonInterop(t *testing.T) { if r.Seq() != wantSeq { t.Errorf("wrong seq: got %d, want %d", r.Seq(), wantSeq) } - if addr, _ := r.NodeAddr(); !bytes.Equal(addr, wantAddr) { + if addr := r.NodeAddr(); !bytes.Equal(addr, wantAddr) { t.Errorf("wrong addr: got %x, want %x", addr, wantAddr) } want := map[Key]interface{}{new(IP4): &wantIP, new(DiscPort): &wantDiscport} for k, v := range want { - if _, err := r.Load(k); err != nil { - t.Errorf("can't load %q: %v", k.ENRKey(), err) - } else if !reflect.DeepEqual(k, v) { - t.Errorf("wrong %q: got %v, want %v", k.ENRKey(), k, v) + 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) { - privkey, err := crypto.HexToECDSA(privkeyHex) - if err != nil { - t.Fatal(err) - } - var r Record - key := randomString(10) // set a big value for random key, expect error r.Set(WithKey(key, randomString(300))) - err = r.Sign(privkey) - if err != errTooBig { + if err := r.Sign(privkey); err != errTooBig { t.Fatalf("expected to get errTooBig, got %#v", err) } // set an acceptable value for random key, expect no error r.Set(WithKey(key, randomString(100))) - err = r.Sign(privkey) - if err != nil { - t.Fatal(err) - } + require.NoError(t, r.Sign(privkey)) } // TestSignEncodeAndDecodeRandom tests encoding/decoding of records containing random key/value pairs. func TestSignEncodeAndDecodeRandom(t *testing.T) { - privkey, err := crypto.HexToECDSA(privkeyHex) - if err != nil { - t.Fatal(err) - } - var r Record // random key/value pairs for testing pairs := map[string]uint32{} - for i := 0; i < 10; i++ { key := randomString(7) value := rnd.Uint32() - - pair := WithKey(key, &value) - r.Set(pair) - pairs[key] = value + r.Set(WithKey(key, &value)) } - if r.Sign(privkey); err != nil { - t.Fatal(err) - } - - _, err = rlp.EncodeToBytes(r) - if err != nil { - t.Fatal(err) - } + require.NoError(t, r.Sign(privkey)) + _, err := rlp.EncodeToBytes(r) + require.NoError(t, err) for k, v := range pairs { + desc := fmt.Sprintf("key %q", k) var got uint32 buf := WithKey(k, &got) - - if ok, err := r.Load(buf); !ok || err != nil { - t.Fatal(ok, err) - } - - if got != v { - t.Fatalf("got %#v, expected %#v", got, v) - } + require.NoError(t, r.Load(buf), desc) + require.Equal(t, v, got, desc) } } func randomString(strlen int) string { - const chars = "abcdefghijklmnopqrstuvwxyz0123456789" - result := make([]byte, strlen) - for i := range result { - result[i] = chars[rnd.Intn(len(chars))] - } - return string(result) + b := make([]byte, strlen) + rnd.Read(b) + return string(b) } diff --git a/p2p/enr/keys.go b/p2p/enr/keys.go index 54660d078a..12bf6d8fb0 100644 --- a/p2p/enr/keys.go +++ b/p2p/enr/keys.go @@ -26,14 +26,21 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) +// Key is implemented by known node record key types. +// +// To define a new key that is to be included in a node record, +// create a Go type that satisfies this interface. The type should +// also implement rlp.Decoder if additional checks are needed on the value. +type Key interface { + ENRKey() string +} + type generic struct { key string value interface{} } -func (g generic) ENRKey() string { - return g.key -} +func (g generic) ENRKey() string { return g.key } func (g generic) EncodeRLP(w io.Writer) error { return rlp.Encode(w, g.value) @@ -43,37 +50,27 @@ func (g *generic) DecodeRLP(s *rlp.Stream) error { return s.Decode(g.value) } -// WithKey returns a new Key that can be set in a Record. -// v must implement the rlp.Encoder and rlp.Decoder interface. +// WithKey wraps any value with a key name. It can be used to set and load arbitrary values +// in a record. The value v must be supported by rlp. To use WithKey with Load, the value +// must be a pointer. func WithKey(k string, v interface{}) Key { return &generic{key: k, value: v} } -// DiscPort represents an UDP port for discovery v5. +// DiscPort is the "discv5" key, which holds the UDP port for discovery v5. type DiscPort uint16 -// ENRKey returns the node record key for an UDP port for discovery. -func (DiscPort) ENRKey() string { - return "discv5" -} +func (v DiscPort) ENRKey() string { return "discv5" } -const ID_SECP256k1_KECCAK = "secp256k1-keccak" // identity scheme identifier - -// ID is the name of the identity scheme, e.g. "secp256k1-keccak". +// ID is the "id" key, which holds the name of the identity scheme. type ID string -// ENRKey returns the node record key for its identity scheme. -func (ID) ENRKey() string { - return "id" -} +func (v ID) ENRKey() string { return "id" } -// IP4 represents an 4-byte IPv4 address in a node record. +// IP4 is the "ip4" key, which holds a 4-byte IPv4 address. type IP4 net.IP -// ENRKey returns the node record key for an IPv4 address. -func (IP4) ENRKey() string { - return "ip4" -} +func (v IP4) ENRKey() string { return "ip4" } // EncodeRLP implements rlp.Encoder. func (v IP4) EncodeRLP(w io.Writer) error { @@ -95,13 +92,10 @@ func (v *IP4) DecodeRLP(s *rlp.Stream) error { return nil } -// IP6 represents an 16-byte IPv6 address in a node record. +// IP6 is the "ip6" key, which holds a 16-byte IPv6 address. type IP6 net.IP -// ENRKey returns the node record key for an IPv6 address. -func (IP6) ENRKey() string { - return "ip6" -} +func (v IP6) ENRKey() string { return "ip6" } // EncodeRLP implements rlp.Encoder. func (v IP6) EncodeRLP(w io.Writer) error { @@ -120,13 +114,10 @@ func (v *IP6) DecodeRLP(s *rlp.Stream) error { return nil } -// Secp256k1 is compressed secp256k1 public key. +// Secp256k1 is the "secp256k1" key, which holds a public key. type Secp256k1 ecdsa.PublicKey -// ENRKey returns the node record key for the secp256k1 public key. -func (Secp256k1) ENRKey() string { - return "secp256k1" -} +func (v Secp256k1) ENRKey() string { return "secp256k1" } // EncodeRLP implements rlp.Encoder. func (v Secp256k1) EncodeRLP(w io.Writer) error { @@ -151,3 +142,24 @@ func (v *Secp256k1) DecodeRLP(s *rlp.Stream) error { return nil } + +// KeyError is an error related to a key. +type KeyError struct { + Key string + Err error +} + +// Error implements error. +func (err *KeyError) Error() string { + if err.Err == errNotFound { + return fmt.Sprintf("missing ENR key %q", err.Key) + } + return fmt.Sprintf("ENR key %q: %v", err.Key, err.Err) +} + +// IsNotFound reports whether the given error means that a key/value pair is +// missing from a record. +func IsNotFound(err error) bool { + kerr, ok := err.(*KeyError) + return ok && kerr.Err == errNotFound +}