diff --git a/p2p/enr/enr.go b/p2p/enr/enr.go
index c018895cc..48683471d 100644
--- a/p2p/enr/enr.go
+++ b/p2p/enr/enr.go
@@ -29,21 +29,16 @@ package enr
import (
"bytes"
- "crypto/ecdsa"
"errors"
"fmt"
"io"
"sort"
- "github.com/ethereum/go-ethereum/crypto"
- "github.com/ethereum/go-ethereum/crypto/sha3"
"github.com/ethereum/go-ethereum/rlp"
)
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")
errInvalidSig = errors.New("invalid signature")
@@ -80,8 +75,8 @@ func (r *Record) Seq() uint64 {
}
// 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.
+// Calling SetSeq is usually not required because setting any key in a signed record
+// increments the sequence number.
func (r *Record) SetSeq(s uint64) {
r.signature = nil
r.raw = nil
@@ -104,33 +99,42 @@ func (r *Record) Load(e Entry) error {
return &KeyError{Key: e.ENRKey(), Err: errNotFound}
}
-// Set adds or updates the given entry in the record.
-// It panics if the value can't be encoded.
+// Set adds or updates the given entry in the record. It panics if the value can't be
+// encoded. If the record is signed, Set increments the sequence number and invalidates
+// the sequence number.
func (r *Record) Set(e Entry) {
- r.signature = nil
- r.raw = nil
blob, err := rlp.EncodeToBytes(e)
if err != nil {
panic(fmt.Errorf("enr: can't encode %s: %v", e.ENRKey(), err))
}
+ r.invalidate()
- i := sort.Search(len(r.pairs), func(i int) bool { return r.pairs[i].k >= e.ENRKey() })
-
- if i < len(r.pairs) && r.pairs[i].k == e.ENRKey() {
+ pairs := make([]pair, len(r.pairs))
+ copy(pairs, r.pairs)
+ i := sort.Search(len(pairs), func(i int) bool { return pairs[i].k >= e.ENRKey() })
+ switch {
+ case i < len(pairs) && pairs[i].k == e.ENRKey():
// element is present at r.pairs[i]
- r.pairs[i].v = blob
- return
- } else if i < len(r.pairs) {
+ pairs[i].v = blob
+ case i < len(r.pairs):
// insert pair before i-th elem
el := pair{e.ENRKey(), blob}
- r.pairs = append(r.pairs, pair{})
- copy(r.pairs[i+1:], r.pairs[i:])
- r.pairs[i] = el
- return
+ pairs = append(pairs, pair{})
+ copy(pairs[i+1:], pairs[i:])
+ pairs[i] = el
+ default:
+ // element should be placed at the end of r.pairs
+ pairs = append(pairs, pair{e.ENRKey(), blob})
}
+ r.pairs = pairs
+}
- // element should be placed at the end of r.pairs
- r.pairs = append(r.pairs, pair{e.ENRKey(), blob})
+func (r *Record) invalidate() {
+ if r.signature == nil {
+ r.seq++
+ }
+ r.signature = nil
+ r.raw = nil
}
// EncodeRLP implements rlp.Encoder. Encoding fails if
@@ -196,39 +200,55 @@ func (r *Record) DecodeRLP(s *rlp.Stream) error {
return err
}
- // Verify signature.
- if err = dec.verifySignature(); err != nil {
+ _, scheme := dec.idScheme()
+ if scheme == nil {
+ return errNoID
+ }
+ if err := scheme.Verify(&dec, dec.signature); err != nil {
return err
}
*r = dec
return nil
}
-type s256raw []byte
-
-func (s256raw) ENRKey() string { return "secp256k1" }
-
// NodeAddr returns the node address. The return value will be nil if the record is
-// unsigned.
+// unsigned or uses an unknown identity scheme.
func (r *Record) NodeAddr() []byte {
- var entry s256raw
- if r.Load(&entry) != nil {
+ _, scheme := r.idScheme()
+ if scheme == nil {
return nil
}
- return crypto.Keccak256(entry)
+ return scheme.NodeAddr(r)
}
-// 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 {
- r.seq = r.seq + 1
- r.Set(ID_SECP256k1_KECCAK)
- r.Set(Secp256k1(privkey.PublicKey))
- return r.signAndEncode(privkey)
+// 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)
+ }
+ 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
}
-func (r *Record) appendPairs(list []interface{}) []interface{} {
+// AppendElements appends the sequence number and entries to the given slice.
+func (r *Record) AppendElements(list []interface{}) []interface{} {
list = append(list, r.seq)
for _, p := range r.pairs {
list = append(list, p.k, p.v)
@@ -236,54 +256,23 @@ func (r *Record) appendPairs(list []interface{}) []interface{} {
return list
}
-func (r *Record) signAndEncode(privkey *ecdsa.PrivateKey) error {
- // Put record elements into a flat list. Leave room for the signature.
- list := make([]interface{}, 1, len(r.pairs)*2+2)
- list = r.appendPairs(list)
-
- // Sign the tail of the list.
- h := sha3.NewKeccak256()
- rlp.Encode(h, list[1:])
- sig, err := crypto.Sign(h.Sum(nil), privkey)
- if err != nil {
- return err
- }
- sig = sig[:len(sig)-1] // remove v
-
- // Put signature in front.
- r.signature, list[0] = sig, sig
- r.raw, err = rlp.EncodeToBytes(list)
- if err != nil {
- return err
+func (r *Record) encode(sig []byte) (raw []byte, err error) {
+ list := make([]interface{}, 1, 2*len(r.pairs)+1)
+ list[0] = sig
+ list = r.AppendElements(list)
+ if raw, err = rlp.EncodeToBytes(list); err != nil {
+ return nil, err
}
- if len(r.raw) > SizeLimit {
- return errTooBig
+ if len(raw) > SizeLimit {
+ return nil, errTooBig
}
- return nil
+ return raw, nil
}
-func (r *Record) verifySignature() error {
- // Get identity scheme, public key, signature.
+func (r *Record) idScheme() (string, IdentityScheme) {
var id ID
- var entry s256raw
if err := r.Load(&id); err != nil {
- return err
- } else if id != ID_SECP256k1_KECCAK {
- return errNoID
+ return "", nil
}
- if err := r.Load(&entry); err != nil {
- return err
- } else if len(entry) != 33 {
- return fmt.Errorf("invalid public key")
- }
-
- // Verify the signature.
- list := make([]interface{}, 0, len(r.pairs)*2+1)
- list = r.appendPairs(list)
- h := sha3.NewKeccak256()
- rlp.Encode(h, list)
- if !crypto.VerifySignature(entry, h.Sum(nil), r.signature) {
- return errInvalidSig
- }
- return nil
+ return string(id), FindIdentityScheme(string(id))
}
diff --git a/p2p/enr/enr_test.go b/p2p/enr/enr_test.go
index ce7767d10..d1d088756 100644
--- a/p2p/enr/enr_test.go
+++ b/p2p/enr/enr_test.go
@@ -54,35 +54,35 @@ func TestGetSetID(t *testing.T) {
assert.Equal(t, id, id2)
}
-// TestGetSetIP4 tests encoding/decoding and setting/getting of the IP4 key.
+// TestGetSetIP4 tests encoding/decoding and setting/getting of the IP key.
func TestGetSetIP4(t *testing.T) {
- ip := IP4{192, 168, 0, 3}
+ ip := IP{192, 168, 0, 3}
var r Record
r.Set(ip)
- var ip2 IP4
+ var ip2 IP
require.NoError(t, r.Load(&ip2))
assert.Equal(t, ip, ip2)
}
-// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP6 key.
+// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP key.
func TestGetSetIP6(t *testing.T) {
- ip := IP6{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}
+ ip := 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
+ var ip2 IP
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)
+func TestGetSetUDP(t *testing.T) {
+ port := UDP(30309)
var r Record
r.Set(port)
- var port2 DiscPort
+ var port2 UDP
require.NoError(t, r.Load(&port2))
assert.Equal(t, port, port2)
}
@@ -90,7 +90,7 @@ func TestGetSetDiscPort(t *testing.T) {
// 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 {
+ if err := SignV4(&r, privkey); err != nil {
t.Fatal(err)
}
@@ -101,16 +101,16 @@ func TestGetSetSecp256k1(t *testing.T) {
func TestLoadErrors(t *testing.T) {
var r Record
- ip4 := IP4{127, 0, 0, 1}
+ ip4 := IP{127, 0, 0, 1}
r.Set(ip4)
// Check error for missing keys.
- var ip6 IP6
- err := r.Load(&ip6)
+ var udp UDP
+ err := r.Load(&udp)
if !IsNotFound(err) {
t.Error("IsNotFound should return true for missing key")
}
- assert.Equal(t, &KeyError{Key: ip6.ENRKey(), Err: errNotFound}, err)
+ assert.Equal(t, &KeyError{Key: udp.ENRKey(), Err: errNotFound}, err)
// Check error for invalid keys.
var list []uint
@@ -174,7 +174,7 @@ func TestDirty(t *testing.T) {
t.Errorf("expected errEncodeUnsigned, got %#v", err)
}
- require.NoError(t, r.Sign(privkey))
+ require.NoError(t, SignV4(&r, privkey))
if !r.Signed() {
t.Error("Signed return false for signed record")
}
@@ -194,13 +194,13 @@ func TestDirty(t *testing.T) {
func TestGetSetOverwrite(t *testing.T) {
var r Record
- ip := IP4{192, 168, 0, 3}
+ ip := IP{192, 168, 0, 3}
r.Set(ip)
- ip2 := IP4{192, 168, 0, 4}
+ ip2 := IP{192, 168, 0, 4}
r.Set(ip2)
- var ip3 IP4
+ var ip3 IP
require.NoError(t, r.Load(&ip3))
assert.Equal(t, ip2, ip3)
}
@@ -208,9 +208,9 @@ func TestGetSetOverwrite(t *testing.T) {
// TestSignEncodeAndDecode tests signing, RLP encoding and RLP decoding of a record.
func TestSignEncodeAndDecode(t *testing.T) {
var r Record
- r.Set(DiscPort(30303))
- r.Set(IP4{127, 0, 0, 1})
- require.NoError(t, r.Sign(privkey))
+ r.Set(UDP(30303))
+ r.Set(IP{127, 0, 0, 1})
+ require.NoError(t, SignV4(&r, privkey))
blob, err := rlp.EncodeToBytes(r)
require.NoError(t, err)
@@ -230,12 +230,12 @@ func TestNodeAddr(t *testing.T) {
t.Errorf("wrong address on empty record: got %v, want %v", addr, nil)
}
- require.NoError(t, r.Sign(privkey))
- expected := "caaa1485d83b18b32ed9ad666026151bf0cae8a0a88c857ae2d4c5be2daa6726"
+ require.NoError(t, SignV4(&r, privkey))
+ expected := "a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7"
assert.Equal(t, expected, hex.EncodeToString(r.NodeAddr()))
}
-var pyRecord, _ = hex.DecodeString("f896b840954dc36583c1f4b69ab59b1375f362f06ee99f3723cd77e64b6de6d211c27d7870642a79d4516997f94091325d2a7ca6215376971455fb221d34f35b277149a1018664697363763582765f82696490736563703235366b312d6b656363616b83697034847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138")
+var pyRecord, _ = hex.DecodeString("f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f")
// TestPythonInterop checks that we can decode and verify a record produced by the Python
// implementation.
@@ -246,10 +246,10 @@ func TestPythonInterop(t *testing.T) {
}
var (
- wantAddr, _ = hex.DecodeString("caaa1485d83b18b32ed9ad666026151bf0cae8a0a88c857ae2d4c5be2daa6726")
- wantSeq = uint64(1)
- wantIP = IP4{127, 0, 0, 1}
- wantDiscport = DiscPort(30303)
+ 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)
@@ -257,7 +257,7 @@ func TestPythonInterop(t *testing.T) {
if addr := r.NodeAddr(); !bytes.Equal(addr, wantAddr) {
t.Errorf("wrong addr: got %x, want %x", addr, wantAddr)
}
- want := map[Entry]interface{}{new(IP4): &wantIP, new(DiscPort): &wantDiscport}
+ 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) {
@@ -272,14 +272,14 @@ func TestRecordTooBig(t *testing.T) {
key := randomString(10)
// set a big value for random key, expect error
- r.Set(WithEntry(key, randomString(300)))
- if err := r.Sign(privkey); err != errTooBig {
+ r.Set(WithEntry(key, randomString(SizeLimit)))
+ if err := SignV4(&r, privkey); 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, r.Sign(privkey))
+ require.NoError(t, SignV4(&r, privkey))
}
// TestSignEncodeAndDecodeRandom tests encoding/decoding of records containing random key/value pairs.
@@ -295,7 +295,7 @@ func TestSignEncodeAndDecodeRandom(t *testing.T) {
r.Set(WithEntry(key, &value))
}
- require.NoError(t, r.Sign(privkey))
+ require.NoError(t, SignV4(&r, privkey))
_, err := rlp.EncodeToBytes(r)
require.NoError(t, err)
diff --git a/p2p/enr/entries.go b/p2p/enr/entries.go
index 7591e6eff..71c7653a2 100644
--- a/p2p/enr/entries.go
+++ b/p2p/enr/entries.go
@@ -57,59 +57,43 @@ func WithEntry(k string, v interface{}) Entry {
return &generic{key: k, value: v}
}
-// DiscPort is the "discv5" key, which holds the UDP port for discovery v5.
-type DiscPort uint16
+// TCP is the "tcp" key, which holds the TCP port of the node.
+type TCP uint16
-func (v DiscPort) ENRKey() string { return "discv5" }
+func (v TCP) ENRKey() string { return "tcp" }
+
+// UDP is the "udp" key, which holds the UDP port of the node.
+type UDP uint16
+
+func (v UDP) ENRKey() string { return "udp" }
// ID is the "id" key, which holds the name of the identity scheme.
type ID string
+const IDv4 = ID("v4") // the default identity scheme
+
func (v ID) ENRKey() string { return "id" }
-// IP4 is the "ip4" key, which holds a 4-byte IPv4 address.
-type IP4 net.IP
+// IP is the "ip" key, which holds the IP address of the node.
+type IP net.IP
-func (v IP4) ENRKey() string { return "ip4" }
+func (v IP) ENRKey() string { return "ip" }
// EncodeRLP implements rlp.Encoder.
-func (v IP4) EncodeRLP(w io.Writer) error {
- ip4 := net.IP(v).To4()
- if ip4 == nil {
- return fmt.Errorf("invalid IPv4 address: %v", v)
- }
- return rlp.Encode(w, ip4)
-}
-
-// DecodeRLP implements rlp.Decoder.
-func (v *IP4) DecodeRLP(s *rlp.Stream) error {
- if err := s.Decode((*net.IP)(v)); err != nil {
- return err
- }
- if len(*v) != 4 {
- return fmt.Errorf("invalid IPv4 address, want 4 bytes: %v", *v)
+func (v IP) EncodeRLP(w io.Writer) error {
+ if ip4 := net.IP(v).To4(); ip4 != nil {
+ return rlp.Encode(w, ip4)
}
- return nil
-}
-
-// IP6 is the "ip6" key, which holds a 16-byte IPv6 address.
-type IP6 net.IP
-
-func (v IP6) ENRKey() string { return "ip6" }
-
-// EncodeRLP implements rlp.Encoder.
-func (v IP6) EncodeRLP(w io.Writer) error {
- ip6 := net.IP(v)
- return rlp.Encode(w, ip6)
+ return rlp.Encode(w, net.IP(v))
}
// DecodeRLP implements rlp.Decoder.
-func (v *IP6) DecodeRLP(s *rlp.Stream) error {
+func (v *IP) DecodeRLP(s *rlp.Stream) error {
if err := s.Decode((*net.IP)(v)); err != nil {
return err
}
- if len(*v) != 16 {
- return fmt.Errorf("invalid IPv6 address, want 16 bytes: %v", *v)
+ if len(*v) != 4 && len(*v) != 16 {
+ return fmt.Errorf("invalid IP address, want 4 or 16 bytes: %v", *v)
}
return nil
}
diff --git a/p2p/enr/idscheme.go b/p2p/enr/idscheme.go
new file mode 100644
index 000000000..efaf68041
--- /dev/null
+++ b/p2p/enr/idscheme.go
@@ -0,0 +1,114 @@
+// 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"
+ "fmt"
+ "sync"
+
+ "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/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
+}
+
+// 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)
+}
+
+// v4ID is the "v4" identity scheme.
+type v4ID struct{}
+
+func init() {
+ RegisterIdentityScheme("v4", v4ID{})
+}
+
+// SignV4 signs a record using the v4 scheme.
+func SignV4(r *Record, privkey *ecdsa.PrivateKey) error {
+ // Copy r to avoid modifying it if signing fails.
+ cpy := *r
+ cpy.Set(ID("v4"))
+ cpy.Set(Secp256k1(privkey.PublicKey))
+
+ h := sha3.NewKeccak256()
+ rlp.Encode(h, cpy.AppendElements(nil))
+ sig, err := crypto.Sign(h.Sum(nil), privkey)
+ if err != nil {
+ return err
+ }
+ sig = sig[:len(sig)-1] // remove v
+ if err = cpy.SetSig("v4", 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 {
+ var entry s256raw
+ if err := r.Load(&entry); err != nil {
+ return err
+ } else if len(entry) != 33 {
+ return fmt.Errorf("invalid public key")
+ }
+
+ h := sha3.NewKeccak256()
+ rlp.Encode(h, r.AppendElements(nil))
+ if !crypto.VerifySignature(entry, h.Sum(nil), sig) {
+ return errInvalidSig
+ }
+ return nil
+}
+
+func (v4ID) NodeAddr(r *Record) []byte {
+ var pubkey Secp256k1
+ err := r.Load(&pubkey)
+ if err != nil {
+ return nil
+ }
+ buf := make([]byte, 64)
+ math.ReadBits(pubkey.X, buf[:32])
+ math.ReadBits(pubkey.Y, buf[32:])
+ return crypto.Keccak256(buf)
+}
diff --git a/p2p/enr/idscheme_test.go b/p2p/enr/idscheme_test.go
new file mode 100644
index 000000000..d790e12f1
--- /dev/null
+++ b/p2p/enr/idscheme_test.go
@@ -0,0 +1,36 @@
+// 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)
+ }
+}