aboutsummaryrefslogtreecommitdiffstats
path: root/proposals/0000-wire-protocol.md
blob: fa8ac1403ea86c2bc377b251ee83a20ce7fd0a8a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360

Title: **DEP-0000: Wire Protocol**

Short Name: `0000-wire-protocol`

Type: Standard

Status: Undefined (as of 2018-02-04)

Github PR: (add HTTPS link here after PR is opened)

Authors: [Paul Frazee](https://github.com/pfrazee),
[Bryan Newbold](https://github.com/bnewbold)


# Summary
[summary]: #summary

This DEP describes the Dat wire protocol: a transport-agnostic message stream
spoken between nodes in a swarm of hypercore network peers (including Dat
clients). The wire protocol includes mechanisms for framing, stream encryption,
and feed key authentication.


# Motivation
[motivation]: #motivation

The protocol described here is already in use as of 2017 (by hypercore, Dat,
and Beaker Browser users), and was partially described in an earlier
[whitepaper][whitepaper]. This document fills in some additional details.

[whitepaper]: https://TODO


# Stream Connections
[stream-details]: #stream-details

The Dat wire protocol depends on a lower binary transport channel which
provides the following semantics:

- reliable delivery (no dropped messages, or partial messages)
- in-order delivery of messages

Peers wishing to connect need to discover each other using some mechanism or
another (see forthcoming DEPs on some options; this process is modular and
swappable), and both need to have the public key for the primary hypercore they
wish to exchange.

Messages are framed by the Dat protocol itself (see Messages section for
details).


## Channels
[channels]: #channels

Multiple hypercore registers can be synchronized over the same protocol
connection. Messages pertaining to the separate registers (aka, "Feeds",
"channels") are tagged with an id for disambiguation.

Note that at least one feed is necessary for each connection (for handshaking
to succeed), and that the first feed is the one used for discovery and
as an encryption key.

To initiate a new channel (after the primary is established), 

## Handshake Procedure
[handshake]: #handshake

A handshake procedure needs to occur for each feed on a channel; the first part
of the first handshake happens in cleartext and both validates discovery keys
and establishes encyption paramters used for the rest of the connection. The
first (primary) channel has `id=0`.

The first (cleartext) message is a Feed message, and includes two fields: a
nonce and a discovery key.

The **nonce** is generated by each peer as a random 32-byte sequence.

The **discovery key** is generated from the public encryption key for a
hypercore register (in this case the first, or "primary" register) by using the
public key to sign the 9-byte ASCII string "hypercore" (with no trailing NULL
byte) using the BLAKE2b keyed hashing algorithm (provided by most BLAKE2b hash
implementations). The discovery key is 32 bytes long.

The discovery key is used in cleartext instead of the public key to avoid
leaking the public key to the network; read access to hypercore registers
(including Dat archives) is controlled by limiting access to public keys.

When the connection is first opened, the connecting peer sends their Feed
message. The receiving peer checks that the discovery key was what they were
expecting (eg, that they know of a public key matching that discovery key and
are willing to synchronize the register associated with that key). If so, they
reply with their own Feed. If not, they drop the connection.

Once Feed messages are exchanged, both peers have all information they need to
encrypt all further content on the channel, and do so (see below for details).
The second part of the handshake is to exchange Handshake messages, which set
some parameters for the channel. Handshakes also include the self-identified
peer id, which can be used to detect accidental self-connections or redundant
connections to the same peer (eg, over different transports).


## Encryption Scheme
[encryption]: #encryption

After the first Feed messages are exchanged (one message in each direction, in
cleartext), all further bytes exchanged over the channel are encrypted.

Framing metadata (aka, message length and type) is encrypted, but a third party
could likely infer message lengths (and thus potentially message types) by
observing packet sizes and timing; no padding is applied at the protocol layer.

The encryption scheme used is libsodium's stream primative, specifically the
XSalsa20 cipher. The cipher is fed a shared key (the primary hypercore register
public key), a nonce (selected by the sender and exchanged during handshake),
and a block offset (representing all encrypted bytes sent on the connection in
this direction so far).

*TODO: the following paragraph gets in to implementation details... some mention
of the 64 byte chunks is needed, but maybe not this much detail?*

The specific libsodium function used is usually
`crypto_stream_xsalsa20_xor_ic()`. Some interfacing code is necessary to
process messages that don't align with the cipher's 64-byte chunk size; unused
bytes in any particular chunk can be ignored. For example, if 1000 encrypted
bytes had been sent on a connection already, and then a new 50 byte message
needed to be encrypted and sent, then one would offset the message by `1000 %
64 = 40` bytes and XOR the first 24 bytes against block 15, then XOR the
remaining 26 bytes against block 16. The bytes would be shifted back and
recombined before sending, so only 50 bytes would go down the connection; the
same process would be followed by the receiver.


# Message Details
[message-details]: #message-details

TODO: description of framing

Wire format is `<len>(<header><message>)`. `header` is a varint, of form
`channel << 4 | <4-bit-type>`.

Messages are encoded (serialized) using Google's [profobuf][protobuf] encoding.

[protobuf]: https://TODO

<table>
  <tr><th>`type` code <th>Name
  <tr><td> N/A <td>[Keep-Alive][msg-keepalive]
  <tr><td> 0 <td>[Feed][msg-feed]
  <tr><td> 1 <td>[Handshake][msg-handshake]
  <tr><td> 2 <td>[Info][msg-info]
  <tr><td> 3 <td>[Have][msg-have]
  <tr><td> 4 <td>[Unhave][msg-unhave]
  <tr><td> 5 <td>[Want][msg-want]
  <tr><td> 6 <td>[Unwant][msg-unwant]
  <tr><td> 7 <td>[Request][msg-request]
  <tr><td> 8 <td>[Cancel][msg-cancel]
  <tr><td> 9 <td>[Data][msg-data]
  <tr><td>15 <td>[Extension][msg-extension]
</table>

#### Keep-Alive
[msg-keepalive]: #msg-keepalive

A message of body length 0 (giving a total message size of 1 byte for the `len`
varint) is a keep-alive. Depending on transport and application needs, peers
may optionally send keep-alive messages to help detect and prevent channel
loss. Peers must always handle keep-alive messages correctly (aka, ignore
them), regardless of transport.

TODO: what is a good default interval?

#### Feed
[msg-feed]: #msg-feed

    // type=0, should be the first message sent on a channel
    message Feed {
      required bytes discoveryKey = 1;
      optional bytes nonce = 2;
    }

#### Handshake
[msg-handshake]: #msg-handshake

    // type=1, overall connection handshake. should be send just after the feed message on the first channel only
    message Handshake {
        optional bytes id = 1;
        optional bool live = 2; // keep the connection open forever? both ends have to agree
        optional bytes userData = 3;
        repeated string extensions = 4;
    }

TODO: What are semantics of 'live' bit? what if there is disagreement?

#### Info
[msg-info]: #msg-info

    // type=2, message indicating state changes etc.
    // initial state for uploading/downloading is true
    // if both ends are not downloading and not live it is safe to consider the stream ended
    message Info {
        optional bool uploading = 1;
        optional bool downloading = 2;
    }

#### Have
[msg-have]: #msg-have

    // type=3, what do we have?
    message Have {
        required uint64 start = 1;
        optional uint64 length = 2 [default = 1]; // defaults to 1
        optional bytes bitfield = 3;
    }

#### Unhave
[msg-unhave]: #msg-unhave

    // type=4, what did we lose?
    message Unhave {
        required uint64 start = 1;
        optional uint64 length = 2 [default = 1]; // defaults to 1
    }

#### Want
[msg-want]: #msg-want

    // type=5, what do we want? remote should start sending have messages in this range
    message Want {
        required uint64 start = 1;
        optional uint64 length = 2; // defaults to Infinity or feed.length (if not live)
    }

#### Unwant
[msg-unwant]: #msg-unwant

    // type=6, what don't we want anymore?
    message Unwant {
        required uint64 start = 1;
        optional uint64 length = 2; // defaults to Infinity or feed.length (if not live)
    }

#### Request
[msg-request]: #msg-request

    // type=7, ask for data
    message Request {
        required uint64 index = 1;
        optional uint64 bytes = 2;
        optional bool hash = 3;
        optional uint64 nodes = 4;
    }

#### Cancel
[msg-cancel]: #msg-cancel

    // type=8, cancel a request
    message Cancel {
        required uint64 index = 1;
        optional uint64 bytes = 2;
        optional bool hash = 3;
    }

#### Data
[msg-data]: #msg-data

    // type=9, get some data
    message Data {
        message Node {
            required uint64 index = 1;
            required bytes hash = 2;
            required uint64 size = 3;
        }
        required uint64 index = 1;
        optional bytes value = 2;
        repeated Node nodes = 3;
        optional bytes signature = 4;
    }

#### Extension
[msg-extension]: #msg-extension

`type=15` is an extension message that is encoded like:

    <varint user-type><payload>

# Examples

## Simple Download

Alyssa P Hacker and Ben Bitdiddle want to share a book... B connects to A.

- full public key and discovery key for the connection
- example nonces (in full)
- messages in "struct" syntax and raw hex:
    - B: sends Feed
    - A: replies Feed
    - B: Handshake (downloading, not live)
    - A: Handshake (uploading, not live)
    - B: Info: downloading only
    - A: Info: uploading only
    - B: Have: nothing
    - A: Have: everything
    - B: Want: first register entry
    - A: Data: first entry
    - (repeat for all other chunks)
- connection closes

## Multiple Feeds

Describe in detail how to "add" a new channel (feed/register) to an existing
connection, using Feed (and Handshake?) messages.

## Swarm Synchronization

TODO: should this more involved example actually live here? or in hypercore
DEP? It feels pretty message-level, but does involve more hypercore semantics.

This example wouldn't include actual messages, but would describe an N-way (3+)
node swarm, with a single (complete) seeder, two peers that both download from
the seeder and exchange messages, and a fourth peer that downloads from one of
the non-seeder peers only.

- Peer A: seeder, writer. Starts with full history and appends to log
- Peer B: swarm, reader. Starts with full history. Live connection. Connected
  to A, C, D.
- Peer C: swarm, reader. Starts with sparse (old) history. Only wants "latest"
  data. Connected to A and, B.
- Peer D: like Peer C, but only connected to B.

# Rationale and alternatives
[alternatives]: #alternatives

- Why is this design the best in the space of possible designs?
- What other designs have been considered and what is the rationale for not choosing them?
- What is the impact of not doing this?


# Unresolved questions
[unresolved]: #unresolved-questions

What are extension strings?  What can 'userData' bytes be used for?

Encryption might not make sense in some contexts (eg, IPC, or if the transport
layer is already providing encryption). Should this DEP recognize this
explicitly?

- What parts of the design do you expect to resolve through the DEP consensus process before this gets merged?
- What parts of the design do you expect to resolve through implementation and code review, or are left to independent library or application developers?
- What related issues do you consider out of scope for this DEP that could be addressed in the future independently of the solution that comes out of this DEP?


# Changelog
[changelog]: #changelog

A brief statemnt about current status can go here, follow by a list of dates
when the status line of this DEP changed (in most-recent-last order).

- YYYY-MM-DD: First complete draft submitted for review