hypercore 10.0.0-alpha.0 → 10.0.0-alpha.12
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 +30 -1
- package/__snapshots__/test/storage.js.snapshot.cjs +15 -0
- package/index.js +143 -36
- package/lib/bitfield.js +3 -2
- package/lib/block-encryption.js +68 -0
- package/lib/block-store.js +3 -1
- package/lib/core.js +4 -13
- package/lib/merkle-tree.js +43 -29
- package/lib/oplog.js +4 -3
- package/lib/protocol.js +9 -6
- package/lib/replicator.js +60 -15
- package/lib/streams.js +39 -0
- package/package.json +7 -5
- package/test/basic.js +21 -10
- package/test/encryption.js +123 -0
- package/test/extension.js +8 -8
- package/test/helpers/index.js +7 -3
- package/test/replicate.js +88 -12
- package/test/storage.js +31 -0
- package/test/streams.js +55 -0
package/index.js
CHANGED
|
@@ -3,6 +3,8 @@ 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 b4a = require('b4a')
|
|
7
|
+
const Xache = require('xache')
|
|
6
8
|
const NoiseSecretStream = require('@hyperswarm/secret-stream')
|
|
7
9
|
const codecs = require('codecs')
|
|
8
10
|
|
|
@@ -11,6 +13,8 @@ const fsctl = requireMaybe('fsctl') || { lock: noop, sparse: noop }
|
|
|
11
13
|
const Replicator = require('./lib/replicator')
|
|
12
14
|
const Extensions = require('./lib/extensions')
|
|
13
15
|
const Core = require('./lib/core')
|
|
16
|
+
const BlockEncryption = require('./lib/block-encryption')
|
|
17
|
+
const { ReadStream } = require('./lib/streams')
|
|
14
18
|
|
|
15
19
|
const promises = Symbol.for('hypercore.promises')
|
|
16
20
|
const inspect = Symbol.for('nodejs.util.inspect.custom')
|
|
@@ -27,14 +31,17 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
27
31
|
opts = key
|
|
28
32
|
key = null
|
|
29
33
|
}
|
|
34
|
+
|
|
30
35
|
if (key && typeof key === 'string') {
|
|
31
|
-
key =
|
|
36
|
+
key = b4a.from(key, 'hex')
|
|
32
37
|
}
|
|
33
|
-
|
|
38
|
+
|
|
39
|
+
if (!opts) opts = {}
|
|
40
|
+
|
|
41
|
+
if (!opts.crypto && key && key.byteLength !== 32) {
|
|
34
42
|
throw new Error('Hypercore key should be 32 bytes')
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
if (!opts) opts = {}
|
|
38
45
|
if (!storage) storage = opts.storage
|
|
39
46
|
|
|
40
47
|
this[promises] = true
|
|
@@ -43,7 +50,9 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
43
50
|
this.crypto = opts.crypto || hypercoreCrypto
|
|
44
51
|
this.core = null
|
|
45
52
|
this.replicator = null
|
|
53
|
+
this.encryption = null
|
|
46
54
|
this.extensions = opts.extensions || new Extensions()
|
|
55
|
+
this.cache = opts.cache === true ? new Xache({ maxSize: 65536, maxAge: 0 }) : (opts.cache || null)
|
|
47
56
|
|
|
48
57
|
this.valueEncoding = null
|
|
49
58
|
this.key = key || null
|
|
@@ -59,6 +68,8 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
59
68
|
this.closing = null
|
|
60
69
|
this.opening = opts._opening || this._open(key, storage, opts)
|
|
61
70
|
this.opening.catch(noop)
|
|
71
|
+
|
|
72
|
+
this._preappend = preappend.bind(this)
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
[inspect] (depth, opts) {
|
|
@@ -84,12 +95,14 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
84
95
|
return noiseStream.rawStream
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
static defaultStorage (storage) {
|
|
98
|
+
static defaultStorage (storage, opts = {}) {
|
|
88
99
|
if (typeof storage !== 'string') return storage
|
|
89
100
|
const directory = storage
|
|
101
|
+
const toLock = opts.lock || 'oplog'
|
|
90
102
|
return function createFile (name) {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
103
|
+
const locked = name === toLock || name.endsWith('/' + toLock)
|
|
104
|
+
const lock = locked ? fsctl.lock : null
|
|
105
|
+
const sparse = locked ? null : null // fsctl.sparse, disable sparse on windows - seems to fail for some people. TODO: investigate
|
|
93
106
|
return raf(name, { directory, lock, sparse })
|
|
94
107
|
}
|
|
95
108
|
}
|
|
@@ -131,6 +144,7 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
131
144
|
this.discoveryKey = o.discoveryKey
|
|
132
145
|
this.core = o.core
|
|
133
146
|
this.replicator = o.replicator
|
|
147
|
+
this.encryption = o.encryption
|
|
134
148
|
this.writable = !!this.sign
|
|
135
149
|
this.autoClose = o.autoClose
|
|
136
150
|
}
|
|
@@ -200,7 +214,7 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
200
214
|
}
|
|
201
215
|
|
|
202
216
|
get byteLength () {
|
|
203
|
-
return this.core === null ? 0 : this.core.tree.byteLength
|
|
217
|
+
return this.core === null ? 0 : this.core.tree.byteLength - (this.core.tree.length * this.padding)
|
|
204
218
|
}
|
|
205
219
|
|
|
206
220
|
get fork () {
|
|
@@ -211,6 +225,14 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
211
225
|
return this.replicator === null ? [] : this.replicator.peers
|
|
212
226
|
}
|
|
213
227
|
|
|
228
|
+
get encryptionKey () {
|
|
229
|
+
return this.encryption && this.encryption.key
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get padding () {
|
|
233
|
+
return this.encryption === null ? 0 : this.encryption.padding
|
|
234
|
+
}
|
|
235
|
+
|
|
214
236
|
ready () {
|
|
215
237
|
return this.opening
|
|
216
238
|
}
|
|
@@ -264,6 +286,10 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
264
286
|
this.key = this.core.header.signer.publicKey
|
|
265
287
|
this.writable = !!this.sign
|
|
266
288
|
|
|
289
|
+
if (!this.encryption && opts.encryptionKey) {
|
|
290
|
+
this.encryption = new BlockEncryption(opts.encryptionKey, this.key)
|
|
291
|
+
}
|
|
292
|
+
|
|
267
293
|
this.extensions.attach(this.replicator)
|
|
268
294
|
this.opened = true
|
|
269
295
|
|
|
@@ -279,8 +305,13 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
279
305
|
_oncoreupdate (status, bitfield, value, from) {
|
|
280
306
|
if (status !== 0) {
|
|
281
307
|
for (let i = 0; i < this.sessions.length; i++) {
|
|
282
|
-
if ((status & 0b10) !== 0)
|
|
283
|
-
|
|
308
|
+
if ((status & 0b10) !== 0) {
|
|
309
|
+
if (this.cache) this.cache.clear()
|
|
310
|
+
this.sessions[i].emit('truncate', this.core.tree.fork)
|
|
311
|
+
}
|
|
312
|
+
if ((status & 0b01) !== 0) {
|
|
313
|
+
this.sessions[i].emit('append')
|
|
314
|
+
}
|
|
284
315
|
}
|
|
285
316
|
|
|
286
317
|
this.replicator.broadcastInfo()
|
|
@@ -293,8 +324,10 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
293
324
|
}
|
|
294
325
|
|
|
295
326
|
if (value) {
|
|
327
|
+
const byteLength = value.byteLength - this.padding
|
|
328
|
+
|
|
296
329
|
for (let i = 0; i < this.sessions.length; i++) {
|
|
297
|
-
this.sessions[i].emit('download', bitfield.start,
|
|
330
|
+
this.sessions[i].emit('download', bitfield.start, byteLength, from)
|
|
298
331
|
}
|
|
299
332
|
}
|
|
300
333
|
}
|
|
@@ -331,7 +364,7 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
331
364
|
async seek (bytes) {
|
|
332
365
|
if (this.opened === false) await this.opening
|
|
333
366
|
|
|
334
|
-
const s = this.core.tree.seek(bytes)
|
|
367
|
+
const s = this.core.tree.seek(bytes, this.padding)
|
|
335
368
|
|
|
336
369
|
return (await s.update()) || this.replicator.requestSeek(s)
|
|
337
370
|
}
|
|
@@ -344,25 +377,59 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
344
377
|
|
|
345
378
|
async get (index, opts) {
|
|
346
379
|
if (this.opened === false) await this.opening
|
|
380
|
+
const c = this.cache && this.cache.get(index)
|
|
381
|
+
if (c) return c
|
|
382
|
+
const fork = this.core.tree.fork
|
|
383
|
+
const b = await this._get(index, opts)
|
|
384
|
+
if (this.cache && fork === this.core.tree.fork && b) this.cache.set(index, b)
|
|
385
|
+
return b
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async _get (index, opts) {
|
|
347
389
|
const encoding = (opts && opts.valueEncoding && c.from(codecs(opts.valueEncoding))) || this.valueEncoding
|
|
348
390
|
|
|
349
|
-
|
|
350
|
-
|
|
391
|
+
let block
|
|
392
|
+
|
|
393
|
+
if (this.core.bitfield.get(index)) {
|
|
394
|
+
block = await this.core.blocks.get(index)
|
|
395
|
+
} else {
|
|
396
|
+
if (opts && opts.onwait) opts.onwait(index)
|
|
397
|
+
block = await this.replicator.requestBlock(index)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (this.encryption) this.encryption.decrypt(index, block)
|
|
401
|
+
return this._decode(encoding, block)
|
|
402
|
+
}
|
|
351
403
|
|
|
352
|
-
|
|
404
|
+
createReadStream (opts) {
|
|
405
|
+
return new ReadStream(this, opts)
|
|
353
406
|
}
|
|
354
407
|
|
|
355
408
|
download (range) {
|
|
356
|
-
const start = (range && range.start) || 0
|
|
357
|
-
const end = typeof (range && range.end) === 'number' ? range.end : -1 // download all
|
|
358
409
|
const linear = !!(range && range.linear)
|
|
359
410
|
|
|
360
|
-
|
|
411
|
+
let start
|
|
412
|
+
let end
|
|
413
|
+
let filter
|
|
414
|
+
|
|
415
|
+
if (range && range.blocks) {
|
|
416
|
+
const blocks = range.blocks instanceof Set
|
|
417
|
+
? range.blocks
|
|
418
|
+
: new Set(range.blocks)
|
|
419
|
+
|
|
420
|
+
start = range.start || (blocks.size ? min(range.blocks) : 0)
|
|
421
|
+
end = range.end || (blocks.size ? max(range.blocks) + 1 : 0)
|
|
422
|
+
|
|
423
|
+
filter = (i) => blocks.has(i)
|
|
424
|
+
} else {
|
|
425
|
+
start = (range && range.start) || 0
|
|
426
|
+
end = typeof (range && range.end) === 'number' ? range.end : -1 // download all
|
|
427
|
+
}
|
|
361
428
|
|
|
362
|
-
const r = Replicator.createRange(start, end, linear)
|
|
429
|
+
const r = Replicator.createRange(start, end, filter, linear)
|
|
363
430
|
|
|
364
431
|
if (this.opened) this.replicator.addRange(r)
|
|
365
|
-
else this.opening.then(() => this.replicator.addRange(r))
|
|
432
|
+
else this.opening.then(() => this.replicator.addRange(r), noop)
|
|
366
433
|
|
|
367
434
|
return r
|
|
368
435
|
}
|
|
@@ -392,22 +459,16 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
392
459
|
if (this.opened === false) await this.opening
|
|
393
460
|
if (this.writable === false) throw new Error('Core is not writable')
|
|
394
461
|
|
|
395
|
-
|
|
396
|
-
const buffers = new Array(blks.length)
|
|
397
|
-
|
|
398
|
-
for (let i = 0; i < blks.length; i++) {
|
|
399
|
-
const blk = blks[i]
|
|
462
|
+
blocks = Array.isArray(blocks) ? blocks : [blocks]
|
|
400
463
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
: this.valueEncoding
|
|
404
|
-
? c.encode(this.valueEncoding, blk)
|
|
405
|
-
: Buffer.from(blk)
|
|
464
|
+
const preappend = this.encryption && this._preappend
|
|
465
|
+
const buffers = new Array(blocks.length)
|
|
406
466
|
|
|
407
|
-
|
|
467
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
468
|
+
buffers[i] = this._encode(this.valueEncoding, blocks[i])
|
|
408
469
|
}
|
|
409
470
|
|
|
410
|
-
return await this.core.append(buffers, this.sign)
|
|
471
|
+
return await this.core.append(buffers, this.sign, { preappend })
|
|
411
472
|
}
|
|
412
473
|
|
|
413
474
|
registerExtension (name, handlers) {
|
|
@@ -418,14 +479,38 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
418
479
|
onextensionupdate () {
|
|
419
480
|
if (this.replicator !== null) this.replicator.broadcastOptions()
|
|
420
481
|
}
|
|
421
|
-
}
|
|
422
482
|
|
|
423
|
-
|
|
483
|
+
_encode (enc, val) {
|
|
484
|
+
const state = { start: this.padding, end: this.padding, buffer: null }
|
|
485
|
+
|
|
486
|
+
if (b4a.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 = b4a.from(val)
|
|
493
|
+
if (state.start === 0) return val
|
|
494
|
+
state.end += val.byteLength
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
state.buffer = b4a.allocUnsafe(state.end)
|
|
498
|
+
|
|
499
|
+
if (enc) enc.encode(state, val)
|
|
500
|
+
else state.buffer.set(val, state.start)
|
|
424
501
|
|
|
425
|
-
|
|
426
|
-
|
|
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
|
+
}
|
|
427
510
|
}
|
|
428
511
|
|
|
512
|
+
function noop () {}
|
|
513
|
+
|
|
429
514
|
function isStream (s) {
|
|
430
515
|
return typeof s === 'object' && s && typeof s.pipe === 'function'
|
|
431
516
|
}
|
|
@@ -439,5 +524,27 @@ function requireMaybe (name) {
|
|
|
439
524
|
}
|
|
440
525
|
|
|
441
526
|
function toHex (buf) {
|
|
442
|
-
return buf &&
|
|
527
|
+
return buf && b4a.toString(buf, 'hex')
|
|
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
|
+
}
|
|
443
550
|
}
|
package/lib/bitfield.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// TODO: needs massive improvements obvs
|
|
2
2
|
|
|
3
3
|
const BigSparseArray = require('big-sparse-array')
|
|
4
|
+
const b4a = require('b4a')
|
|
4
5
|
|
|
5
6
|
class FixedBitfield {
|
|
6
7
|
constructor (index, bitfield) {
|
|
@@ -116,9 +117,9 @@ module.exports = class Bitfield {
|
|
|
116
117
|
let error = null
|
|
117
118
|
|
|
118
119
|
for (const page of this.unflushed) {
|
|
119
|
-
const
|
|
120
|
+
const buf = b4a.from(page.bitfield.buffer, page.bitfield.byteOffset, page.bitfield.byteLength)
|
|
120
121
|
page.dirty = false
|
|
121
|
-
this.storage.write(page.index * 4096,
|
|
122
|
+
this.storage.write(page.index * 4096, buf, done)
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
function done (err) {
|
|
@@ -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,3 @@
|
|
|
1
|
-
const fsctl = requireMaybe('fsctl')
|
|
2
1
|
const hypercoreCrypto = require('hypercore-crypto')
|
|
3
2
|
const Oplog = require('./oplog')
|
|
4
3
|
const Mutex = require('./mutex')
|
|
@@ -7,8 +6,6 @@ const BlockStore = require('./block-store')
|
|
|
7
6
|
const Bitfield = require('./bitfield')
|
|
8
7
|
const { oplogHeader, oplogEntry } = require('./messages')
|
|
9
8
|
|
|
10
|
-
const lock = fsctl ? fsctl.lock : null
|
|
11
|
-
|
|
12
9
|
module.exports = class Core {
|
|
13
10
|
constructor (header, crypto, oplog, tree, blocks, bitfield, sign, onupdate) {
|
|
14
11
|
this.onupdate = onupdate
|
|
@@ -29,7 +26,7 @@ module.exports = class Core {
|
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
static async open (storage, opts = {}) {
|
|
32
|
-
const oplogFile = storage('oplog'
|
|
29
|
+
const oplogFile = storage('oplog')
|
|
33
30
|
const treeFile = storage('tree')
|
|
34
31
|
const bitfieldFile = storage('bitfield')
|
|
35
32
|
const dataFile = storage('data')
|
|
@@ -217,10 +214,12 @@ module.exports = class Core {
|
|
|
217
214
|
}
|
|
218
215
|
}
|
|
219
216
|
|
|
220
|
-
async append (values, sign = this.defaultSign) {
|
|
217
|
+
async append (values, sign = this.defaultSign, hooks = {}) {
|
|
221
218
|
await this._mutex.lock()
|
|
222
219
|
|
|
223
220
|
try {
|
|
221
|
+
if (hooks.preappend) await hooks.preappend(values)
|
|
222
|
+
|
|
224
223
|
if (!values.length) return this.tree.length
|
|
225
224
|
|
|
226
225
|
const batch = this.tree.batch()
|
|
@@ -467,11 +466,3 @@ function updateUserData (list, key, value) {
|
|
|
467
466
|
}
|
|
468
467
|
|
|
469
468
|
function noop () {}
|
|
470
|
-
|
|
471
|
-
function requireMaybe (name) {
|
|
472
|
-
try {
|
|
473
|
-
return require(name)
|
|
474
|
-
} catch {
|
|
475
|
-
return null
|
|
476
|
-
}
|
|
477
|
-
}
|
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) {
|
|
@@ -272,11 +273,15 @@ class ReorgBatch extends MerkleTreeBatch {
|
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
class ByteSeeker {
|
|
275
|
-
constructor (tree, bytes) {
|
|
276
|
+
constructor (tree, bytes, padding = 0) {
|
|
276
277
|
this.tree = tree
|
|
277
278
|
this.bytes = bytes
|
|
278
|
-
this.
|
|
279
|
-
|
|
279
|
+
this.padding = padding
|
|
280
|
+
|
|
281
|
+
const size = tree.byteLength - (tree.length * padding)
|
|
282
|
+
|
|
283
|
+
this.start = bytes >= size ? tree.length : 0
|
|
284
|
+
this.end = bytes < size ? tree.length : 0
|
|
280
285
|
}
|
|
281
286
|
|
|
282
287
|
nodes () {
|
|
@@ -287,12 +292,12 @@ class ByteSeeker {
|
|
|
287
292
|
if (!bytes) return [0, 0]
|
|
288
293
|
|
|
289
294
|
for (const node of this.tree.roots) { // all async ticks happen once we find the root so safe
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
295
|
+
let size = node.size
|
|
296
|
+
if (this.padding > 0) size -= this.padding * flat.countLeaves(node.index)
|
|
293
297
|
|
|
294
|
-
if (bytes
|
|
295
|
-
|
|
298
|
+
if (bytes === size) return [flat.rightSpan(node.index) + 2, 0]
|
|
299
|
+
if (bytes > size) {
|
|
300
|
+
bytes -= size
|
|
296
301
|
continue
|
|
297
302
|
}
|
|
298
303
|
|
|
@@ -301,9 +306,12 @@ class ByteSeeker {
|
|
|
301
306
|
while ((ite.index & 1) !== 0) {
|
|
302
307
|
const l = await this.tree.get(ite.leftChild(), false)
|
|
303
308
|
if (l) {
|
|
304
|
-
|
|
305
|
-
if (
|
|
306
|
-
|
|
309
|
+
let size = l.size
|
|
310
|
+
if (this.padding > 0) size -= this.padding * ite.countLeaves()
|
|
311
|
+
|
|
312
|
+
if (size === bytes) return [ite.rightSpan() + 2, 0]
|
|
313
|
+
if (size > bytes) continue
|
|
314
|
+
bytes -= size
|
|
307
315
|
ite.sibling()
|
|
308
316
|
} else {
|
|
309
317
|
ite.parent()
|
|
@@ -355,8 +363,8 @@ module.exports = class MerkleTree {
|
|
|
355
363
|
return new MerkleTreeBatch(this)
|
|
356
364
|
}
|
|
357
365
|
|
|
358
|
-
seek (bytes) {
|
|
359
|
-
return new ByteSeeker(this, bytes)
|
|
366
|
+
seek (bytes, padding) {
|
|
367
|
+
return new ByteSeeker(this, bytes, padding)
|
|
360
368
|
}
|
|
361
369
|
|
|
362
370
|
hash () {
|
|
@@ -433,17 +441,23 @@ module.exports = class MerkleTree {
|
|
|
433
441
|
// TODO: write neighbors together etc etc
|
|
434
442
|
// TODO: bench loading a full disk page and copy to that instead
|
|
435
443
|
return new Promise((resolve, reject) => {
|
|
436
|
-
const slab =
|
|
444
|
+
const slab = b4a.allocUnsafe(40 * this.flushing.size)
|
|
437
445
|
|
|
438
446
|
let error = null
|
|
439
447
|
let missing = this.flushing.size + 1
|
|
440
448
|
let offset = 0
|
|
441
449
|
|
|
442
450
|
for (const node of this.flushing.values()) {
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
451
|
+
const state = {
|
|
452
|
+
start: 0,
|
|
453
|
+
end: 40,
|
|
454
|
+
buffer: slab.subarray(offset, offset += 40)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
c.uint64.encode(state, node.size)
|
|
458
|
+
c.raw.encode(state, node.hash)
|
|
459
|
+
|
|
460
|
+
this.storage.write(node.index * 40, state.buffer, done)
|
|
447
461
|
}
|
|
448
462
|
|
|
449
463
|
done(null)
|
|
@@ -1038,10 +1052,10 @@ function getStoredNode (storage, index, error) {
|
|
|
1038
1052
|
return
|
|
1039
1053
|
}
|
|
1040
1054
|
|
|
1041
|
-
const hash = data.
|
|
1042
|
-
const size =
|
|
1055
|
+
const hash = data.subarray(8)
|
|
1056
|
+
const size = c.decode(c.uint64, data)
|
|
1043
1057
|
|
|
1044
|
-
if (size === 0 &&
|
|
1058
|
+
if (size === 0 && b4a.compare(hash, BLANK_HASH) === 0) {
|
|
1045
1059
|
if (error) reject(new Error('Could not load node: ' + index))
|
|
1046
1060
|
else resolve(null)
|
|
1047
1061
|
return
|
|
@@ -1088,9 +1102,9 @@ function log2 (n) {
|
|
|
1088
1102
|
}
|
|
1089
1103
|
|
|
1090
1104
|
function signable (hash, length, fork) {
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
return
|
|
1105
|
+
const state = { start: 0, end: 48, buffer: b4a.alloc(48) }
|
|
1106
|
+
c.raw.encode(state, hash)
|
|
1107
|
+
c.uint64.encode(state, length)
|
|
1108
|
+
c.uint64.encode(state, fork)
|
|
1109
|
+
return state.buffer
|
|
1096
1110
|
}
|
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
|