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 {