hypercore 10.0.0-alpha.3 → 10.0.0-alpha.7
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/.github/workflows/test-node.yml +1 -2
- package/README.md +17 -1
- package/index.js +134 -29
- package/lib/block-encryption.js +67 -0
- package/lib/core.js +3 -1
- package/lib/merkle-tree.js +20 -13
- package/lib/protocol.js +3 -1
- package/lib/replicator.js +57 -13
- package/package.json +6 -4
- package/test/basic.js +12 -0
- package/test/encryption.js +85 -0
- package/test/replicate.js +76 -0
package/README.md
CHANGED
|
@@ -63,7 +63,8 @@ Note that `tree`, `data`, and `bitfield` are normally heavily sparse files.
|
|
|
63
63
|
createIfMissing: true, // create a new Hypercore key pair if none was present in storage
|
|
64
64
|
overwrite: false, // overwrite any old Hypercore that might already exist
|
|
65
65
|
valueEncoding: 'json' | 'utf-8' | 'binary', // defaults to binary
|
|
66
|
-
keyPair: kp // optionally pass the public key and secret key as a key pair
|
|
66
|
+
keyPair: kp, // optionally pass the public key and secret key as a key pair
|
|
67
|
+
encryptionKey: k // optionally pass an encryption key to enable block encryption
|
|
67
68
|
}
|
|
68
69
|
```
|
|
69
70
|
|
|
@@ -113,6 +114,7 @@ A range can have the following properties:
|
|
|
113
114
|
{
|
|
114
115
|
start: startIndex,
|
|
115
116
|
end: nonInclusiveEndIndex,
|
|
117
|
+
blocks: [index1, index2, ...],
|
|
116
118
|
linear: false // download range linearly and not randomly
|
|
117
119
|
}
|
|
118
120
|
```
|
|
@@ -125,6 +127,12 @@ To download the full core continously (often referred to as non sparse mode) do
|
|
|
125
127
|
core.download({ start: 0, end: -1 })
|
|
126
128
|
```
|
|
127
129
|
|
|
130
|
+
To downloaded a discrete range of blocks pass a list of indices.
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
core.download({ blocks: [4, 9, 7] });
|
|
134
|
+
```
|
|
135
|
+
|
|
128
136
|
To cancel downloading a range simply destroy the range instance.
|
|
129
137
|
|
|
130
138
|
``` js
|
|
@@ -195,6 +203,10 @@ In contrast to `core.key` this key does not allow you to verify the data but can
|
|
|
195
203
|
|
|
196
204
|
Populated after `ready` has been emitted. Will be `null` before the event.
|
|
197
205
|
|
|
206
|
+
#### `core.encryptionKey`
|
|
207
|
+
|
|
208
|
+
Buffer containing the optional block encryption key of this core. Will be `null` unless block encryption is enabled.
|
|
209
|
+
|
|
198
210
|
#### `core.length`
|
|
199
211
|
|
|
200
212
|
How many blocks of data are available on this core?
|
|
@@ -213,6 +225,10 @@ What is the current fork id of this core?
|
|
|
213
225
|
|
|
214
226
|
Populated after `ready` has been emitted. Will be `0` before the event.
|
|
215
227
|
|
|
228
|
+
#### `core.padding`
|
|
229
|
+
|
|
230
|
+
How much padding is applied to each block of this core? Will be `0` unless block encryption is enabled.
|
|
231
|
+
|
|
216
232
|
#### `const stream = core.replicate(isInitiatorOrReplicationStream)`
|
|
217
233
|
|
|
218
234
|
Create a replication stream. You should pipe this to another Hypercore instance.
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const raf = require('random-access-file')
|
|
|
3
3
|
const isOptions = require('is-options')
|
|
4
4
|
const hypercoreCrypto = require('hypercore-crypto')
|
|
5
5
|
const c = require('compact-encoding')
|
|
6
|
+
const Xache = require('xache')
|
|
6
7
|
const NoiseSecretStream = require('@hyperswarm/secret-stream')
|
|
7
8
|
const codecs = require('codecs')
|
|
8
9
|
|
|
@@ -11,6 +12,7 @@ const fsctl = requireMaybe('fsctl') || { lock: noop, sparse: noop }
|
|
|
11
12
|
const Replicator = require('./lib/replicator')
|
|
12
13
|
const Extensions = require('./lib/extensions')
|
|
13
14
|
const Core = require('./lib/core')
|
|
15
|
+
const BlockEncryption = require('./lib/block-encryption')
|
|
14
16
|
|
|
15
17
|
const promises = Symbol.for('hypercore.promises')
|
|
16
18
|
const inspect = Symbol.for('nodejs.util.inspect.custom')
|
|
@@ -27,14 +29,17 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
27
29
|
opts = key
|
|
28
30
|
key = null
|
|
29
31
|
}
|
|
32
|
+
|
|
30
33
|
if (key && typeof key === 'string') {
|
|
31
34
|
key = Buffer.from(key, 'hex')
|
|
32
35
|
}
|
|
33
|
-
|
|
36
|
+
|
|
37
|
+
if (!opts) opts = {}
|
|
38
|
+
|
|
39
|
+
if (!opts.crypto && key && key.byteLength !== 32) {
|
|
34
40
|
throw new Error('Hypercore key should be 32 bytes')
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
if (!opts) opts = {}
|
|
38
43
|
if (!storage) storage = opts.storage
|
|
39
44
|
|
|
40
45
|
this[promises] = true
|
|
@@ -44,6 +49,8 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
44
49
|
this.core = null
|
|
45
50
|
this.replicator = null
|
|
46
51
|
this.extensions = opts.extensions || new Extensions()
|
|
52
|
+
this.cache = opts.cache === true ? new Xache({ maxSize: 65536, maxAge: 0 }) : (opts.cache || null)
|
|
53
|
+
this.encryption = opts.encryption || null
|
|
47
54
|
|
|
48
55
|
this.valueEncoding = null
|
|
49
56
|
this.key = key || null
|
|
@@ -59,6 +66,8 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
59
66
|
this.closing = null
|
|
60
67
|
this.opening = opts._opening || this._open(key, storage, opts)
|
|
61
68
|
this.opening.catch(noop)
|
|
69
|
+
|
|
70
|
+
this._preappend = this.encryption && preappend.bind(this)
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
[inspect] (depth, opts) {
|
|
@@ -114,6 +123,7 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
114
123
|
...opts,
|
|
115
124
|
sign: opts.sign || (keyPair && keyPair.secretKey && Core.createSigner(this.crypto, keyPair)) || this.sign,
|
|
116
125
|
valueEncoding: this.valueEncoding,
|
|
126
|
+
encryption: this.encryption,
|
|
117
127
|
extensions: this.extensions,
|
|
118
128
|
_opening: this.opening,
|
|
119
129
|
_sessions: this.sessions
|
|
@@ -202,7 +212,7 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
get byteLength () {
|
|
205
|
-
return this.core === null ? 0 : this.core.tree.byteLength
|
|
215
|
+
return this.core === null ? 0 : this.core.tree.byteLength - (this.core.tree.length * this.padding)
|
|
206
216
|
}
|
|
207
217
|
|
|
208
218
|
get fork () {
|
|
@@ -213,6 +223,14 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
213
223
|
return this.replicator === null ? [] : this.replicator.peers
|
|
214
224
|
}
|
|
215
225
|
|
|
226
|
+
get encryptionKey () {
|
|
227
|
+
return this.encryption && this.encryption.key
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
get padding () {
|
|
231
|
+
return this.encryption === null ? 0 : this.encryption.padding
|
|
232
|
+
}
|
|
233
|
+
|
|
216
234
|
ready () {
|
|
217
235
|
return this.opening
|
|
218
236
|
}
|
|
@@ -266,6 +284,11 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
266
284
|
this.key = this.core.header.signer.publicKey
|
|
267
285
|
this.writable = !!this.sign
|
|
268
286
|
|
|
287
|
+
if (!this.encryption && opts.encryptionKey) {
|
|
288
|
+
this.encryption = new BlockEncryption(opts.encryptionKey, this.key)
|
|
289
|
+
this._preappend = preappend.bind(this)
|
|
290
|
+
}
|
|
291
|
+
|
|
269
292
|
this.extensions.attach(this.replicator)
|
|
270
293
|
this.opened = true
|
|
271
294
|
|
|
@@ -281,8 +304,13 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
281
304
|
_oncoreupdate (status, bitfield, value, from) {
|
|
282
305
|
if (status !== 0) {
|
|
283
306
|
for (let i = 0; i < this.sessions.length; i++) {
|
|
284
|
-
if ((status & 0b10) !== 0)
|
|
285
|
-
|
|
307
|
+
if ((status & 0b10) !== 0) {
|
|
308
|
+
if (this.cache) this.cache.clear()
|
|
309
|
+
this.sessions[i].emit('truncate', this.core.tree.fork)
|
|
310
|
+
}
|
|
311
|
+
if ((status & 0b01) !== 0) {
|
|
312
|
+
this.sessions[i].emit('append')
|
|
313
|
+
}
|
|
286
314
|
}
|
|
287
315
|
|
|
288
316
|
this.replicator.broadcastInfo()
|
|
@@ -295,6 +323,12 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
295
323
|
}
|
|
296
324
|
|
|
297
325
|
if (value) {
|
|
326
|
+
if (this.encryption) {
|
|
327
|
+
this.encryption.decrypt(bitfield.start, value)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
value = value.subarray(this.padding)
|
|
331
|
+
|
|
298
332
|
for (let i = 0; i < this.sessions.length; i++) {
|
|
299
333
|
this.sessions[i].emit('download', bitfield.start, value, from)
|
|
300
334
|
}
|
|
@@ -333,7 +367,7 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
333
367
|
async seek (bytes) {
|
|
334
368
|
if (this.opened === false) await this.opening
|
|
335
369
|
|
|
336
|
-
const s = this.core.tree.seek(bytes)
|
|
370
|
+
const s = this.core.tree.seek(bytes, this.padding)
|
|
337
371
|
|
|
338
372
|
return (await s.update()) || this.replicator.requestSeek(s)
|
|
339
373
|
}
|
|
@@ -346,22 +380,52 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
346
380
|
|
|
347
381
|
async get (index, opts) {
|
|
348
382
|
if (this.opened === false) await this.opening
|
|
383
|
+
const c = this.cache && this.cache.get(index)
|
|
384
|
+
if (c) return c
|
|
385
|
+
const fork = this.core.tree.fork
|
|
386
|
+
const b = await this._get(index, opts)
|
|
387
|
+
if (this.cache && fork === this.core.tree.fork && b) this.cache.set(index, b)
|
|
388
|
+
return b
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async _get (index, opts) {
|
|
349
392
|
const encoding = (opts && opts.valueEncoding && c.from(codecs(opts.valueEncoding))) || this.valueEncoding
|
|
350
393
|
|
|
351
|
-
|
|
352
|
-
|
|
394
|
+
let block
|
|
395
|
+
|
|
396
|
+
if (this.core.bitfield.get(index)) {
|
|
397
|
+
block = await this.core.blocks.get(index)
|
|
398
|
+
} else {
|
|
399
|
+
if (opts && opts.onwait) opts.onwait(index)
|
|
400
|
+
block = await this.replicator.requestBlock(index)
|
|
401
|
+
}
|
|
353
402
|
|
|
354
|
-
|
|
403
|
+
if (this.encryption) this.encryption.decrypt(index, block)
|
|
404
|
+
return this._decode(encoding, block)
|
|
355
405
|
}
|
|
356
406
|
|
|
357
407
|
download (range) {
|
|
358
|
-
const start = (range && range.start) || 0
|
|
359
|
-
const end = typeof (range && range.end) === 'number' ? range.end : -1 // download all
|
|
360
408
|
const linear = !!(range && range.linear)
|
|
361
409
|
|
|
362
|
-
|
|
410
|
+
let start
|
|
411
|
+
let end
|
|
412
|
+
let filter
|
|
413
|
+
|
|
414
|
+
if (range && range.blocks) {
|
|
415
|
+
const blocks = range.blocks instanceof Set
|
|
416
|
+
? range.blocks
|
|
417
|
+
: new Set(range.blocks)
|
|
418
|
+
|
|
419
|
+
start = range.start || (blocks.size ? min(range.blocks) : 0)
|
|
420
|
+
end = range.end || (blocks.size ? max(range.blocks) + 1 : 0)
|
|
421
|
+
|
|
422
|
+
filter = (i) => blocks.has(i)
|
|
423
|
+
} else {
|
|
424
|
+
start = (range && range.start) || 0
|
|
425
|
+
end = typeof (range && range.end) === 'number' ? range.end : -1 // download all
|
|
426
|
+
}
|
|
363
427
|
|
|
364
|
-
const r = Replicator.createRange(start, end, linear)
|
|
428
|
+
const r = Replicator.createRange(start, end, filter, linear)
|
|
365
429
|
|
|
366
430
|
if (this.opened) this.replicator.addRange(r)
|
|
367
431
|
else this.opening.then(() => this.replicator.addRange(r), noop)
|
|
@@ -394,22 +458,17 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
394
458
|
if (this.opened === false) await this.opening
|
|
395
459
|
if (this.writable === false) throw new Error('Core is not writable')
|
|
396
460
|
|
|
397
|
-
|
|
398
|
-
const buffers = new Array(blks.length)
|
|
461
|
+
blocks = Array.isArray(blocks) ? blocks : [blocks]
|
|
399
462
|
|
|
400
|
-
|
|
401
|
-
const blk = blks[i]
|
|
463
|
+
const buffers = new Array(blocks.length)
|
|
402
464
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
: this.valueEncoding
|
|
406
|
-
? c.encode(this.valueEncoding, blk)
|
|
407
|
-
: Buffer.from(blk)
|
|
408
|
-
|
|
409
|
-
buffers[i] = buf
|
|
465
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
466
|
+
buffers[i] = this._encode(this.valueEncoding, blocks[i])
|
|
410
467
|
}
|
|
411
468
|
|
|
412
|
-
return await this.core.append(buffers, this.sign
|
|
469
|
+
return await this.core.append(buffers, this.sign, {
|
|
470
|
+
preappend: this._preappend
|
|
471
|
+
})
|
|
413
472
|
}
|
|
414
473
|
|
|
415
474
|
registerExtension (name, handlers) {
|
|
@@ -420,14 +479,38 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
420
479
|
onextensionupdate () {
|
|
421
480
|
if (this.replicator !== null) this.replicator.broadcastOptions()
|
|
422
481
|
}
|
|
423
|
-
}
|
|
424
482
|
|
|
425
|
-
|
|
483
|
+
_encode (enc, val) {
|
|
484
|
+
const state = { start: this.padding, end: this.padding, buffer: null }
|
|
485
|
+
|
|
486
|
+
if (Buffer.isBuffer(val)) {
|
|
487
|
+
if (state.start === 0) return val
|
|
488
|
+
state.end += val.byteLength
|
|
489
|
+
} else if (enc) {
|
|
490
|
+
enc.preencode(state, val)
|
|
491
|
+
} else {
|
|
492
|
+
val = Buffer.from(val)
|
|
493
|
+
if (state.start === 0) return val
|
|
494
|
+
state.end += val.byteLength
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
state.buffer = Buffer.allocUnsafe(state.end)
|
|
498
|
+
|
|
499
|
+
if (enc) enc.encode(state, val)
|
|
500
|
+
else state.buffer.set(val, state.start)
|
|
426
501
|
|
|
427
|
-
|
|
428
|
-
|
|
502
|
+
return state.buffer
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
_decode (enc, block) {
|
|
506
|
+
block = block.subarray(this.padding)
|
|
507
|
+
if (enc) return c.decode(enc, block)
|
|
508
|
+
return block
|
|
509
|
+
}
|
|
429
510
|
}
|
|
430
511
|
|
|
512
|
+
function noop () {}
|
|
513
|
+
|
|
431
514
|
function isStream (s) {
|
|
432
515
|
return typeof s === 'object' && s && typeof s.pipe === 'function'
|
|
433
516
|
}
|
|
@@ -443,3 +526,25 @@ function requireMaybe (name) {
|
|
|
443
526
|
function toHex (buf) {
|
|
444
527
|
return buf && buf.toString('hex')
|
|
445
528
|
}
|
|
529
|
+
|
|
530
|
+
function reduce (iter, fn, acc) {
|
|
531
|
+
for (const item of iter) acc = fn(acc, item)
|
|
532
|
+
return acc
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function min (arr) {
|
|
536
|
+
return reduce(arr, (a, b) => Math.min(a, b), Infinity)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function max (arr) {
|
|
540
|
+
return reduce(arr, (a, b) => Math.max(a, b), -Infinity)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function preappend (blocks) {
|
|
544
|
+
const offset = this.core.tree.length
|
|
545
|
+
const fork = this.core.tree.fork
|
|
546
|
+
|
|
547
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
548
|
+
this.encryption.encrypt(offset + i, blocks[i], fork)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const sodium = require('sodium-universal')
|
|
2
|
+
const c = require('compact-encoding')
|
|
3
|
+
|
|
4
|
+
const nonce = Buffer.alloc(sodium.crypto_stream_NONCEBYTES)
|
|
5
|
+
|
|
6
|
+
module.exports = class BlockEncryption {
|
|
7
|
+
constructor (encryptionKey, hypercoreKey) {
|
|
8
|
+
const subKeys = Buffer.alloc(2 * sodium.crypto_stream_KEYBYTES)
|
|
9
|
+
|
|
10
|
+
this.key = encryptionKey
|
|
11
|
+
this.blockKey = subKeys.subarray(0, sodium.crypto_stream_KEYBYTES)
|
|
12
|
+
this.blindingKey = subKeys.subarray(sodium.crypto_stream_KEYBYTES)
|
|
13
|
+
this.padding = 8
|
|
14
|
+
|
|
15
|
+
sodium.crypto_generichash(this.blockKey, encryptionKey, hypercoreKey)
|
|
16
|
+
sodium.crypto_generichash(this.blindingKey, this.blockKey)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
encrypt (index, block, fork) {
|
|
20
|
+
const padding = block.subarray(0, this.padding)
|
|
21
|
+
block = block.subarray(this.padding)
|
|
22
|
+
|
|
23
|
+
c.uint64.encode({ start: 0, end: 8, buffer: padding }, fork)
|
|
24
|
+
c.uint64.encode({ start: 0, end: 8, buffer: nonce }, index)
|
|
25
|
+
|
|
26
|
+
// Zero out any previous padding.
|
|
27
|
+
nonce.fill(0, 8, 8 + padding.byteLength)
|
|
28
|
+
|
|
29
|
+
// Blind the fork ID, possibly risking reusing the nonce on a reorg of the
|
|
30
|
+
// Hypercore. This is fine as the blinding is best-effort and the latest
|
|
31
|
+
// fork ID shared on replication anyway.
|
|
32
|
+
sodium.crypto_stream_xor(
|
|
33
|
+
padding,
|
|
34
|
+
padding,
|
|
35
|
+
nonce,
|
|
36
|
+
this.blindingKey
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
nonce.set(padding, 8)
|
|
40
|
+
|
|
41
|
+
// The combination of a (blinded) fork ID and a block index is unique for a
|
|
42
|
+
// given Hypercore and is therefore a valid nonce for encrypting the block.
|
|
43
|
+
sodium.crypto_stream_xor(
|
|
44
|
+
block,
|
|
45
|
+
block,
|
|
46
|
+
nonce,
|
|
47
|
+
this.blockKey
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
decrypt (index, block) {
|
|
52
|
+
const padding = block.subarray(0, this.padding)
|
|
53
|
+
block = block.subarray(this.padding)
|
|
54
|
+
|
|
55
|
+
c.uint64.encode({ start: 0, end: 8, buffer: nonce }, index)
|
|
56
|
+
|
|
57
|
+
nonce.set(padding, 8)
|
|
58
|
+
|
|
59
|
+
// Decrypt the block using the blinded fork ID.
|
|
60
|
+
sodium.crypto_stream_xor(
|
|
61
|
+
block,
|
|
62
|
+
block,
|
|
63
|
+
nonce,
|
|
64
|
+
this.blockKey
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
package/lib/core.js
CHANGED
|
@@ -214,10 +214,12 @@ module.exports = class Core {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
async append (values, sign = this.defaultSign) {
|
|
217
|
+
async append (values, sign = this.defaultSign, hooks = {}) {
|
|
218
218
|
await this._mutex.lock()
|
|
219
219
|
|
|
220
220
|
try {
|
|
221
|
+
if (hooks.preappend) await hooks.preappend(values)
|
|
222
|
+
|
|
221
223
|
if (!values.length) return this.tree.length
|
|
222
224
|
|
|
223
225
|
const batch = this.tree.batch()
|
package/lib/merkle-tree.js
CHANGED
|
@@ -272,11 +272,15 @@ class ReorgBatch extends MerkleTreeBatch {
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
class ByteSeeker {
|
|
275
|
-
constructor (tree, bytes) {
|
|
275
|
+
constructor (tree, bytes, padding = 0) {
|
|
276
276
|
this.tree = tree
|
|
277
277
|
this.bytes = bytes
|
|
278
|
-
this.
|
|
279
|
-
|
|
278
|
+
this.padding = padding
|
|
279
|
+
|
|
280
|
+
const size = tree.byteLength - (tree.length * padding)
|
|
281
|
+
|
|
282
|
+
this.start = bytes >= size ? tree.length : 0
|
|
283
|
+
this.end = bytes < size ? tree.length : 0
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
nodes () {
|
|
@@ -287,12 +291,12 @@ class ByteSeeker {
|
|
|
287
291
|
if (!bytes) return [0, 0]
|
|
288
292
|
|
|
289
293
|
for (const node of this.tree.roots) { // all async ticks happen once we find the root so safe
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
294
|
+
let size = node.size
|
|
295
|
+
if (this.padding > 0) size -= this.padding * flat.countLeaves(node.index)
|
|
293
296
|
|
|
294
|
-
if (bytes
|
|
295
|
-
|
|
297
|
+
if (bytes === size) return [flat.rightSpan(node.index) + 2, 0]
|
|
298
|
+
if (bytes > size) {
|
|
299
|
+
bytes -= size
|
|
296
300
|
continue
|
|
297
301
|
}
|
|
298
302
|
|
|
@@ -301,9 +305,12 @@ class ByteSeeker {
|
|
|
301
305
|
while ((ite.index & 1) !== 0) {
|
|
302
306
|
const l = await this.tree.get(ite.leftChild(), false)
|
|
303
307
|
if (l) {
|
|
304
|
-
|
|
305
|
-
if (
|
|
306
|
-
|
|
308
|
+
let size = l.size
|
|
309
|
+
if (this.padding > 0) size -= this.padding * ite.countLeaves()
|
|
310
|
+
|
|
311
|
+
if (size === bytes) return [ite.rightSpan() + 2, 0]
|
|
312
|
+
if (size > bytes) continue
|
|
313
|
+
bytes -= size
|
|
307
314
|
ite.sibling()
|
|
308
315
|
} else {
|
|
309
316
|
ite.parent()
|
|
@@ -355,8 +362,8 @@ module.exports = class MerkleTree {
|
|
|
355
362
|
return new MerkleTreeBatch(this)
|
|
356
363
|
}
|
|
357
364
|
|
|
358
|
-
seek (bytes) {
|
|
359
|
-
return new ByteSeeker(this, bytes)
|
|
365
|
+
seek (bytes, padding) {
|
|
366
|
+
return new ByteSeeker(this, bytes, padding)
|
|
360
367
|
}
|
|
361
368
|
|
|
362
369
|
hash () {
|
package/lib/protocol.js
CHANGED
|
@@ -125,6 +125,7 @@ class Peer {
|
|
|
125
125
|
this.resend = false
|
|
126
126
|
this.state = state
|
|
127
127
|
this.extensions = new Map()
|
|
128
|
+
this.destroyed = false
|
|
128
129
|
|
|
129
130
|
this._destroyer = this._safeDestroy.bind(this)
|
|
130
131
|
}
|
|
@@ -229,6 +230,7 @@ class Peer {
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
destroy (err) {
|
|
233
|
+
this.destroyed = true
|
|
232
234
|
return this.protocol.unregisterPeer(this, err)
|
|
233
235
|
}
|
|
234
236
|
}
|
|
@@ -296,7 +298,7 @@ module.exports = class Protocol {
|
|
|
296
298
|
peer.remoteAlias = -1
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
peer.handlers.onunregister(
|
|
301
|
+
peer.handlers.onunregister(peer, err)
|
|
300
302
|
|
|
301
303
|
if (err) this.noiseStream.destroy(err)
|
|
302
304
|
}
|
package/lib/replicator.js
CHANGED
|
@@ -25,11 +25,12 @@ class InvertedPromise {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
class Request {
|
|
28
|
-
constructor (index, seek) {
|
|
28
|
+
constructor (index, seek, nodes) {
|
|
29
29
|
this.peer = null
|
|
30
30
|
this.index = index
|
|
31
31
|
this.seek = seek
|
|
32
32
|
this.value = seek === 0
|
|
33
|
+
this.nodes = nodes
|
|
33
34
|
this.promises = []
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -119,9 +120,10 @@ class Seek {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
class Range {
|
|
122
|
-
constructor (start, end, linear) {
|
|
123
|
+
constructor (start, end, filter, linear) {
|
|
123
124
|
this.start = start
|
|
124
125
|
this.end = end
|
|
126
|
+
this.filter = filter
|
|
125
127
|
this.linear = !!linear
|
|
126
128
|
this.promise = null
|
|
127
129
|
this.done = false
|
|
@@ -144,7 +146,7 @@ class Range {
|
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
for (; this._start < this.end; this._start++) {
|
|
147
|
-
if (!bitfield.get(this._start)) return false
|
|
149
|
+
if (this.filter(this._start) && !bitfield.get(this._start)) return false
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
return true
|
|
@@ -278,7 +280,9 @@ class RequestPool {
|
|
|
278
280
|
|
|
279
281
|
_updateSeek (peer, seek) {
|
|
280
282
|
if (seek.request) return false
|
|
281
|
-
|
|
283
|
+
// We have to snapshot the nodes here now, due to the caching of the request...
|
|
284
|
+
const nodes = log2(seek.seeker.end - seek.seeker.start)
|
|
285
|
+
seek.request = this._requestRange(peer, seek.seeker.start, seek.seeker.end, seek.seeker.bytes, nodes)
|
|
282
286
|
return seek.request !== null
|
|
283
287
|
}
|
|
284
288
|
|
|
@@ -296,10 +300,13 @@ class RequestPool {
|
|
|
296
300
|
const end = range.end === -1 ? peer.state.length : range.end
|
|
297
301
|
if (end <= range._start) return false
|
|
298
302
|
|
|
299
|
-
if (range.linear) return !!this._requestRange(peer, range._start, end, 0)
|
|
303
|
+
if (range.linear) return !!this._requestRange(peer, range._start, end, 0, 0, range.filter)
|
|
300
304
|
|
|
301
305
|
const r = range._start + Math.floor(Math.random() * (end - range._start))
|
|
302
|
-
return !!(
|
|
306
|
+
return !!(
|
|
307
|
+
this._requestRange(peer, r, end, 0, 0, range.filter) ||
|
|
308
|
+
this._requestRange(peer, range._start, r, 0, 0, range.filter)
|
|
309
|
+
)
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
_updateUpgrade (peer) {
|
|
@@ -328,7 +335,7 @@ class RequestPool {
|
|
|
328
335
|
this.pendingUpgrade = null
|
|
329
336
|
}
|
|
330
337
|
|
|
331
|
-
_requestRange (peer, start, end, seek) {
|
|
338
|
+
_requestRange (peer, start, end, seek, nodes, filter = tautology) {
|
|
332
339
|
const remote = peer.state.bitfield
|
|
333
340
|
const local = this.core.bitfield
|
|
334
341
|
|
|
@@ -336,12 +343,12 @@ class RequestPool {
|
|
|
336
343
|
if (end === -1) end = peer.state.length
|
|
337
344
|
|
|
338
345
|
for (let i = start; i < end; i++) {
|
|
339
|
-
if (!remote.get(i) || local.get(i)) continue
|
|
346
|
+
if (!filter(i) || !remote.get(i) || local.get(i)) continue
|
|
340
347
|
// TODO: if this was a NO_VALUE request, retry if no blocks can be found elsewhere
|
|
341
348
|
if (this.requests.has(i)) continue
|
|
342
349
|
|
|
343
350
|
// TODO: if seeking and i >= core.length, let that takes precendance in the upgrade req
|
|
344
|
-
const req = new Request(i, i < this.core.tree.length ? seek : 0)
|
|
351
|
+
const req = new Request(i, i < this.core.tree.length ? seek : 0, nodes)
|
|
345
352
|
this.requests.set(i, req)
|
|
346
353
|
this.send(peer, req)
|
|
347
354
|
return req
|
|
@@ -390,16 +397,28 @@ class RequestPool {
|
|
|
390
397
|
|
|
391
398
|
if (data.block.index < this.core.tree.length || this.core.truncating > 0) {
|
|
392
399
|
try {
|
|
393
|
-
data.block.nodes = await this.core.tree.nodes(data.block.index * 2)
|
|
400
|
+
data.block.nodes = Math.max(req.nodes, await this.core.tree.nodes(data.block.index * 2))
|
|
394
401
|
} catch (err) {
|
|
395
402
|
console.error('TODO handle me:', err.stack)
|
|
396
403
|
}
|
|
397
404
|
}
|
|
398
405
|
|
|
406
|
+
if (peer.destroyed) {
|
|
407
|
+
req.peer = null
|
|
408
|
+
this.pending.push(req)
|
|
409
|
+
if (upgrading) {
|
|
410
|
+
this.upgrading.resolve()
|
|
411
|
+
this.upgrading = null
|
|
412
|
+
}
|
|
413
|
+
this.replicator.updateAll()
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
|
|
399
417
|
if (fork !== this.core.tree.fork || paused(peer, this.core.tree.fork) || this.core.truncating > 0) {
|
|
400
418
|
if (peer.state.inflight > 0) peer.state.inflight--
|
|
401
419
|
if (req.promises.length) { // someone is eagerly waiting for this request
|
|
402
420
|
req.peer = null // resend on some other peer
|
|
421
|
+
this.pending.push(req)
|
|
403
422
|
} else { // otherwise delete the request
|
|
404
423
|
this.requests.delete(req.index)
|
|
405
424
|
}
|
|
@@ -594,7 +613,7 @@ class RequestPool {
|
|
|
594
613
|
return e.createPromise()
|
|
595
614
|
}
|
|
596
615
|
|
|
597
|
-
const r = new Request(index, 0)
|
|
616
|
+
const r = new Request(index, 0, 0)
|
|
598
617
|
|
|
599
618
|
this.requests.set(index, r)
|
|
600
619
|
this.pending.push(r)
|
|
@@ -662,8 +681,18 @@ module.exports = class Replicator {
|
|
|
662
681
|
return promise
|
|
663
682
|
}
|
|
664
683
|
|
|
665
|
-
static createRange (start, end, linear) {
|
|
666
|
-
|
|
684
|
+
static createRange (start, end, filter, linear) {
|
|
685
|
+
// createRange(start, end)
|
|
686
|
+
if (filter === undefined) {
|
|
687
|
+
filter = tautology
|
|
688
|
+
|
|
689
|
+
// createRange(start, end, linear)
|
|
690
|
+
} else if (typeof filter === 'boolean') {
|
|
691
|
+
linear = filter
|
|
692
|
+
filter = tautology
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return new Range(start, end, filter, linear)
|
|
667
696
|
}
|
|
668
697
|
|
|
669
698
|
addRange (range) {
|
|
@@ -810,3 +839,18 @@ function pages (core) {
|
|
|
810
839
|
}
|
|
811
840
|
|
|
812
841
|
function noop () {}
|
|
842
|
+
|
|
843
|
+
function tautology () {
|
|
844
|
+
return true
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function log2 (n) {
|
|
848
|
+
let res = 1
|
|
849
|
+
|
|
850
|
+
while (n > 2) {
|
|
851
|
+
n /= 2
|
|
852
|
+
res++
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return res
|
|
856
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypercore",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.7",
|
|
4
4
|
"description": "Hypercore 10",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -31,15 +31,17 @@
|
|
|
31
31
|
"@hyperswarm/secret-stream": "^5.0.0",
|
|
32
32
|
"big-sparse-array": "^1.0.2",
|
|
33
33
|
"codecs": "^2.2.0",
|
|
34
|
-
"compact-encoding": "^2.
|
|
34
|
+
"compact-encoding": "^2.5.0",
|
|
35
35
|
"crc32-universal": "^1.0.1",
|
|
36
|
-
"flat-tree": "^1.
|
|
36
|
+
"flat-tree": "^1.9.0",
|
|
37
37
|
"hypercore-crypto": "^2.1.1",
|
|
38
38
|
"is-options": "^1.0.1",
|
|
39
39
|
"random-access-file": "^2.1.4",
|
|
40
40
|
"random-array-iterator": "^1.0.0",
|
|
41
41
|
"safety-catch": "^1.0.1",
|
|
42
|
-
"
|
|
42
|
+
"sodium-universal": "^3.0.4",
|
|
43
|
+
"uint64le": "^1.0.0",
|
|
44
|
+
"xache": "^1.0.0"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"brittle": "^1.4.1",
|
package/test/basic.js
CHANGED
|
@@ -76,3 +76,15 @@ test('storage options', async function (t) {
|
|
|
76
76
|
t.alike(await core.get(0), Buffer.from('hello'))
|
|
77
77
|
t.end()
|
|
78
78
|
})
|
|
79
|
+
|
|
80
|
+
test(
|
|
81
|
+
'allow publicKeys with different byteLength that 32, if opts.crypto were passed',
|
|
82
|
+
function (t) {
|
|
83
|
+
const key = Buffer.alloc(33).fill('a')
|
|
84
|
+
|
|
85
|
+
const core = new Hypercore(ram, key, { crypto: {} })
|
|
86
|
+
|
|
87
|
+
t.is(core.key, key)
|
|
88
|
+
t.pass('creating a core with more than 32 byteLength key did not throw')
|
|
89
|
+
}
|
|
90
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const test = require('brittle')
|
|
2
|
+
const { create, replicate } = require('./helpers')
|
|
3
|
+
|
|
4
|
+
const encryptionKey = Buffer.alloc(32, 'hello world')
|
|
5
|
+
|
|
6
|
+
test('encrypted append and get', async function (t) {
|
|
7
|
+
const a = await create({ encryptionKey })
|
|
8
|
+
|
|
9
|
+
t.alike(a.encryptionKey, encryptionKey)
|
|
10
|
+
|
|
11
|
+
await a.append(['hello'])
|
|
12
|
+
|
|
13
|
+
t.is(a.byteLength, 5)
|
|
14
|
+
t.is(a.core.tree.byteLength, 5 + a.padding)
|
|
15
|
+
|
|
16
|
+
const unencrypted = await a.get(0)
|
|
17
|
+
const encrypted = await a.core.blocks.get(0)
|
|
18
|
+
|
|
19
|
+
t.alike(unencrypted, Buffer.from('hello'))
|
|
20
|
+
t.unlike(unencrypted, encrypted)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('encrypted seek', async function (t) {
|
|
24
|
+
const a = await create({ encryptionKey })
|
|
25
|
+
|
|
26
|
+
await a.append(['hello', 'world', '!'])
|
|
27
|
+
|
|
28
|
+
t.alike(await a.seek(0), [0, 0])
|
|
29
|
+
t.alike(await a.seek(4), [0, 4])
|
|
30
|
+
t.alike(await a.seek(5), [1, 0])
|
|
31
|
+
t.alike(await a.seek(6), [1, 1])
|
|
32
|
+
t.alike(await a.seek(6), [1, 1])
|
|
33
|
+
t.alike(await a.seek(9), [1, 4])
|
|
34
|
+
t.alike(await a.seek(10), [2, 0])
|
|
35
|
+
t.alike(await a.seek(11), [3, 0])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('encrypted replication', async function (t) {
|
|
39
|
+
const a = await create({ encryptionKey })
|
|
40
|
+
|
|
41
|
+
await a.append(['a', 'b', 'c', 'd', 'e'])
|
|
42
|
+
|
|
43
|
+
t.test('with encryption key', async function (t) {
|
|
44
|
+
t.plan(10)
|
|
45
|
+
|
|
46
|
+
const b = await create(a.key, { encryptionKey })
|
|
47
|
+
|
|
48
|
+
b.on('download', (i, block) => {
|
|
49
|
+
t.alike(block, Buffer.from([i + /* a */ 0x61]))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
replicate(a, b, t)
|
|
53
|
+
|
|
54
|
+
const r = b.download({ start: 0, end: a.length })
|
|
55
|
+
await r.downloaded()
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < 5; i++) {
|
|
58
|
+
t.alike(await b.get(i), await a.get(i))
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
t.test('without encryption key', async function (t) {
|
|
63
|
+
const b = await create(a.key)
|
|
64
|
+
|
|
65
|
+
replicate(a, b, t)
|
|
66
|
+
|
|
67
|
+
const r = b.download({ start: 0, end: a.length })
|
|
68
|
+
await r.downloaded()
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < 5; i++) {
|
|
71
|
+
t.unlike(await b.get(i), await a.get(i))
|
|
72
|
+
t.alike(await b.get(i), await a.core.blocks.get(i))
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('encrypted sessions', async function (t) {
|
|
78
|
+
const a = await create({ encryptionKey })
|
|
79
|
+
|
|
80
|
+
await a.append(['hello'])
|
|
81
|
+
|
|
82
|
+
const session = a.session()
|
|
83
|
+
|
|
84
|
+
t.alike(await session.get(0), Buffer.from('hello'))
|
|
85
|
+
})
|
package/test/replicate.js
CHANGED
|
@@ -294,3 +294,79 @@ test('multiplexing multiple times over the same stream', async function (t) {
|
|
|
294
294
|
t.is(b1.length, a1.length, 'same length')
|
|
295
295
|
t.end()
|
|
296
296
|
})
|
|
297
|
+
|
|
298
|
+
test('destroying a stream and re-replicating works', async function (t) {
|
|
299
|
+
const core = await create()
|
|
300
|
+
|
|
301
|
+
while (core.length < 33) await core.append(Buffer.from('#' + core.length))
|
|
302
|
+
|
|
303
|
+
const clone = await create(core.key)
|
|
304
|
+
|
|
305
|
+
let s1 = core.replicate(true)
|
|
306
|
+
let s2 = clone.replicate(false)
|
|
307
|
+
|
|
308
|
+
s1.pipe(s2).pipe(s1)
|
|
309
|
+
|
|
310
|
+
await s2.opened
|
|
311
|
+
|
|
312
|
+
const all = []
|
|
313
|
+
for (let i = 0; i < 33; i++) {
|
|
314
|
+
all.push(clone.get(i))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
clone.once('download', function () {
|
|
318
|
+
// simulate stream failure in the middle of bulk downloading
|
|
319
|
+
s1.destroy()
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
await new Promise((resolve) => s1.once('close', resolve))
|
|
323
|
+
|
|
324
|
+
// retry
|
|
325
|
+
s1 = core.replicate(true)
|
|
326
|
+
s2 = clone.replicate(false)
|
|
327
|
+
|
|
328
|
+
s1.pipe(s2).pipe(s1)
|
|
329
|
+
|
|
330
|
+
const blocks = await Promise.all(all)
|
|
331
|
+
|
|
332
|
+
t.is(blocks.length, 33, 'downloaded 33 blocks')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('replicate discrete range', async function (t) {
|
|
336
|
+
const a = await create()
|
|
337
|
+
|
|
338
|
+
await a.append(['a', 'b', 'c', 'd', 'e'])
|
|
339
|
+
|
|
340
|
+
const b = await create(a.key)
|
|
341
|
+
|
|
342
|
+
let d = 0
|
|
343
|
+
b.on('download', () => d++)
|
|
344
|
+
|
|
345
|
+
replicate(a, b, t)
|
|
346
|
+
|
|
347
|
+
const r = b.download({ blocks: [0, 2, 3] })
|
|
348
|
+
await r.downloaded()
|
|
349
|
+
|
|
350
|
+
t.is(d, 3)
|
|
351
|
+
t.alike(await b.get(0), Buffer.from('a'))
|
|
352
|
+
t.alike(await b.get(2), Buffer.from('c'))
|
|
353
|
+
t.alike(await b.get(3), Buffer.from('d'))
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test('replicate discrete empty range', async function (t) {
|
|
357
|
+
const a = await create()
|
|
358
|
+
|
|
359
|
+
await a.append(['a', 'b', 'c', 'd', 'e'])
|
|
360
|
+
|
|
361
|
+
const b = await create(a.key)
|
|
362
|
+
|
|
363
|
+
let d = 0
|
|
364
|
+
b.on('download', () => d++)
|
|
365
|
+
|
|
366
|
+
replicate(a, b, t)
|
|
367
|
+
|
|
368
|
+
const r = b.download({ blocks: [] })
|
|
369
|
+
await r.downloaded()
|
|
370
|
+
|
|
371
|
+
t.is(d, 0)
|
|
372
|
+
})
|