hypercore 10.0.0-alpha.2 → 10.0.0-alpha.23
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/README.md +34 -2
- package/index.js +268 -127
- package/lib/bitfield.js +3 -2
- package/lib/block-encryption.js +68 -0
- package/lib/block-store.js +3 -1
- package/lib/core.js +6 -3
- package/lib/merkle-tree.js +62 -35
- package/lib/oplog.js +4 -3
- package/lib/protocol.js +69 -8
- package/lib/replicator.js +62 -16
- package/lib/streams.js +56 -0
- package/package.json +14 -7
- package/.github/workflows/test-node.yml +0 -24
- package/UPGRADE.md +0 -9
- package/examples/announce.js +0 -19
- package/examples/basic.js +0 -10
- package/examples/http.js +0 -123
- package/examples/lookup.js +0 -20
- package/test/basic.js +0 -78
- package/test/bitfield.js +0 -71
- package/test/core.js +0 -290
- package/test/encodings.js +0 -18
- package/test/extension.js +0 -71
- package/test/helpers/index.js +0 -23
- package/test/merkle-tree.js +0 -518
- package/test/mutex.js +0 -137
- package/test/oplog.js +0 -399
- package/test/preload.js +0 -72
- package/test/replicate.js +0 -296
- package/test/sessions.js +0 -173
- package/test/user-data.js +0 -47
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const sodium = require('sodium-universal')
|
|
2
|
+
const c = require('compact-encoding')
|
|
3
|
+
const b4a = require('b4a')
|
|
4
|
+
|
|
5
|
+
const nonce = b4a.alloc(sodium.crypto_stream_NONCEBYTES)
|
|
6
|
+
|
|
7
|
+
module.exports = class BlockEncryption {
|
|
8
|
+
constructor (encryptionKey, hypercoreKey) {
|
|
9
|
+
const subKeys = b4a.alloc(2 * sodium.crypto_stream_KEYBYTES)
|
|
10
|
+
|
|
11
|
+
this.key = encryptionKey
|
|
12
|
+
this.blockKey = subKeys.subarray(0, sodium.crypto_stream_KEYBYTES)
|
|
13
|
+
this.blindingKey = subKeys.subarray(sodium.crypto_stream_KEYBYTES)
|
|
14
|
+
this.padding = 8
|
|
15
|
+
|
|
16
|
+
sodium.crypto_generichash(this.blockKey, encryptionKey, hypercoreKey)
|
|
17
|
+
sodium.crypto_generichash(this.blindingKey, this.blockKey)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
encrypt (index, block, fork) {
|
|
21
|
+
const padding = block.subarray(0, this.padding)
|
|
22
|
+
block = block.subarray(this.padding)
|
|
23
|
+
|
|
24
|
+
c.uint64.encode({ start: 0, end: 8, buffer: padding }, fork)
|
|
25
|
+
c.uint64.encode({ start: 0, end: 8, buffer: nonce }, index)
|
|
26
|
+
|
|
27
|
+
// Zero out any previous padding.
|
|
28
|
+
nonce.fill(0, 8, 8 + padding.byteLength)
|
|
29
|
+
|
|
30
|
+
// Blind the fork ID, possibly risking reusing the nonce on a reorg of the
|
|
31
|
+
// Hypercore. This is fine as the blinding is best-effort and the latest
|
|
32
|
+
// fork ID shared on replication anyway.
|
|
33
|
+
sodium.crypto_stream_xor(
|
|
34
|
+
padding,
|
|
35
|
+
padding,
|
|
36
|
+
nonce,
|
|
37
|
+
this.blindingKey
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
nonce.set(padding, 8)
|
|
41
|
+
|
|
42
|
+
// The combination of a (blinded) fork ID and a block index is unique for a
|
|
43
|
+
// given Hypercore and is therefore a valid nonce for encrypting the block.
|
|
44
|
+
sodium.crypto_stream_xor(
|
|
45
|
+
block,
|
|
46
|
+
block,
|
|
47
|
+
nonce,
|
|
48
|
+
this.blockKey
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
decrypt (index, block) {
|
|
53
|
+
const padding = block.subarray(0, this.padding)
|
|
54
|
+
block = block.subarray(this.padding)
|
|
55
|
+
|
|
56
|
+
c.uint64.encode({ start: 0, end: 8, buffer: nonce }, index)
|
|
57
|
+
|
|
58
|
+
nonce.set(padding, 8)
|
|
59
|
+
|
|
60
|
+
// Decrypt the block using the blinded fork ID.
|
|
61
|
+
sodium.crypto_stream_xor(
|
|
62
|
+
block,
|
|
63
|
+
block,
|
|
64
|
+
nonce,
|
|
65
|
+
this.blockKey
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
package/lib/block-store.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const b4a = require('b4a')
|
|
2
|
+
|
|
1
3
|
module.exports = class BlockStore {
|
|
2
4
|
constructor (storage, tree) {
|
|
3
5
|
this.storage = storage
|
|
@@ -15,7 +17,7 @@ module.exports = class BlockStore {
|
|
|
15
17
|
|
|
16
18
|
putBatch (i, batch, offset) {
|
|
17
19
|
if (batch.length === 0) return Promise.resolve()
|
|
18
|
-
return this.put(i, batch.length === 1 ? batch[0] :
|
|
20
|
+
return this.put(i, batch.length === 1 ? batch[0] : b4a.concat(batch), offset)
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
clear () {
|
package/lib/core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const hypercoreCrypto = require('hypercore-crypto')
|
|
2
|
+
const b4a = require('b4a')
|
|
2
3
|
const Oplog = require('./oplog')
|
|
3
4
|
const Mutex = require('./mutex')
|
|
4
5
|
const MerkleTree = require('./merkle-tree')
|
|
@@ -90,7 +91,7 @@ module.exports = class Core {
|
|
|
90
91
|
await oplog.flush(header)
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
if (opts.keyPair && !header.signer.publicKey
|
|
94
|
+
if (opts.keyPair && !b4a.equals(header.signer.publicKey, opts.keyPair.publicKey)) {
|
|
94
95
|
throw new Error('Another hypercore is stored here')
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -176,7 +177,7 @@ module.exports = class Core {
|
|
|
176
177
|
|
|
177
178
|
for (const u of this.header.userData) {
|
|
178
179
|
if (u.key !== key) continue
|
|
179
|
-
if (value &&
|
|
180
|
+
if (value && b4a.equals(u.value, value)) return
|
|
180
181
|
empty = false
|
|
181
182
|
break
|
|
182
183
|
}
|
|
@@ -214,10 +215,12 @@ module.exports = class Core {
|
|
|
214
215
|
}
|
|
215
216
|
}
|
|
216
217
|
|
|
217
|
-
async append (values, sign = this.defaultSign) {
|
|
218
|
+
async append (values, sign = this.defaultSign, hooks = {}) {
|
|
218
219
|
await this._mutex.lock()
|
|
219
220
|
|
|
220
221
|
try {
|
|
222
|
+
if (hooks.preappend) await hooks.preappend(values)
|
|
223
|
+
|
|
221
224
|
if (!values.length) return this.tree.length
|
|
222
225
|
|
|
223
226
|
const batch = this.tree.batch()
|
package/lib/merkle-tree.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const flat = require('flat-tree')
|
|
2
2
|
const crypto = require('hypercore-crypto')
|
|
3
|
-
const
|
|
3
|
+
const c = require('compact-encoding')
|
|
4
|
+
const b4a = require('b4a')
|
|
4
5
|
|
|
5
|
-
const BLANK_HASH =
|
|
6
|
-
const OLD_TREE =
|
|
6
|
+
const BLANK_HASH = b4a.alloc(32)
|
|
7
|
+
const OLD_TREE = b4a.from([5, 2, 87, 2, 0, 0, 40, 7, 66, 76, 65, 75, 69, 50, 98])
|
|
7
8
|
|
|
8
9
|
class NodeQueue {
|
|
9
10
|
constructor (nodes, extra = null) {
|
|
@@ -214,7 +215,7 @@ class ReorgBatch extends MerkleTreeBatch {
|
|
|
214
215
|
const nodes = []
|
|
215
216
|
const root = verifyBlock(proof, this.tree.crypto, nodes)
|
|
216
217
|
|
|
217
|
-
if (root === null || !root.hash
|
|
218
|
+
if (root === null || !b4a.equals(root.hash, this.diff.hash)) return false
|
|
218
219
|
|
|
219
220
|
this.nodes.push(...nodes)
|
|
220
221
|
return this._update(nodes)
|
|
@@ -232,7 +233,7 @@ class ReorgBatch extends MerkleTreeBatch {
|
|
|
232
233
|
if (!left) break
|
|
233
234
|
|
|
234
235
|
const existing = await this.tree.get(left.index, false)
|
|
235
|
-
if (!existing || !existing.hash
|
|
236
|
+
if (!existing || !b4a.equals(existing.hash, left.hash)) {
|
|
236
237
|
diff = left
|
|
237
238
|
} else {
|
|
238
239
|
diff = n.get(ite.sibling())
|
|
@@ -246,6 +247,8 @@ class ReorgBatch extends MerkleTreeBatch {
|
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
_updateDiffRoot (diff) {
|
|
250
|
+
if (this.want === null) return true
|
|
251
|
+
|
|
249
252
|
const spans = flat.spans(diff.index)
|
|
250
253
|
const start = spans[0] / 2
|
|
251
254
|
const end = Math.min(this.treeLength, spans[1] / 2 + 1)
|
|
@@ -272,11 +275,15 @@ class ReorgBatch extends MerkleTreeBatch {
|
|
|
272
275
|
}
|
|
273
276
|
|
|
274
277
|
class ByteSeeker {
|
|
275
|
-
constructor (tree, bytes) {
|
|
278
|
+
constructor (tree, bytes, padding = 0) {
|
|
276
279
|
this.tree = tree
|
|
277
280
|
this.bytes = bytes
|
|
278
|
-
this.
|
|
279
|
-
|
|
281
|
+
this.padding = padding
|
|
282
|
+
|
|
283
|
+
const size = tree.byteLength - (tree.length * padding)
|
|
284
|
+
|
|
285
|
+
this.start = bytes >= size ? tree.length : 0
|
|
286
|
+
this.end = bytes < size ? tree.length : 0
|
|
280
287
|
}
|
|
281
288
|
|
|
282
289
|
nodes () {
|
|
@@ -287,12 +294,12 @@ class ByteSeeker {
|
|
|
287
294
|
if (!bytes) return [0, 0]
|
|
288
295
|
|
|
289
296
|
for (const node of this.tree.roots) { // all async ticks happen once we find the root so safe
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
297
|
+
let size = node.size
|
|
298
|
+
if (this.padding > 0) size -= this.padding * flat.countLeaves(node.index)
|
|
293
299
|
|
|
294
|
-
if (bytes
|
|
295
|
-
|
|
300
|
+
if (bytes === size) return [flat.rightSpan(node.index) + 2, 0]
|
|
301
|
+
if (bytes > size) {
|
|
302
|
+
bytes -= size
|
|
296
303
|
continue
|
|
297
304
|
}
|
|
298
305
|
|
|
@@ -301,9 +308,12 @@ class ByteSeeker {
|
|
|
301
308
|
while ((ite.index & 1) !== 0) {
|
|
302
309
|
const l = await this.tree.get(ite.leftChild(), false)
|
|
303
310
|
if (l) {
|
|
304
|
-
|
|
305
|
-
if (
|
|
306
|
-
|
|
311
|
+
let size = l.size
|
|
312
|
+
if (this.padding > 0) size -= this.padding * ite.countLeaves()
|
|
313
|
+
|
|
314
|
+
if (size === bytes) return [ite.rightSpan() + 2, 0]
|
|
315
|
+
if (size > bytes) continue
|
|
316
|
+
bytes -= size
|
|
307
317
|
ite.sibling()
|
|
308
318
|
} else {
|
|
309
319
|
ite.parent()
|
|
@@ -347,7 +357,7 @@ module.exports = class MerkleTree {
|
|
|
347
357
|
}
|
|
348
358
|
|
|
349
359
|
addNode (node) {
|
|
350
|
-
if (node.size === 0 && node.hash
|
|
360
|
+
if (node.size === 0 && b4a.equals(node.hash, BLANK_HASH)) node = blankNode(node.index)
|
|
351
361
|
this.unflushed.set(node.index, node)
|
|
352
362
|
}
|
|
353
363
|
|
|
@@ -355,8 +365,8 @@ module.exports = class MerkleTree {
|
|
|
355
365
|
return new MerkleTreeBatch(this)
|
|
356
366
|
}
|
|
357
367
|
|
|
358
|
-
seek (bytes) {
|
|
359
|
-
return new ByteSeeker(this, bytes)
|
|
368
|
+
seek (bytes, padding) {
|
|
369
|
+
return new ByteSeeker(this, bytes, padding)
|
|
360
370
|
}
|
|
361
371
|
|
|
362
372
|
hash () {
|
|
@@ -371,6 +381,17 @@ module.exports = class MerkleTree {
|
|
|
371
381
|
return this.signature !== null && this.crypto.verify(this.signable(), this.signature, key)
|
|
372
382
|
}
|
|
373
383
|
|
|
384
|
+
getRoots (length) {
|
|
385
|
+
const indexes = flat.fullRoots(2 * length)
|
|
386
|
+
const roots = new Array(indexes.length)
|
|
387
|
+
|
|
388
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
389
|
+
roots[i] = this.get(indexes[i], true)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return Promise.all(roots)
|
|
393
|
+
}
|
|
394
|
+
|
|
374
395
|
get (index, error = true) {
|
|
375
396
|
let node = this.unflushed.get(index)
|
|
376
397
|
|
|
@@ -433,17 +454,23 @@ module.exports = class MerkleTree {
|
|
|
433
454
|
// TODO: write neighbors together etc etc
|
|
434
455
|
// TODO: bench loading a full disk page and copy to that instead
|
|
435
456
|
return new Promise((resolve, reject) => {
|
|
436
|
-
const slab =
|
|
457
|
+
const slab = b4a.allocUnsafe(40 * this.flushing.size)
|
|
437
458
|
|
|
438
459
|
let error = null
|
|
439
460
|
let missing = this.flushing.size + 1
|
|
440
461
|
let offset = 0
|
|
441
462
|
|
|
442
463
|
for (const node of this.flushing.values()) {
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
464
|
+
const state = {
|
|
465
|
+
start: 0,
|
|
466
|
+
end: 40,
|
|
467
|
+
buffer: slab.subarray(offset, offset += 40)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
c.uint64.encode(state, node.size)
|
|
471
|
+
c.raw.encode(state, node.hash)
|
|
472
|
+
|
|
473
|
+
this.storage.write(node.index * 40, state.buffer, done)
|
|
447
474
|
}
|
|
448
475
|
|
|
449
476
|
done(null)
|
|
@@ -520,7 +547,7 @@ module.exports = class MerkleTree {
|
|
|
520
547
|
|
|
521
548
|
for (const root of batch.roots) {
|
|
522
549
|
const existing = await this.get(root.index, false)
|
|
523
|
-
if (existing && existing.hash
|
|
550
|
+
if (existing && b4a.equals(existing.hash, root.hash)) continue
|
|
524
551
|
batch._updateDiffRoot(root)
|
|
525
552
|
break
|
|
526
553
|
}
|
|
@@ -548,7 +575,7 @@ module.exports = class MerkleTree {
|
|
|
548
575
|
|
|
549
576
|
if (unverified) {
|
|
550
577
|
const verified = await this.get(unverified.index)
|
|
551
|
-
if (!verified.hash
|
|
578
|
+
if (!b4a.equals(verified.hash, unverified.hash)) {
|
|
552
579
|
throw new Error('Invalid checksum at node ' + unverified.index)
|
|
553
580
|
}
|
|
554
581
|
}
|
|
@@ -693,7 +720,7 @@ module.exports = class MerkleTree {
|
|
|
693
720
|
await new Promise((resolve, reject) => {
|
|
694
721
|
storage.read(0, OLD_TREE.length, (err, buf) => {
|
|
695
722
|
if (err) return resolve()
|
|
696
|
-
if (
|
|
723
|
+
if (b4a.equals(buf, OLD_TREE)) return reject(new Error('Storage contains an incompatible merkle tree'))
|
|
697
724
|
resolve()
|
|
698
725
|
})
|
|
699
726
|
})
|
|
@@ -1038,10 +1065,10 @@ function getStoredNode (storage, index, error) {
|
|
|
1038
1065
|
return
|
|
1039
1066
|
}
|
|
1040
1067
|
|
|
1041
|
-
const hash = data.
|
|
1042
|
-
const size =
|
|
1068
|
+
const hash = data.subarray(8)
|
|
1069
|
+
const size = c.decode(c.uint64, data)
|
|
1043
1070
|
|
|
1044
|
-
if (size === 0 &&
|
|
1071
|
+
if (size === 0 && b4a.compare(hash, BLANK_HASH) === 0) {
|
|
1045
1072
|
if (error) reject(new Error('Could not load node: ' + index))
|
|
1046
1073
|
else resolve(null)
|
|
1047
1074
|
return
|
|
@@ -1088,9 +1115,9 @@ function log2 (n) {
|
|
|
1088
1115
|
}
|
|
1089
1116
|
|
|
1090
1117
|
function signable (hash, length, fork) {
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
return
|
|
1118
|
+
const state = { start: 0, end: 48, buffer: b4a.alloc(48) }
|
|
1119
|
+
c.raw.encode(state, hash)
|
|
1120
|
+
c.uint64.encode(state, length)
|
|
1121
|
+
c.uint64.encode(state, fork)
|
|
1122
|
+
return state.buffer
|
|
1096
1123
|
}
|
package/lib/oplog.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const cenc = require('compact-encoding')
|
|
2
|
+
const b4a = require('b4a')
|
|
2
3
|
const crc32 = require('crc32-universal')
|
|
3
4
|
|
|
4
5
|
module.exports = class Oplog {
|
|
@@ -133,7 +134,7 @@ module.exports = class Oplog {
|
|
|
133
134
|
return new Promise((resolve, reject) => {
|
|
134
135
|
this.storage.open(err => {
|
|
135
136
|
if (err && err.code !== 'ENOENT') return reject(err)
|
|
136
|
-
if (err) return resolve(
|
|
137
|
+
if (err) return resolve(b4a.alloc(0))
|
|
137
138
|
this.storage.stat((err, stat) => {
|
|
138
139
|
if (err && err.code !== 'ENOENT') return reject(err)
|
|
139
140
|
this.storage.read(0, stat.size, (err, buf) => {
|
|
@@ -151,7 +152,7 @@ module.exports = class Oplog {
|
|
|
151
152
|
const bit = (this._headers[i] + 1) & 1
|
|
152
153
|
|
|
153
154
|
this.headerEncoding.preencode(state, header)
|
|
154
|
-
state.buffer =
|
|
155
|
+
state.buffer = b4a.allocUnsafe(state.end)
|
|
155
156
|
this.headerEncoding.encode(state, header)
|
|
156
157
|
this._addHeader(state, state.end - 8, bit, 0)
|
|
157
158
|
|
|
@@ -187,7 +188,7 @@ module.exports = class Oplog {
|
|
|
187
188
|
this.entryEncoding.preencode(state, batch[i])
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
state.buffer =
|
|
191
|
+
state.buffer = b4a.allocUnsafe(state.end)
|
|
191
192
|
|
|
192
193
|
for (let i = 0; i < batch.length; i++) {
|
|
193
194
|
const start = state.start += 8 // space for header
|
package/lib/protocol.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
const { uint, from: fromEncoding } = require('compact-encoding')
|
|
2
|
+
const b4a = require('b4a')
|
|
2
3
|
const safetyCatch = require('safety-catch')
|
|
3
4
|
const codecs = require('codecs')
|
|
5
|
+
const sodium = require('sodium-universal')
|
|
4
6
|
|
|
5
7
|
const messages = require('./messages')
|
|
6
8
|
|
|
9
|
+
const slab = b4a.alloc(96)
|
|
10
|
+
const NS = slab.subarray(0, 32)
|
|
11
|
+
const NS_HYPERCORE_INITIATOR = slab.subarray(32, 64)
|
|
12
|
+
const NS_HYPERCORE_RESPONDER = slab.subarray(64, 96)
|
|
13
|
+
|
|
14
|
+
sodium.crypto_generichash(NS, b4a.from('hypercore'))
|
|
15
|
+
sodium.crypto_generichash(NS_HYPERCORE_INITIATOR, b4a.from([0]), NS)
|
|
16
|
+
sodium.crypto_generichash(NS_HYPERCORE_RESPONDER, b4a.from([1]), NS)
|
|
17
|
+
|
|
7
18
|
class Extension {
|
|
8
19
|
constructor (protocol, type, name, handlers) {
|
|
9
20
|
this.protocol = protocol
|
|
@@ -38,7 +49,7 @@ class Extension {
|
|
|
38
49
|
_sendAlias (message, alias) {
|
|
39
50
|
if (this.destroyed) return
|
|
40
51
|
|
|
41
|
-
if (this.
|
|
52
|
+
if (this.remoteSupports) {
|
|
42
53
|
return this.protocol.send(this.type, this.encoding, alias, message)
|
|
43
54
|
}
|
|
44
55
|
|
|
@@ -125,6 +136,7 @@ class Peer {
|
|
|
125
136
|
this.resend = false
|
|
126
137
|
this.state = state
|
|
127
138
|
this.extensions = new Map()
|
|
139
|
+
this.destroyed = false
|
|
128
140
|
|
|
129
141
|
this._destroyer = this._safeDestroy.bind(this)
|
|
130
142
|
}
|
|
@@ -229,6 +241,7 @@ class Peer {
|
|
|
229
241
|
}
|
|
230
242
|
|
|
231
243
|
destroy (err) {
|
|
244
|
+
this.destroyed = true
|
|
232
245
|
return this.protocol.unregisterPeer(this, err)
|
|
233
246
|
}
|
|
234
247
|
}
|
|
@@ -253,11 +266,17 @@ module.exports = class Protocol {
|
|
|
253
266
|
this._localExtensions = 128
|
|
254
267
|
this._remoteExtensions = []
|
|
255
268
|
this._extensions = new Map()
|
|
269
|
+
this._keepAliveInterval = null
|
|
270
|
+
this._pendingCaps = []
|
|
256
271
|
|
|
257
272
|
this._destroyer = this._safeDestroy.bind(this)
|
|
258
273
|
this.noiseStream.on('data', this.onmessage.bind(this))
|
|
259
274
|
this.noiseStream.on('end', this.noiseStream.end) // no half open
|
|
275
|
+
this.noiseStream.on('finish', () => {
|
|
276
|
+
this.setKeepAlive(false)
|
|
277
|
+
})
|
|
260
278
|
this.noiseStream.on('close', () => {
|
|
279
|
+
this.setKeepAlive(false)
|
|
261
280
|
// TODO: If the stream was destroyed with an error, we probably want to forward it here
|
|
262
281
|
for (const peer of this._peers.values()) {
|
|
263
282
|
peer.destroy(null)
|
|
@@ -267,6 +286,18 @@ module.exports = class Protocol {
|
|
|
267
286
|
this._sendHandshake()
|
|
268
287
|
}
|
|
269
288
|
|
|
289
|
+
setKeepAlive (enable) {
|
|
290
|
+
if (enable) {
|
|
291
|
+
if (this._keepAliveInterval) return
|
|
292
|
+
this._keepAliveInterval = setInterval(this.ping.bind(this), 5000)
|
|
293
|
+
if (this._keepAliveInterval.unref) this._keepAliveInterval.unref()
|
|
294
|
+
} else {
|
|
295
|
+
if (!this._keepAliveInterval) return
|
|
296
|
+
clearInterval(this._keepAliveInterval)
|
|
297
|
+
this._keepAliveInterval = null
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
270
301
|
_sendHandshake () {
|
|
271
302
|
const m = { protocolVersion: this.protocolVersion, userAgent: this.userAgent }
|
|
272
303
|
const state = { start: 0, end: 0, buffer: null }
|
|
@@ -283,20 +314,20 @@ module.exports = class Protocol {
|
|
|
283
314
|
|
|
284
315
|
registerPeer (key, discoveryKey, handlers = {}, state = null) {
|
|
285
316
|
const peer = new Peer(this, this._localAliases++, key, discoveryKey, handlers, state)
|
|
286
|
-
this._peers.set(
|
|
317
|
+
this._peers.set(b4a.toString(discoveryKey, 'hex'), peer)
|
|
287
318
|
this._announceCore(peer.alias, key, discoveryKey)
|
|
288
319
|
return peer
|
|
289
320
|
}
|
|
290
321
|
|
|
291
322
|
unregisterPeer (peer, err) {
|
|
292
|
-
this._peers.delete(peer.discoveryKey
|
|
323
|
+
this._peers.delete(b4a.toString(peer.discoveryKey, 'hex'))
|
|
293
324
|
|
|
294
325
|
if (peer.remoteAlias > -1) {
|
|
295
326
|
this._remoteAliases[peer.remoteAlias] = null
|
|
296
327
|
peer.remoteAlias = -1
|
|
297
328
|
}
|
|
298
329
|
|
|
299
|
-
peer.handlers.onunregister(
|
|
330
|
+
peer.handlers.onunregister(peer, err)
|
|
300
331
|
|
|
301
332
|
if (err) this.noiseStream.destroy(err)
|
|
302
333
|
}
|
|
@@ -329,6 +360,11 @@ module.exports = class Protocol {
|
|
|
329
360
|
|
|
330
361
|
if (batch.length === 0) return
|
|
331
362
|
|
|
363
|
+
while (this._pendingCaps.length > 0) {
|
|
364
|
+
const [key, cap] = this._pendingCaps.pop()
|
|
365
|
+
hypercoreCapability(this.noiseStream.isInitiator, this.noiseStream.handshakeHash, key, cap)
|
|
366
|
+
}
|
|
367
|
+
|
|
332
368
|
const state = { start: 0, end: 0, buffer: null }
|
|
333
369
|
const lens = new Array(batch.length)
|
|
334
370
|
|
|
@@ -369,14 +405,24 @@ module.exports = class Protocol {
|
|
|
369
405
|
}
|
|
370
406
|
|
|
371
407
|
_announceCore (alias, key, discoveryKey) {
|
|
408
|
+
const cap = b4a.alloc(32)
|
|
409
|
+
|
|
410
|
+
if (!this.noiseStream.handshakeHash) {
|
|
411
|
+
this._pendingCaps.push([key, cap]) // encode it later...
|
|
412
|
+
} else {
|
|
413
|
+
hypercoreCapability(this.noiseStream.isInitiator, this.noiseStream.handshakeHash, key, cap)
|
|
414
|
+
}
|
|
415
|
+
|
|
372
416
|
this.send(2, messages.core, -1, {
|
|
373
417
|
alias: alias,
|
|
374
418
|
discoveryKey: discoveryKey,
|
|
375
|
-
capability:
|
|
419
|
+
capability: cap
|
|
376
420
|
})
|
|
377
421
|
}
|
|
378
422
|
|
|
379
423
|
_decode (buffer) {
|
|
424
|
+
if (buffer.byteLength === 0) return
|
|
425
|
+
|
|
380
426
|
const state = { start: 0, end: buffer.length, buffer }
|
|
381
427
|
|
|
382
428
|
if (this._firstMessage === true) {
|
|
@@ -441,7 +487,7 @@ module.exports = class Protocol {
|
|
|
441
487
|
}
|
|
442
488
|
|
|
443
489
|
_oncore (m) {
|
|
444
|
-
const hex = m.discoveryKey
|
|
490
|
+
const hex = b4a.toString(m.discoveryKey, 'hex')
|
|
445
491
|
const peer = this._peers.get(hex)
|
|
446
492
|
|
|
447
493
|
// allow one alloc
|
|
@@ -450,7 +496,11 @@ module.exports = class Protocol {
|
|
|
450
496
|
if (m.alias === this._remoteAliases.length) this._remoteAliases.push(null)
|
|
451
497
|
|
|
452
498
|
if (peer) {
|
|
453
|
-
|
|
499
|
+
const expectedCap = hypercoreCapability(!this.noiseStream.isInitiator, this.noiseStream.handshakeHash, peer.key)
|
|
500
|
+
if (!b4a.equals(expectedCap, m.capability)) {
|
|
501
|
+
this.destroy(new Error('Remote sent an invalid capability'))
|
|
502
|
+
return
|
|
503
|
+
}
|
|
454
504
|
|
|
455
505
|
if (m.alias >= this._remoteAliases.length) {
|
|
456
506
|
this.destroy(new Error('Remote alias out of bounds'))
|
|
@@ -477,7 +527,7 @@ module.exports = class Protocol {
|
|
|
477
527
|
}
|
|
478
528
|
|
|
479
529
|
_onunknowncore (m) {
|
|
480
|
-
const peer = this._peers.get(m.discoveryKey
|
|
530
|
+
const peer = this._peers.get(b4a.toString(m.discoveryKey, 'hex'))
|
|
481
531
|
if (!peer) return
|
|
482
532
|
|
|
483
533
|
peer.resend = true
|
|
@@ -505,6 +555,11 @@ module.exports = class Protocol {
|
|
|
505
555
|
return this.noiseStream.write(state.buffer)
|
|
506
556
|
}
|
|
507
557
|
|
|
558
|
+
ping () {
|
|
559
|
+
const empty = this.noiseStream.alloc(0)
|
|
560
|
+
this.noiseStream.write(empty)
|
|
561
|
+
}
|
|
562
|
+
|
|
508
563
|
destroy (err) {
|
|
509
564
|
return this.noiseStream.destroy(err)
|
|
510
565
|
}
|
|
@@ -520,3 +575,9 @@ function noop () {}
|
|
|
520
575
|
function isPromise (p) {
|
|
521
576
|
return !!p && typeof p.then === 'function'
|
|
522
577
|
}
|
|
578
|
+
|
|
579
|
+
function hypercoreCapability (initiator, handshakeHash, key, cap = b4a.alloc(32)) {
|
|
580
|
+
const ns = initiator ? NS_HYPERCORE_INITIATOR : NS_HYPERCORE_RESPONDER
|
|
581
|
+
sodium.crypto_generichash_batch(cap, [handshakeHash, key], ns)
|
|
582
|
+
return cap
|
|
583
|
+
}
|