Skip to content

Commit b05b09a

Browse files
Extends cryptographic keys API, reduce frequency of OnMessageSent being called, improve cache locality of Kademlia routing table. (#274)
* client, node: have OnMessageSent called less frequently kademlia/protocol, kademlia/table: switch from using a slice to a linked list for representing the routing table to make id updates more efficient, and add Last() to get the last entry of a bucket near a target public key client, node, keys: add LoadKeysFromHex to load a private key from hex, add Signature type to represent cryptographic signatures, and add UnmarshalSignature which uses unsafe hackery to convert a byte slice into a signature client: add missing check for length of overlay handshake * client: use pkg-local constants instead of constants from ed25519
1 parent 274f07b commit b05b09a

File tree

6 files changed

+135
-55
lines changed

6 files changed

+135
-55
lines changed

client.go

+27-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"encoding/hex"
1010
"errors"
1111
"fmt"
12-
"github.com/oasislabs/ed25519"
1312
"go.uber.org/zap"
1413
"io"
1514
"net"
@@ -411,7 +410,9 @@ func (c *Client) handshake() {
411410

412411
// Send our Ed25519 ephemeral public key and signature of the message '.__noise_handshake'.
413412

414-
if err := c.write(append(pub[:], sec.Sign([]byte(".__noise_handshake"))...)); err != nil {
413+
signature := sec.Sign([]byte(".__noise_handshake"))
414+
415+
if err := c.write(append(pub[:], signature[:]...)); err != nil {
415416
c.reportError(fmt.Errorf("failed to send session handshake: %w", err))
416417
return
417418
}
@@ -424,21 +425,21 @@ func (c *Client) handshake() {
424425
return
425426
}
426427

427-
if len(data) != ed25519.PublicKeySize+ed25519.SignatureSize {
428+
if len(data) != SizePublicKey+SizeSignature {
428429
c.reportError(fmt.Errorf("received invalid number of bytes opening a session: expected %d byte(s), but got %d byte(s)",
429-
ed25519.PublicKeySize+ed25519.SignatureSize,
430+
SizePublicKey+SizeSignature,
430431
len(data),
431432
))
432433

433434
return
434435
}
435436

436437
var peerPublicKey PublicKey
437-
copy(peerPublicKey[:], data[:ed25519.PublicKeySize])
438+
copy(peerPublicKey[:], data[:SizePublicKey])
438439

439440
// Verify ownership of our peers Ed25519 public key by verifying the signature they sent.
440441

441-
if !peerPublicKey.Verify([]byte(".__noise_handshake"), data[ed25519.PublicKeySize:]) {
442+
if !peerPublicKey.Verify([]byte(".__noise_handshake"), UnmarshalSignature(data[SizePublicKey:SizePublicKey+SizeSignature])) {
442443
c.reportError(errors.New("could not verify session handshake"))
443444
return
444445
}
@@ -472,7 +473,8 @@ func (c *Client) handshake() {
472473
// Send to our peer our overlay ID.
473474

474475
buf := c.node.id.Marshal()
475-
buf = append(buf, c.node.Sign(append(buf, shared...))...)
476+
signature = c.node.Sign(append(buf, shared...))
477+
buf = append(buf, signature[:]...)
476478

477479
if err := c.write(buf); err != nil {
478480
c.reportError(fmt.Errorf("failed to send session handshake: %w", err))
@@ -498,7 +500,16 @@ func (c *Client) handshake() {
498500
buf = make([]byte, id.Size())
499501
copy(buf, data)
500502

501-
if !id.ID.Verify(append(buf, shared...), data[len(buf):]) {
503+
if len(data) != len(buf)+SizeSignature {
504+
c.reportError(fmt.Errorf("received invalid number of bytes handshaking: expected %d byte(s), got %d byte(s)",
505+
len(buf)+SizeSignature,
506+
len(data),
507+
))
508+
509+
return
510+
}
511+
512+
if !id.ID.Verify(append(buf, shared...), UnmarshalSignature(data[len(buf):len(buf)+SizeSignature])) {
502513
c.reportError(errors.New("overlay handshake signature is malformed"))
503514
return
504515
}
@@ -594,6 +605,14 @@ func (c *Client) writeLoop() {
594605

595606
break
596607
}
608+
609+
for _, protocol := range c.node.protocols {
610+
if protocol.OnMessageSent == nil {
611+
continue
612+
}
613+
614+
protocol.OnMessageSent(c)
615+
}
597616
}
598617
}
599618

kademlia/protocol.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ func (p *Protocol) Ack(id noise.ID) {
103103
return
104104
}
105105

106-
bucket := p.table.Bucket(id.ID)
107-
last := bucket[len(bucket)-1]
106+
last := p.table.Last(id.ID)
108107

109108
ctx, cancel := context.WithTimeout(context.Background(), p.pingTimeout)
110109
pong, err := p.node.RequestMessage(ctx, last.Address, Ping{})

kademlia/table.go

+49-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kademlia
22

33
import (
4+
"container/list"
45
"fmt"
56
"github.com/perlin-network/noise"
67
"sync"
@@ -10,7 +11,7 @@ import (
1011
type Table struct {
1112
sync.RWMutex
1213

13-
entries [noise.SizePublicKey * 8][]noise.ID
14+
entries [noise.SizePublicKey * 8]*list.List
1415
self noise.ID
1516
size int
1617
}
@@ -20,6 +21,10 @@ type Table struct {
2021
func NewTable(self noise.ID) *Table {
2122
table := &Table{self: self}
2223

24+
for i := 0; i < len(table.entries); i++ {
25+
table.entries[i] = list.New()
26+
}
27+
2328
if _, err := table.Update(self); err != nil {
2429
panic(err)
2530
}
@@ -32,12 +37,27 @@ func (t *Table) Self() noise.ID {
3237
return t.self
3338
}
3439

35-
// Bucket returns all IDs in the bucket where target resides within.
40+
// Last returns the last id of the bucket where target resides within.
41+
func (t *Table) Last(target noise.PublicKey) noise.ID {
42+
t.RLock()
43+
defer t.RUnlock()
44+
45+
return t.entries[t.getBucketIndex(target)].Back().Value.(noise.ID)
46+
}
47+
48+
// Bucket returns all entries of a bucket where target reside within.
3649
func (t *Table) Bucket(target noise.PublicKey) []noise.ID {
3750
t.RLock()
3851
defer t.RUnlock()
3952

40-
return t.entries[t.getBucketIndex(target)]
53+
bucket := t.entries[t.getBucketIndex(target)]
54+
entries := make([]noise.ID, 0, bucket.Len())
55+
56+
for e := bucket.Front(); e != nil; e = e.Next() {
57+
entries = append(entries, e.Value.(noise.ID))
58+
}
59+
60+
return entries
4161
}
4262

4363
// Update attempts to insert the target node/peer ID into this routing table. If the bucket it was expected
@@ -52,17 +72,17 @@ func (t *Table) Update(target noise.ID) (bool, error) {
5272
t.Lock()
5373
defer t.Unlock()
5474

55-
idx := t.getBucketIndex(target.ID)
75+
bucket := t.entries[t.getBucketIndex(target.ID)]
5676

57-
for i, id := range t.entries[idx] {
58-
if id.ID == target.ID { // Found the target ID already inside the routing table.
59-
t.entries[idx] = append(append([]noise.ID{target}, t.entries[idx][:i]...), t.entries[idx][i+1:]...)
77+
for e := bucket.Front(); e != nil; e = e.Next() {
78+
if e.Value.(noise.ID).ID == target.ID { // Found the target ID already inside the routing table.
79+
bucket.MoveToFront(e)
6080
return false, nil
6181
}
6282
}
6383

64-
if len(t.entries[idx]) < BucketSize { // The bucket is not yet under full capacity.
65-
t.entries[idx] = append([]noise.ID{target}, t.entries[idx]...)
84+
if bucket.Len() < BucketSize { // The bucket is not yet under full capacity.
85+
bucket.PushFront(target)
6686
t.size++
6787
return true, nil
6888
}
@@ -77,8 +97,10 @@ func (t *Table) Recorded(target noise.PublicKey) bool {
7797
t.RLock()
7898
defer t.RUnlock()
7999

80-
for _, id := range t.entries[t.getBucketIndex(target)] {
81-
if id.ID == target {
100+
bucket := t.entries[t.getBucketIndex(target)]
101+
102+
for e := bucket.Front(); e != nil; e = e.Next() {
103+
if e.Value.(noise.ID).ID == target {
82104
return true
83105
}
84106
}
@@ -92,11 +114,13 @@ func (t *Table) Delete(target noise.PublicKey) (noise.ID, bool) {
92114
t.Lock()
93115
defer t.Unlock()
94116

95-
idx := t.getBucketIndex(target)
117+
bucket := t.entries[t.getBucketIndex(target)]
118+
119+
for e := bucket.Front(); e != nil; e = e.Next() {
120+
id := e.Value.(noise.ID)
96121

97-
for i, id := range t.entries[idx] {
98122
if id.ID == target {
99-
t.entries[idx] = append(t.entries[idx][:i], t.entries[idx][i+1:]...)
123+
bucket.Remove(e)
100124
t.size--
101125
return id, true
102126
}
@@ -111,10 +135,12 @@ func (t *Table) DeleteByAddress(target string) (noise.ID, bool) {
111135
t.Lock()
112136
defer t.Unlock()
113137

114-
for i, bucket := range t.entries {
115-
for j, id := range bucket {
138+
for _, bucket := range t.entries {
139+
for e := bucket.Front(); e != nil; e = e.Next() {
140+
id := e.Value.(noise.ID)
141+
116142
if id.Address == target {
117-
t.entries[i] = append(t.entries[i][:j], t.entries[i][j+1:]...)
143+
bucket.Remove(e)
118144
t.size--
119145
return id, true
120146
}
@@ -133,8 +159,10 @@ func (t *Table) Peers() []noise.ID {
133159
func (t *Table) FindClosest(target noise.PublicKey, k int) []noise.ID {
134160
var closest []noise.ID
135161

136-
f := func(bucket []noise.ID) {
137-
for _, id := range bucket {
162+
f := func(bucket *list.List) {
163+
for e := bucket.Front(); e != nil; e = e.Next() {
164+
id := e.Value.(noise.ID)
165+
138166
if id.ID != target {
139167
closest = append(closest, id)
140168
}
@@ -175,8 +203,8 @@ func (t *Table) Entries() []noise.ID {
175203
entries := make([]noise.ID, 0, t.size)
176204

177205
for _, bucket := range t.entries {
178-
for _, id := range bucket {
179-
entries = append(entries, id)
206+
for e := bucket.Front(); e != nil; e = e.Next() {
207+
entries = append(entries, e.Value.(noise.ID))
180208
}
181209
}
182210

keys.go

+56-6
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package noise
33
import (
44
"encoding/hex"
55
"encoding/json"
6+
"fmt"
67
"github.com/oasislabs/ed25519"
78
"io"
9+
"reflect"
10+
"unsafe"
811
)
912

1013
const (
@@ -13,6 +16,9 @@ const (
1316

1417
// SizePrivateKey is the size in bytes of a nodes/peers private key.
1518
SizePrivateKey = ed25519.PrivateKeySize
19+
20+
// SizeSignature is the size in bytes of a cryptographic signature.
21+
SizeSignature = ed25519.SignatureSize
1622
)
1723

1824
type (
@@ -21,6 +27,9 @@ type (
2127

2228
// PrivateKey is the default node/peer private key type.
2329
PrivateKey [SizePrivateKey]byte
30+
31+
// Signature is the default node/peer cryptographic signature type.
32+
Signature [SizeSignature]byte
2433
)
2534

2635
var (
@@ -29,10 +38,13 @@ var (
2938

3039
// ZeroPrivateKey is the zero-value for a node/peer private key.
3140
ZeroPrivateKey PrivateKey
41+
42+
// ZeroSignature is the zero-value for a cryptographic signature.
43+
ZeroSignature Signature
3244
)
3345

3446
// GenerateKeys randomly generates a new pair of cryptographic keys. Nil may be passed to rand in order to use
35-
// crypto/rand by default. It throws an error if rand is invalid.
47+
// crypto/rand by default. It returns an error if rand is invalid.
3648
func GenerateKeys(rand io.Reader) (publicKey PublicKey, privateKey PrivateKey, err error) {
3749
pub, priv, err := ed25519.GenerateKey(rand)
3850
if err != nil {
@@ -45,9 +57,30 @@ func GenerateKeys(rand io.Reader) (publicKey PublicKey, privateKey PrivateKey, e
4557
return publicKey, privateKey, nil
4658
}
4759

60+
// LoadKeysFromHex loads a private key from a hex string. It returns an error if secretHex is not hex-encoded or is
61+
// an invalid number of bytes. In the case of the latter error, the error is wrapped as io.ErrUnexpectedEOF. Calling
62+
// this function performs 1 allocation.
63+
func LoadKeysFromHex(secretHex string) (PrivateKey, error) {
64+
secret, err := hex.DecodeString(secretHex)
65+
if err != nil {
66+
return ZeroPrivateKey, fmt.Errorf("private key provided in hex failed to be decoded: %w", err)
67+
}
68+
69+
if len(secret) != SizePrivateKey {
70+
return ZeroPrivateKey, fmt.Errorf("got private key of %d byte(s), but expected %d byte(s): %w",
71+
len(secret), SizePrivateKey, io.ErrUnexpectedEOF,
72+
)
73+
}
74+
75+
var privateKey PrivateKey
76+
copy(privateKey[:], secret)
77+
78+
return privateKey, nil
79+
}
80+
4881
// Verify returns true if the cryptographic signature of data is representative of this public key.
49-
func (k PublicKey) Verify(data, signature []byte) bool {
50-
return ed25519.Verify(k[:], data, signature)
82+
func (k PublicKey) Verify(data []byte, signature Signature) bool {
83+
return ed25519.Verify(k[:], data, signature[:])
5184
}
5285

5386
// String returns the hexadecimal representation of this public key.
@@ -61,8 +94,8 @@ func (k PublicKey) MarshalJSON() ([]byte, error) {
6194
}
6295

6396
// Sign uses this private key to sign data and return its cryptographic signature as a slice of bytes.
64-
func (k PrivateKey) Sign(data []byte) []byte {
65-
return ed25519.Sign(k[:], data)
97+
func (k PrivateKey) Sign(data []byte) Signature {
98+
return UnmarshalSignature(ed25519.Sign(k[:], data))
6699
}
67100

68101
// String returns the hexadecimal representation of this private key.
@@ -72,7 +105,7 @@ func (k PrivateKey) String() string {
72105

73106
// MarshalJSON returns the hexadecimal representation of this private key in JSON. It should never throw an error.
74107
func (k PrivateKey) MarshalJSON() ([]byte, error) {
75-
return json.Marshal(hex.EncodeToString(k[:]))
108+
return json.Marshal(k.String())
76109
}
77110

78111
// Public returns the public key associated to this private key.
@@ -82,3 +115,20 @@ func (k PrivateKey) Public() PublicKey {
82115

83116
return publicKey
84117
}
118+
119+
// String returns the hexadecimal representation of this signature.
120+
func (s Signature) String() string {
121+
return hex.EncodeToString(s[:])
122+
}
123+
124+
// MarshalJSON returns the hexadecimal representation of this signature in JSON. It should never throw an error.
125+
func (s Signature) MarshalJSON() ([]byte, error) {
126+
return json.Marshal(s.String())
127+
}
128+
129+
// UnmarshalSignature decodes data into a Signature instance. It panics if data is not of expected length by instilling
130+
// a bound check hint to the compiler. It uses unsafe hackery to zero-alloc convert data into a Signature.
131+
func UnmarshalSignature(data []byte) Signature {
132+
_ = data[SizeSignature-1]
133+
return *(*Signature)(unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&data)).Data))
134+
}

mod.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type Protocol struct {
3939
// OnPingFailed is called whenever any attempt by a node to dial a peer at addr fails.
4040
OnPingFailed func(addr string, err error)
4141

42-
// OnMessageSent is called whenever a message or request is successfully sent to a peer.
42+
// OnMessageSent is called whenever bytes of a message or request or response have been flushed/sent to a peer.
4343
OnMessageSent func(client *Client)
4444

4545
// OnMessageRecv is called whenever a message or response is received from a peer.

0 commit comments

Comments
 (0)