@peerbit/stream-interface 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +3 -0
- package/lib/esm/index.d.ts +11 -0
- package/lib/esm/index.js +2 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/messages.d.ts +133 -0
- package/lib/esm/messages.js +678 -0
- package/lib/esm/messages.js.map +1 -0
- package/lib/esm/package.json +3 -0
- package/package.json +58 -0
- package/src/index.ts +14 -0
- package/src/messages.ts +758 -0
package/src/messages.ts
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import {
|
|
2
|
+
variant,
|
|
3
|
+
vec,
|
|
4
|
+
field,
|
|
5
|
+
serialize,
|
|
6
|
+
deserialize,
|
|
7
|
+
fixedArray,
|
|
8
|
+
option,
|
|
9
|
+
} from "@dao-xyz/borsh";
|
|
10
|
+
import { equals } from "uint8arrays";
|
|
11
|
+
import { Uint8ArrayList } from "uint8arraylist";
|
|
12
|
+
import {
|
|
13
|
+
PublicSignKey,
|
|
14
|
+
SignatureWithKey,
|
|
15
|
+
verify,
|
|
16
|
+
randomBytes,
|
|
17
|
+
sha256Base64,
|
|
18
|
+
sha256,
|
|
19
|
+
} from "@peerbit/crypto";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The default msgID implementation
|
|
23
|
+
* Child class can override this.
|
|
24
|
+
*/
|
|
25
|
+
export const getMsgId = async (msg: Uint8ArrayList | Uint8Array) => {
|
|
26
|
+
// first bytes is discriminator,
|
|
27
|
+
// next 32 bytes should be an id
|
|
28
|
+
//return Buffer.from(msg.slice(0, 33)).toString('base64');
|
|
29
|
+
|
|
30
|
+
return sha256Base64(msg.subarray(0, 33)); // base64EncArr(msg, 0, ID_LENGTH + 1);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let concatBytes: (arr: Uint8Array[], totalLength: number) => Uint8Array;
|
|
34
|
+
if ((globalThis as any).Buffer) {
|
|
35
|
+
concatBytes = (globalThis as any).Buffer.concat;
|
|
36
|
+
} else {
|
|
37
|
+
concatBytes = (arrays, length) => {
|
|
38
|
+
if (length == null) {
|
|
39
|
+
let length = 0;
|
|
40
|
+
for (const element of arrays) {
|
|
41
|
+
length += element.length;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const output = new Uint8Array(length);
|
|
45
|
+
let offset = 0;
|
|
46
|
+
for (const arr of arrays) {
|
|
47
|
+
output.set(arr, offset);
|
|
48
|
+
offset += arr.length;
|
|
49
|
+
}
|
|
50
|
+
return output;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const ID_LENGTH = 32;
|
|
55
|
+
|
|
56
|
+
const WEEK_MS = 7 * 24 * 60 * 60 + 1000;
|
|
57
|
+
|
|
58
|
+
@variant(0)
|
|
59
|
+
export class MessageHeader {
|
|
60
|
+
@field({ type: fixedArray("u8", ID_LENGTH) })
|
|
61
|
+
private _id: Uint8Array;
|
|
62
|
+
|
|
63
|
+
@field({ type: "u64" })
|
|
64
|
+
private _timestamp: bigint;
|
|
65
|
+
|
|
66
|
+
@field({ type: "u64" })
|
|
67
|
+
private _expires: bigint;
|
|
68
|
+
|
|
69
|
+
constructor(properties?: { expires?: bigint; id?: Uint8Array }) {
|
|
70
|
+
this._id = properties?.id || randomBytes(ID_LENGTH);
|
|
71
|
+
this._expires = properties?.expires || BigInt(+new Date() + WEEK_MS);
|
|
72
|
+
this._timestamp = BigInt(+new Date());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get id() {
|
|
76
|
+
return this._id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get expires() {
|
|
80
|
+
return this._expires;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get timetamp() {
|
|
84
|
+
return this._timestamp;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
equals(other: MessageHeader) {
|
|
88
|
+
return this._expires === other.expires && equals(this._id, other.id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
verify() {
|
|
92
|
+
return this.expires >= +new Date();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class PublicKeys {
|
|
97
|
+
@field({ type: vec(PublicSignKey) })
|
|
98
|
+
keys: PublicSignKey[];
|
|
99
|
+
constructor(keys: PublicSignKey[]) {
|
|
100
|
+
this.keys = keys;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const SIGNATURES_SIZE_ENCODING = "u8"; // with 7 steps you know everyone in the world?, so u8 *should* suffice
|
|
105
|
+
@variant(0)
|
|
106
|
+
export class Signatures {
|
|
107
|
+
@field({ type: vec(SignatureWithKey, SIGNATURES_SIZE_ENCODING) })
|
|
108
|
+
signatures: SignatureWithKey[];
|
|
109
|
+
|
|
110
|
+
constructor(signatures: SignatureWithKey[] = []) {
|
|
111
|
+
this.signatures = signatures;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
equals(other: Signatures) {
|
|
115
|
+
return (
|
|
116
|
+
this.signatures.length === other.signatures.length &&
|
|
117
|
+
this.signatures.every((value, i) => other.signatures[i].equals(value))
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get publicKeys(): PublicSignKey[] {
|
|
122
|
+
return this.signatures.map((x) => x.publicKey);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
hashPublicKeys(): Promise<string> {
|
|
126
|
+
return sha256Base64(serialize(new PublicKeys(this.publicKeys)));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const keyMap: Map<string, PublicSignKey> = new Map();
|
|
131
|
+
interface Signed {
|
|
132
|
+
get signatures(): Signatures;
|
|
133
|
+
}
|
|
134
|
+
interface Suffix {
|
|
135
|
+
getSuffix(iteration: number): Uint8Array | Uint8Array[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const verifyMultiSig = async (
|
|
139
|
+
message: Suffix & Prefixed & Signed,
|
|
140
|
+
expectSignatures: boolean
|
|
141
|
+
) => {
|
|
142
|
+
const signatures = message.signatures.signatures;
|
|
143
|
+
if (signatures.length === 0) {
|
|
144
|
+
return !expectSignatures;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await message.createPrefix();
|
|
148
|
+
|
|
149
|
+
const dataGenerator = getMultiSigDataToSignHistory(message, 0);
|
|
150
|
+
let done: boolean | undefined = false;
|
|
151
|
+
for (const signature of signatures) {
|
|
152
|
+
if (done) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
"Unexpected, the amount of signatures does not match the amount of data verify"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const data = dataGenerator.next();
|
|
158
|
+
done = data.done;
|
|
159
|
+
if (!(await verify(signature, data.value!))) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
};
|
|
165
|
+
interface Prefixed {
|
|
166
|
+
prefix: Uint8Array;
|
|
167
|
+
createPrefix: () => Promise<Uint8Array>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const emptySignatures = serialize(new Signatures());
|
|
171
|
+
function* getMultiSigDataToSignHistory(
|
|
172
|
+
message: Suffix & Prefixed & Signed,
|
|
173
|
+
from = 0
|
|
174
|
+
): Generator<Uint8Array, undefined, void> {
|
|
175
|
+
if (from === 0) {
|
|
176
|
+
yield concatBytes(
|
|
177
|
+
[message.prefix, emptySignatures],
|
|
178
|
+
message.prefix.length + emptySignatures.length
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (
|
|
183
|
+
let i = Math.max(from - 1, 0);
|
|
184
|
+
i < message.signatures.signatures.length;
|
|
185
|
+
i++
|
|
186
|
+
) {
|
|
187
|
+
const bytes = message.getSuffix(i); // TODO make more performant
|
|
188
|
+
const concat = [message.prefix];
|
|
189
|
+
let len = message.prefix.length;
|
|
190
|
+
if (bytes instanceof Uint8Array) {
|
|
191
|
+
concat.push(bytes);
|
|
192
|
+
len += bytes.byteLength;
|
|
193
|
+
} else {
|
|
194
|
+
for (const arr of bytes) {
|
|
195
|
+
concat.push(arr);
|
|
196
|
+
len += arr.byteLength;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
yield concatBytes(concat, len);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export abstract class Message {
|
|
205
|
+
static deserialize(bytes: Uint8ArrayList) {
|
|
206
|
+
if (bytes.get(0) === DATA_VARIANT) {
|
|
207
|
+
// Data
|
|
208
|
+
return DataMessage.deserialize(bytes);
|
|
209
|
+
} else if (bytes.get(0) === HELLO_VARIANT) {
|
|
210
|
+
// heartbeat
|
|
211
|
+
return Hello.deserialize(bytes);
|
|
212
|
+
} else if (bytes.get(0) === GOODBYE_VARIANT) {
|
|
213
|
+
// heartbeat
|
|
214
|
+
return Goodbye.deserialize(bytes);
|
|
215
|
+
} else if (bytes.get(0) === PING_VARIANT) {
|
|
216
|
+
return PingPong.deserialize(bytes);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throw new Error("Unsupported");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
abstract serialize(): Uint8ArrayList | Uint8Array;
|
|
223
|
+
abstract equals(other: Message): boolean;
|
|
224
|
+
abstract verify(expectSignatures: boolean): Promise<boolean>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// I pack data with this message
|
|
228
|
+
const DATA_VARIANT = 0;
|
|
229
|
+
@variant(DATA_VARIANT)
|
|
230
|
+
export class DataMessage extends Message {
|
|
231
|
+
@field({ type: MessageHeader })
|
|
232
|
+
private _header: MessageHeader;
|
|
233
|
+
|
|
234
|
+
@field({ type: vec("string") })
|
|
235
|
+
private _to: string[]; // not signed! TODO should we sign this?
|
|
236
|
+
|
|
237
|
+
@field({ type: Signatures })
|
|
238
|
+
private _signatures: Signatures;
|
|
239
|
+
|
|
240
|
+
@field({ type: Uint8Array })
|
|
241
|
+
private _data: Uint8Array;
|
|
242
|
+
|
|
243
|
+
constructor(properties: {
|
|
244
|
+
header?: MessageHeader;
|
|
245
|
+
to?: string[];
|
|
246
|
+
data: Uint8Array;
|
|
247
|
+
signatures?: Signatures;
|
|
248
|
+
}) {
|
|
249
|
+
super();
|
|
250
|
+
this._data = properties.data;
|
|
251
|
+
this._header = properties.header || new MessageHeader();
|
|
252
|
+
this._to = properties.to || [];
|
|
253
|
+
this._signatures = properties.signatures || new Signatures();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
get id(): Uint8Array {
|
|
257
|
+
return this._header.id;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
get signatures(): Signatures {
|
|
261
|
+
return this._signatures;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
get header(): MessageHeader {
|
|
265
|
+
return this._header;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
get to(): string[] {
|
|
269
|
+
return this._to;
|
|
270
|
+
}
|
|
271
|
+
set to(to: string[]) {
|
|
272
|
+
this._serialized = undefined;
|
|
273
|
+
this._to = to;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
get sender(): PublicSignKey {
|
|
277
|
+
return this.signatures.signatures[0].publicKey;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
get data(): Uint8Array {
|
|
281
|
+
return this._data;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_serialized: Uint8Array | undefined;
|
|
285
|
+
get serialized(): Uint8Array | undefined {
|
|
286
|
+
return this.serialized;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
_prefix: Uint8Array | undefined;
|
|
290
|
+
get prefix(): Uint8Array {
|
|
291
|
+
if (!this._prefix) {
|
|
292
|
+
throw new Error("Prefix not created");
|
|
293
|
+
}
|
|
294
|
+
return this._prefix;
|
|
295
|
+
}
|
|
296
|
+
async createPrefix(): Promise<Uint8Array> {
|
|
297
|
+
if (this._prefix) {
|
|
298
|
+
return this._prefix;
|
|
299
|
+
}
|
|
300
|
+
const headerSer = serialize(this._header);
|
|
301
|
+
const hashBytes = await sha256(this.data);
|
|
302
|
+
this._prefix = concatBytes(
|
|
303
|
+
[new Uint8Array([DATA_VARIANT]), headerSer, hashBytes],
|
|
304
|
+
1 + headerSer.length + hashBytes.length
|
|
305
|
+
);
|
|
306
|
+
return this._prefix;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
getSuffix(iteration: number): Uint8Array {
|
|
310
|
+
return serialize(
|
|
311
|
+
new Signatures(this.signatures.signatures.slice(0, iteration + 1))
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async sign(sign: (bytes: Uint8Array) => Promise<SignatureWithKey>) {
|
|
316
|
+
this._serialized = undefined; // because we will change this object, so the serialized version will not be applicable anymore
|
|
317
|
+
await this.createPrefix();
|
|
318
|
+
this.signatures.signatures.push(
|
|
319
|
+
await sign(
|
|
320
|
+
getMultiSigDataToSignHistory(
|
|
321
|
+
this,
|
|
322
|
+
this.signatures.signatures.length
|
|
323
|
+
).next().value!
|
|
324
|
+
)
|
|
325
|
+
);
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async verify(expectSignatures: boolean): Promise<boolean> {
|
|
330
|
+
return this._header.verify() && verifyMultiSig(this, expectSignatures);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Manually ser/der for performance gains */
|
|
334
|
+
serialize() {
|
|
335
|
+
if (this._serialized) {
|
|
336
|
+
return this._serialized;
|
|
337
|
+
}
|
|
338
|
+
return serialize(this);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
static deserialize(bytes: Uint8ArrayList): DataMessage {
|
|
342
|
+
if (bytes.get(0) !== 0) {
|
|
343
|
+
throw new Error("Unsupported");
|
|
344
|
+
}
|
|
345
|
+
const arr = bytes.subarray();
|
|
346
|
+
const ret = deserialize(arr, DataMessage);
|
|
347
|
+
ret._serialized = arr;
|
|
348
|
+
return ret;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
equals(other: Message) {
|
|
352
|
+
if (other instanceof DataMessage) {
|
|
353
|
+
const a =
|
|
354
|
+
equals(this.data, other.data) &&
|
|
355
|
+
equals(this.id, other.id) &&
|
|
356
|
+
this.to.length === other.to.length;
|
|
357
|
+
if (!a) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
for (let i = 0; i < this.to.length; i++) {
|
|
361
|
+
if (this.to[i] !== other.to[i]) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return this.signatures.equals(other.signatures);
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
@variant(0)
|
|
371
|
+
export class NetworkInfo {
|
|
372
|
+
@field({ type: vec("u32", SIGNATURES_SIZE_ENCODING) })
|
|
373
|
+
pingLatencies: number[];
|
|
374
|
+
|
|
375
|
+
constructor(pingLatencies: number[]) {
|
|
376
|
+
this.pingLatencies = pingLatencies;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// I send this too all my peers
|
|
381
|
+
const HELLO_VARIANT = 1;
|
|
382
|
+
@variant(HELLO_VARIANT)
|
|
383
|
+
export class Hello extends Message {
|
|
384
|
+
@field({ type: MessageHeader })
|
|
385
|
+
header: MessageHeader;
|
|
386
|
+
|
|
387
|
+
@field({ type: vec("string") })
|
|
388
|
+
multiaddrs: string[];
|
|
389
|
+
|
|
390
|
+
@field({ type: option(Uint8Array) })
|
|
391
|
+
data?: Uint8Array;
|
|
392
|
+
|
|
393
|
+
@field({ type: NetworkInfo })
|
|
394
|
+
networkInfo: NetworkInfo;
|
|
395
|
+
|
|
396
|
+
@field({ type: Signatures })
|
|
397
|
+
signatures: Signatures;
|
|
398
|
+
|
|
399
|
+
constructor(options?: { multiaddrs?: string[]; data?: Uint8Array }) {
|
|
400
|
+
super();
|
|
401
|
+
this.header = new MessageHeader();
|
|
402
|
+
this.data = options?.data;
|
|
403
|
+
this.multiaddrs =
|
|
404
|
+
options?.multiaddrs?.filter((x) => !x.includes("/p2p-circuit/")) || []; // don't forward relay addresess (TODO ?)
|
|
405
|
+
this.signatures = new Signatures();
|
|
406
|
+
this.networkInfo = new NetworkInfo([]);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
get sender(): PublicSignKey {
|
|
410
|
+
return this.signatures.signatures[0].publicKey;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
serialize() {
|
|
414
|
+
return serialize(this);
|
|
415
|
+
}
|
|
416
|
+
static deserialize(bytes: Uint8ArrayList): Hello {
|
|
417
|
+
const result = deserialize(bytes.subarray(), Hello);
|
|
418
|
+
if (result.signatures.signatures.length === 0) {
|
|
419
|
+
throw new Error("Missing sender on Hello");
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
_prefix: Uint8Array | undefined;
|
|
425
|
+
get prefix(): Uint8Array {
|
|
426
|
+
if (!this._prefix) {
|
|
427
|
+
throw new Error("Prefix not created");
|
|
428
|
+
}
|
|
429
|
+
return this._prefix;
|
|
430
|
+
}
|
|
431
|
+
async createPrefix(): Promise<Uint8Array> {
|
|
432
|
+
if (this._prefix) {
|
|
433
|
+
return this._prefix;
|
|
434
|
+
}
|
|
435
|
+
const headerSer = serialize(this.header);
|
|
436
|
+
const hashBytes = this.data ? await sha256(this.data) : new Uint8Array();
|
|
437
|
+
this._prefix = concatBytes(
|
|
438
|
+
[new Uint8Array([HELLO_VARIANT]), headerSer, hashBytes],
|
|
439
|
+
1 + headerSer.length + hashBytes.length
|
|
440
|
+
);
|
|
441
|
+
return this._prefix;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
getSuffix(iteration: number): Uint8Array[] {
|
|
445
|
+
return [
|
|
446
|
+
serialize(
|
|
447
|
+
new NetworkInfo(this.networkInfo.pingLatencies.slice(0, iteration + 1))
|
|
448
|
+
),
|
|
449
|
+
serialize(
|
|
450
|
+
new Signatures(this.signatures.signatures.slice(0, iteration + 1))
|
|
451
|
+
),
|
|
452
|
+
];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async sign(sign: (bytes: Uint8Array) => Promise<SignatureWithKey>) {
|
|
456
|
+
await this.createPrefix();
|
|
457
|
+
const toSign = getMultiSigDataToSignHistory(
|
|
458
|
+
this,
|
|
459
|
+
this.signatures.signatures.length
|
|
460
|
+
).next().value!;
|
|
461
|
+
this.signatures.signatures.push(await sign(toSign));
|
|
462
|
+
return this;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async verify(expectSignatures: boolean): Promise<boolean> {
|
|
466
|
+
return (
|
|
467
|
+
this.header.verify() &&
|
|
468
|
+
this.networkInfo.pingLatencies.length ===
|
|
469
|
+
this.signatures.signatures.length - 1 &&
|
|
470
|
+
verifyMultiSig(this, expectSignatures)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
equals(other: Message) {
|
|
475
|
+
if (other instanceof Hello) {
|
|
476
|
+
const dataEquals =
|
|
477
|
+
(!!this.data && !!other.data && equals(this.data, other.data)) ||
|
|
478
|
+
!this.data === !other.data;
|
|
479
|
+
if (!dataEquals) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return (
|
|
484
|
+
this.header.equals(other.header) &&
|
|
485
|
+
this.signatures.equals(other.signatures)
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Me or some my peer disconnected
|
|
493
|
+
const GOODBYE_VARIANT = 2;
|
|
494
|
+
@variant(GOODBYE_VARIANT)
|
|
495
|
+
export class Goodbye extends Message {
|
|
496
|
+
@field({ type: MessageHeader })
|
|
497
|
+
header: MessageHeader;
|
|
498
|
+
|
|
499
|
+
@field({ type: "bool" })
|
|
500
|
+
early?: boolean; // is early goodbye, I send this to my peers so when I disconnect, they can relay the message for me
|
|
501
|
+
|
|
502
|
+
@field({ type: option(Uint8Array) })
|
|
503
|
+
data?: Uint8Array; // not signed
|
|
504
|
+
|
|
505
|
+
@field({ type: Signatures })
|
|
506
|
+
signatures: Signatures;
|
|
507
|
+
|
|
508
|
+
constructor(properties?: {
|
|
509
|
+
header?: MessageHeader;
|
|
510
|
+
data?: Uint8Array;
|
|
511
|
+
early?: boolean;
|
|
512
|
+
}) {
|
|
513
|
+
// disconnected: PeerId | string,
|
|
514
|
+
super();
|
|
515
|
+
this.header = properties?.header || new MessageHeader();
|
|
516
|
+
this.data = properties?.data;
|
|
517
|
+
this.early = properties?.early;
|
|
518
|
+
this.signatures = new Signatures();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
get sender(): PublicSignKey {
|
|
522
|
+
return this.signatures.signatures[0]!.publicKey;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
serialize() {
|
|
526
|
+
return serialize(this);
|
|
527
|
+
}
|
|
528
|
+
static deserialize(bytes: Uint8ArrayList): Goodbye {
|
|
529
|
+
const result = deserialize(bytes.subarray(), Goodbye);
|
|
530
|
+
if (result.signatures.signatures.length === 0) {
|
|
531
|
+
throw new Error("Missing sender on Goodbye");
|
|
532
|
+
}
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
_prefix: Uint8Array | undefined;
|
|
537
|
+
get prefix(): Uint8Array {
|
|
538
|
+
if (!this._prefix) {
|
|
539
|
+
throw new Error("Prefix not created");
|
|
540
|
+
}
|
|
541
|
+
return this._prefix;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async createPrefix(): Promise<Uint8Array> {
|
|
545
|
+
if (this._prefix) {
|
|
546
|
+
return this._prefix;
|
|
547
|
+
}
|
|
548
|
+
const headerSer = serialize(this.header);
|
|
549
|
+
const hashBytes = this.data ? await sha256(this.data) : new Uint8Array();
|
|
550
|
+
this._prefix = concatBytes(
|
|
551
|
+
[new Uint8Array([GOODBYE_VARIANT]), headerSer, hashBytes],
|
|
552
|
+
1 + headerSer.length + 1 + hashBytes.length
|
|
553
|
+
);
|
|
554
|
+
return this._prefix;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
getSuffix(iteration: number): Uint8Array {
|
|
558
|
+
return serialize(
|
|
559
|
+
new Signatures(this.signatures.signatures.slice(0, iteration + 1))
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async sign(sign: (bytes: Uint8Array) => Promise<SignatureWithKey>) {
|
|
564
|
+
await this.createPrefix();
|
|
565
|
+
this.signatures.signatures.push(
|
|
566
|
+
await sign(
|
|
567
|
+
getMultiSigDataToSignHistory(
|
|
568
|
+
this,
|
|
569
|
+
this.signatures.signatures.length
|
|
570
|
+
).next().value!
|
|
571
|
+
)
|
|
572
|
+
);
|
|
573
|
+
return this;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async verify(expectSignatures: boolean): Promise<boolean> {
|
|
577
|
+
return this.header.verify() && verifyMultiSig(this, expectSignatures);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
equals(other: Message) {
|
|
581
|
+
if (other instanceof Goodbye) {
|
|
582
|
+
if (this.early !== other.early) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const dataEquals =
|
|
587
|
+
(!!this.data && !!other.data && equals(this.data, other.data)) ||
|
|
588
|
+
!this.data === !other.data;
|
|
589
|
+
if (!dataEquals) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
return (
|
|
593
|
+
this.header.equals(other.header) &&
|
|
594
|
+
this.signatures.equals(other.signatures)
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const PING_VARIANT = 3;
|
|
602
|
+
|
|
603
|
+
@variant(PING_VARIANT)
|
|
604
|
+
export abstract class PingPong extends Message {
|
|
605
|
+
static deserialize(bytes: Uint8ArrayList) {
|
|
606
|
+
return deserialize(bytes.subarray(), PingPong);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
serialize(): Uint8ArrayList | Uint8Array {
|
|
610
|
+
return serialize(this);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
verify(_expectSignatures: boolean): Promise<boolean> {
|
|
614
|
+
return Promise.resolve(true);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
abstract get pingBytes(): Uint8Array;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
@variant(0)
|
|
621
|
+
export class Ping extends PingPong {
|
|
622
|
+
@field({ type: fixedArray("u8", 32) })
|
|
623
|
+
pingBytes: Uint8Array;
|
|
624
|
+
|
|
625
|
+
constructor() {
|
|
626
|
+
super();
|
|
627
|
+
this.pingBytes = randomBytes(32);
|
|
628
|
+
}
|
|
629
|
+
equals(other: Message) {
|
|
630
|
+
if (other instanceof Ping) {
|
|
631
|
+
return equals(this.pingBytes, other.pingBytes);
|
|
632
|
+
}
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
@variant(1)
|
|
638
|
+
export class Pong extends PingPong {
|
|
639
|
+
@field({ type: fixedArray("u8", 32) })
|
|
640
|
+
pingBytes: Uint8Array;
|
|
641
|
+
|
|
642
|
+
constructor(pingBytes: Uint8Array) {
|
|
643
|
+
super();
|
|
644
|
+
this.pingBytes = pingBytes;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
equals(other: Message) {
|
|
648
|
+
if (other instanceof Pong) {
|
|
649
|
+
return equals(this.pingBytes, other.pingBytes);
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
@variant(0)
|
|
656
|
+
export class Connections {
|
|
657
|
+
@field({ type: vec(fixedArray("string", 2)) })
|
|
658
|
+
connections: [string, string][];
|
|
659
|
+
|
|
660
|
+
constructor(connections: [string, string][]) {
|
|
661
|
+
this.connections = connections;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
equals(other: Connections) {
|
|
665
|
+
if (this.connections.length !== other.connections.length) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
for (let i = 0; i < this.connections.length; i++) {
|
|
669
|
+
if (this.connections[i].length !== other.connections[i].length) {
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
const a1 = this.connections[i][0];
|
|
673
|
+
const a2 = this.connections[i][1];
|
|
674
|
+
const b1 = other.connections[i][0];
|
|
675
|
+
const b2 = other.connections[i][1];
|
|
676
|
+
|
|
677
|
+
if (a1 === b1 && a2 === b2) {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (a1 === b2 && a2 === b1) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Share connections
|
|
690
|
+
/* const NETWORK_INFO_VARIANT = 3;
|
|
691
|
+
@variant(NETWORK_INFO_VARIANT)
|
|
692
|
+
export class NetworkInfo extends Message {
|
|
693
|
+
|
|
694
|
+
@field({ type: MessageHeader })
|
|
695
|
+
header: MessageHeader;
|
|
696
|
+
|
|
697
|
+
@field({ type: Connections })
|
|
698
|
+
connections: Connections;
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
@field({ type: Signatures })
|
|
702
|
+
signatures: Signatures
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
constructor(connections: [string, string][]) {
|
|
706
|
+
super();
|
|
707
|
+
this.header = new MessageHeader();
|
|
708
|
+
this.connections = new Connections(connections);
|
|
709
|
+
this.signatures = new Signatures()
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
getDataToSign(): Uint8Array {
|
|
713
|
+
return this.serialize()
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
_prefix: Uint8Array | undefined
|
|
717
|
+
get prefix(): Uint8Array {
|
|
718
|
+
if (this._prefix)
|
|
719
|
+
return this._prefix
|
|
720
|
+
const header = serialize(this.header);
|
|
721
|
+
const connections = serialize(this.connections);
|
|
722
|
+
this._prefix = concatBytes([new Uint8Array([NETWORK_INFO_VARIANT]), header, connections], 1 + header.length + connections.length);
|
|
723
|
+
return this._prefix;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
sign(sign: (bytes: Uint8Array) => SignatureWithKey) {
|
|
727
|
+
this.signatures.signatures.push(sign(getMultiSigDataToSignHistory(this, this.signatures.signatures.length).next().value!));
|
|
728
|
+
return this;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
verify(): boolean {
|
|
732
|
+
return this.header.verify() && verifyMultiSig(this)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
serialize() {
|
|
737
|
+
return serialize(this)
|
|
738
|
+
}
|
|
739
|
+
static deserialize(bytes: Uint8ArrayList): NetworkInfo {
|
|
740
|
+
return deserialize(bytes.subarray(), NetworkInfo)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
equals(other: Message) {
|
|
744
|
+
if (other instanceof NetworkInfo) {
|
|
745
|
+
if (!equals(this.header.id, other.header.id) || !this.header.equals(other.header)) { // TODO fix uneccessary copy
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (!this.connections.equals(other.connections)) {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
return this.signatures.equals(other.signatures)
|
|
753
|
+
}
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
*/
|