hypercore 10.4.1 → 10.5.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/README.md +22 -4
- package/index.js +34 -12
- package/lib/core.js +24 -2
- package/lib/errors.js +20 -0
- package/lib/info.js +32 -2
- package/lib/merkle-tree.js +35 -13
- package/lib/messages.js +2 -1
- package/lib/oplog.js +2 -1
- package/lib/replicator.js +70 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ const blockIfFast = await core.get(43, { timeout: 5000 })
|
|
|
107
107
|
const blockLocal = await core.get(44, { wait: false })
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
`options` include:
|
|
111
111
|
|
|
112
112
|
``` js
|
|
113
113
|
{
|
|
@@ -118,6 +118,10 @@ Additional options include
|
|
|
118
118
|
}
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
#### `const has = await core.has(start, [end])`
|
|
122
|
+
|
|
123
|
+
Check if the core has all blocks between `start` and `end`.
|
|
124
|
+
|
|
121
125
|
#### `const updated = await core.update()`
|
|
122
126
|
|
|
123
127
|
Wait for the core to try and find a signed update to it's length.
|
|
@@ -163,7 +167,7 @@ for await (const data of fullStream) {
|
|
|
163
167
|
}
|
|
164
168
|
```
|
|
165
169
|
|
|
166
|
-
|
|
170
|
+
`options` include:
|
|
167
171
|
|
|
168
172
|
``` js
|
|
169
173
|
{
|
|
@@ -238,7 +242,7 @@ To cancel downloading a range simply destroy the range instance.
|
|
|
238
242
|
range.destroy()
|
|
239
243
|
```
|
|
240
244
|
|
|
241
|
-
#### `const info = await core.info()`
|
|
245
|
+
#### `const info = await core.info([options])`
|
|
242
246
|
|
|
243
247
|
Get information about this core, such as its total size in bytes.
|
|
244
248
|
|
|
@@ -252,7 +256,21 @@ Info {
|
|
|
252
256
|
contiguousLength: 16,
|
|
253
257
|
byteLength: 742,
|
|
254
258
|
fork: 0,
|
|
255
|
-
padding: 8
|
|
259
|
+
padding: 8,
|
|
260
|
+
storage: {
|
|
261
|
+
oplog: 8192,
|
|
262
|
+
tree: 4096,
|
|
263
|
+
blocks: 4096,
|
|
264
|
+
bitfield: 4096
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`options` include:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
{
|
|
273
|
+
storage: false // get storage estimates in bytes, disabled by default
|
|
256
274
|
}
|
|
257
275
|
```
|
|
258
276
|
|
package/index.js
CHANGED
|
@@ -324,7 +324,8 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
324
324
|
crypto: this.crypto,
|
|
325
325
|
legacy: opts.legacy,
|
|
326
326
|
auth: opts.auth,
|
|
327
|
-
onupdate: this._oncoreupdate.bind(this)
|
|
327
|
+
onupdate: this._oncoreupdate.bind(this),
|
|
328
|
+
onconflict: this._oncoreconflict.bind(this)
|
|
328
329
|
})
|
|
329
330
|
|
|
330
331
|
if (opts.userData) {
|
|
@@ -377,13 +378,13 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
377
378
|
return prev.length !== next.length || prev.fork !== next.fork
|
|
378
379
|
}
|
|
379
380
|
|
|
380
|
-
close () {
|
|
381
|
+
close (err) {
|
|
381
382
|
if (this.closing) return this.closing
|
|
382
|
-
this.closing = this._close()
|
|
383
|
+
this.closing = this._close(err || null)
|
|
383
384
|
return this.closing
|
|
384
385
|
}
|
|
385
386
|
|
|
386
|
-
async _close () {
|
|
387
|
+
async _close (err) {
|
|
387
388
|
await this.opening
|
|
388
389
|
|
|
389
390
|
const i = this.sessions.indexOf(this)
|
|
@@ -403,19 +404,23 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
403
404
|
|
|
404
405
|
if (this.replicator !== null) {
|
|
405
406
|
this.replicator.findingPeers -= this._findingPeers
|
|
406
|
-
this.replicator.clearRequests(this.activeRequests)
|
|
407
|
+
this.replicator.clearRequests(this.activeRequests, err)
|
|
407
408
|
}
|
|
408
409
|
|
|
409
410
|
this._findingPeers = 0
|
|
410
411
|
|
|
411
412
|
if (this.sessions.length) {
|
|
412
413
|
// if this is the last session and we are auto closing, trigger that first to enforce error handling
|
|
413
|
-
if (this.sessions.length === 1 && this.autoClose) await this.sessions[0].close()
|
|
414
|
+
if (this.sessions.length === 1 && this.autoClose) await this.sessions[0].close(err)
|
|
414
415
|
// emit "fake" close as this is a session
|
|
415
416
|
this.emit('close', false)
|
|
416
417
|
return
|
|
417
418
|
}
|
|
418
419
|
|
|
420
|
+
if (this.replicator !== null) {
|
|
421
|
+
this.replicator.destroy()
|
|
422
|
+
}
|
|
423
|
+
|
|
419
424
|
await this.core.close()
|
|
420
425
|
|
|
421
426
|
this.emit('close', true)
|
|
@@ -510,6 +515,18 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
510
515
|
}
|
|
511
516
|
}
|
|
512
517
|
|
|
518
|
+
async _oncoreconflict (proof, from) {
|
|
519
|
+
await this.replicator.onconflict(from)
|
|
520
|
+
|
|
521
|
+
for (const s of this.sessions) s.emit('conflict', proof.upgrade.length, proof.fork, proof)
|
|
522
|
+
|
|
523
|
+
const err = new Error('Two conflicting signatures exist for length ' + proof.upgrade.length)
|
|
524
|
+
|
|
525
|
+
const all = []
|
|
526
|
+
for (const s of this.sessions) all.push(s.close(err))
|
|
527
|
+
await Promise.allSettled(all)
|
|
528
|
+
}
|
|
529
|
+
|
|
513
530
|
_oncoreupdate (status, bitfield, value, from) {
|
|
514
531
|
if (status !== 0) {
|
|
515
532
|
const truncatedNonSparse = (status & 0b1000) !== 0
|
|
@@ -614,10 +631,10 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
614
631
|
}
|
|
615
632
|
}
|
|
616
633
|
|
|
617
|
-
async info () {
|
|
634
|
+
async info (opts) {
|
|
618
635
|
if (this.opened === false) await this.opening
|
|
619
636
|
|
|
620
|
-
return Info.from(this)
|
|
637
|
+
return Info.from(this, opts)
|
|
621
638
|
}
|
|
622
639
|
|
|
623
640
|
async update (opts) {
|
|
@@ -666,10 +683,15 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
666
683
|
return req.promise
|
|
667
684
|
}
|
|
668
685
|
|
|
669
|
-
async has (
|
|
686
|
+
async has (start, end = start + 1) {
|
|
670
687
|
if (this.opened === false) await this.opening
|
|
671
688
|
|
|
672
|
-
|
|
689
|
+
const length = end - start
|
|
690
|
+
if (length <= 0) return false
|
|
691
|
+
if (length === 1) return this.core.bitfield.get(start)
|
|
692
|
+
|
|
693
|
+
const i = this.core.bitfield.firstUnset(start)
|
|
694
|
+
return i === -1 || i >= end
|
|
673
695
|
}
|
|
674
696
|
|
|
675
697
|
async get (index, opts) {
|
|
@@ -804,13 +826,13 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
804
826
|
}
|
|
805
827
|
}
|
|
806
828
|
|
|
807
|
-
return
|
|
829
|
+
return this.core.append(buffers, this.auth, { preappend })
|
|
808
830
|
}
|
|
809
831
|
|
|
810
832
|
async treeHash (length) {
|
|
811
833
|
if (length === undefined) {
|
|
812
834
|
await this.ready()
|
|
813
|
-
length = this.core.length
|
|
835
|
+
length = this.core.tree.length
|
|
814
836
|
}
|
|
815
837
|
|
|
816
838
|
const roots = await this.core.tree.getRoots(length)
|
package/lib/core.js
CHANGED
|
@@ -9,8 +9,9 @@ const { BAD_ARGUMENT, STORAGE_EMPTY, STORAGE_CONFLICT, INVALID_SIGNATURE } = req
|
|
|
9
9
|
const m = require('./messages')
|
|
10
10
|
|
|
11
11
|
module.exports = class Core {
|
|
12
|
-
constructor (header, crypto, oplog, tree, blocks, bitfield, auth, legacy, onupdate) {
|
|
12
|
+
constructor (header, crypto, oplog, tree, blocks, bitfield, auth, legacy, onupdate, onconflict) {
|
|
13
13
|
this.onupdate = onupdate
|
|
14
|
+
this.onconflict = onconflict
|
|
14
15
|
this.header = header
|
|
15
16
|
this.crypto = crypto
|
|
16
17
|
this.oplog = oplog
|
|
@@ -158,7 +159,7 @@ module.exports = class Core {
|
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
return new this(header, crypto, oplog, tree, blocks, bitfield, auth, !!opts.legacy, opts.onupdate || noop)
|
|
162
|
+
return new this(header, crypto, oplog, tree, blocks, bitfield, auth, !!opts.legacy, opts.onupdate || noop, opts.onconflict || noop)
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
_shouldFlush () {
|
|
@@ -437,6 +438,27 @@ module.exports = class Core {
|
|
|
437
438
|
return verifies[0] !== null
|
|
438
439
|
}
|
|
439
440
|
|
|
441
|
+
async checkConflict (proof, from) {
|
|
442
|
+
if (this.tree.length < proof.upgrade.length || proof.fork !== this.tree.fork) {
|
|
443
|
+
// out of date this proof - ignore for now
|
|
444
|
+
return false
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const batch = this.tree.verifyFullyRemote(proof)
|
|
448
|
+
|
|
449
|
+
if (!batch.signature || !this._signed(batch, batch.hash())) {
|
|
450
|
+
throw INVALID_SIGNATURE('Proof contains an invalid signature with no input from us')
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const remoteTreeHash = this.crypto.tree(proof.upgrade.nodes)
|
|
454
|
+
const localTreeHash = this.crypto.tree(await this.tree.getRoots(proof.upgrade.length))
|
|
455
|
+
|
|
456
|
+
if (b4a.equals(localTreeHash, remoteTreeHash)) return false
|
|
457
|
+
|
|
458
|
+
await this.onconflict(proof)
|
|
459
|
+
return true
|
|
460
|
+
}
|
|
461
|
+
|
|
440
462
|
async verify (proof, from) {
|
|
441
463
|
// We cannot apply "other forks" atm.
|
|
442
464
|
// We should probably still try and they are likely super similar for non upgrades
|
package/lib/errors.js
CHANGED
|
@@ -32,6 +32,18 @@ module.exports = class HypercoreError extends Error {
|
|
|
32
32
|
return new HypercoreError(msg, 'INVALID_CAPABILITY', HypercoreError.INVALID_CAPABILITY)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
static INVALID_CHECKSUM (msg = 'Invalid checksum') {
|
|
36
|
+
return new HypercoreError(msg, 'INVALID_CHECKSUM', HypercoreError.INVALID_CHECKSUM)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static INVALID_OPERATION (msg) {
|
|
40
|
+
return new HypercoreError(msg, 'INVALID_OPERATION', HypercoreError.INVALID_OPERATION)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static INVALID_PROOF (msg = 'Proof not verifiable') {
|
|
44
|
+
return new HypercoreError(msg, 'INVALID_PROOF', HypercoreError.INVALID_PROOF)
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
static SNAPSHOT_NOT_AVAILABLE (msg = 'Snapshot is not available') {
|
|
36
48
|
return new HypercoreError(msg, 'SNAPSHOT_NOT_AVAILABLE', HypercoreError.SNAPSHOT_NOT_AVAILABLE)
|
|
37
49
|
}
|
|
@@ -47,4 +59,12 @@ module.exports = class HypercoreError extends Error {
|
|
|
47
59
|
static SESSION_CLOSED (msg = 'Session is closed') {
|
|
48
60
|
return new HypercoreError(msg, 'SESSION_CLOSED', HypercoreError.SESSION_CLOSED)
|
|
49
61
|
}
|
|
62
|
+
|
|
63
|
+
static OPLOG_CORRUPT (msg = 'Oplog file appears corrupt or out of date') {
|
|
64
|
+
return new HypercoreError(msg, 'OPLOG_CORRUPT', HypercoreError.OPLOG_CORRUPT)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static INVALID_OPLOG_VERSION (msg = 'Invalid header version') {
|
|
68
|
+
return new HypercoreError(msg, 'INVALID_OPLOG_VERSION', HypercoreError.INVALID_OPLOG_VERSION)
|
|
69
|
+
}
|
|
50
70
|
}
|
package/lib/info.js
CHANGED
|
@@ -7,9 +7,10 @@ module.exports = class Info {
|
|
|
7
7
|
this.byteLength = opts.byteLength || 0
|
|
8
8
|
this.fork = opts.fork || 0
|
|
9
9
|
this.padding = opts.padding || 0
|
|
10
|
+
this.storage = opts.storage || null
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
static async from (session) {
|
|
13
|
+
static async from (session, opts = {}) {
|
|
13
14
|
return new Info({
|
|
14
15
|
key: session.key,
|
|
15
16
|
discoveryKey: session.discoveryKey,
|
|
@@ -17,7 +18,36 @@ module.exports = class Info {
|
|
|
17
18
|
contiguousLength: session.contiguousLength,
|
|
18
19
|
byteLength: session.byteLength,
|
|
19
20
|
fork: session.fork,
|
|
20
|
-
padding: session.padding
|
|
21
|
+
padding: session.padding,
|
|
22
|
+
storage: opts.storage ? await this.storage(session) : null
|
|
21
23
|
})
|
|
22
24
|
}
|
|
25
|
+
|
|
26
|
+
static async storage (session) {
|
|
27
|
+
const { oplog, tree, blocks, bitfield } = session.core
|
|
28
|
+
try {
|
|
29
|
+
return {
|
|
30
|
+
oplog: await bytesUsed(oplog.storage),
|
|
31
|
+
tree: await bytesUsed(tree.storage),
|
|
32
|
+
blocks: await bytesUsed(blocks.storage),
|
|
33
|
+
bitfield: await bytesUsed(bitfield.storage)
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function bytesUsed (file) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
file.stat((err, st) => {
|
|
42
|
+
if (err) {
|
|
43
|
+
resolve(0) // prob just file not found (TODO, improve)
|
|
44
|
+
} else if (typeof st.blocks !== 'number') {
|
|
45
|
+
reject(new Error('cannot determine bytes used'))
|
|
46
|
+
} else {
|
|
47
|
+
resolve(st.blocks * 512)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
23
53
|
}
|
package/lib/merkle-tree.js
CHANGED
|
@@ -4,6 +4,7 @@ const c = require('compact-encoding')
|
|
|
4
4
|
const Xache = require('xache')
|
|
5
5
|
const b4a = require('b4a')
|
|
6
6
|
const caps = require('./caps')
|
|
7
|
+
const { INVALID_PROOF, INVALID_CHECKSUM, INVALID_OPERATION, BAD_ARGUMENT } = require('./errors')
|
|
7
8
|
|
|
8
9
|
const BLANK_HASH = b4a.alloc(32)
|
|
9
10
|
const OLD_TREE = b4a.from([5, 2, 87, 2, 0, 0, 40, 7, 66, 76, 65, 75, 69, 50, 98])
|
|
@@ -26,12 +27,12 @@ class NodeQueue {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
if (this.i >= this.nodes.length) {
|
|
29
|
-
throw
|
|
30
|
+
throw INVALID_OPERATION('Expected node ' + index + ', got (nil)')
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const node = this.nodes[this.i++]
|
|
33
34
|
if (node.index !== index) {
|
|
34
|
-
throw
|
|
35
|
+
throw INVALID_OPERATION('Expected node ' + index + ', got node ' + node.index)
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
this.length--
|
|
@@ -109,7 +110,7 @@ class MerkleTreeBatch {
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
commit () {
|
|
112
|
-
if (!this.commitable()) throw
|
|
113
|
+
if (!this.commitable()) throw INVALID_OPERATION('Tree was modified during batch, refusing to commit')
|
|
113
114
|
|
|
114
115
|
if (this.upgraded) this._commitUpgrade()
|
|
115
116
|
|
|
@@ -423,7 +424,7 @@ module.exports = class MerkleTree {
|
|
|
423
424
|
|
|
424
425
|
if (node !== undefined) {
|
|
425
426
|
if (node.hash === BLANK_HASH) {
|
|
426
|
-
if (error) throw
|
|
427
|
+
if (error) throw INVALID_OPERATION('Could not load node: ' + index)
|
|
427
428
|
return Promise.resolve(null)
|
|
428
429
|
}
|
|
429
430
|
return Promise.resolve(node)
|
|
@@ -560,7 +561,7 @@ module.exports = class MerkleTree {
|
|
|
560
561
|
}
|
|
561
562
|
|
|
562
563
|
if (!verifyUpgrade(proof, unverified, batch)) {
|
|
563
|
-
throw
|
|
564
|
+
throw INVALID_PROOF('Fork proof not verifiable')
|
|
564
565
|
}
|
|
565
566
|
|
|
566
567
|
for (const root of batch.roots) {
|
|
@@ -580,6 +581,27 @@ module.exports = class MerkleTree {
|
|
|
580
581
|
return batch
|
|
581
582
|
}
|
|
582
583
|
|
|
584
|
+
verifyFullyRemote (proof) {
|
|
585
|
+
// TODO: impl this less hackishly
|
|
586
|
+
const batch = new MerkleTreeBatch(this)
|
|
587
|
+
|
|
588
|
+
batch.fork = proof.fork
|
|
589
|
+
batch.roots = []
|
|
590
|
+
batch.length = 0
|
|
591
|
+
batch.ancestors = 0
|
|
592
|
+
batch.byteLength = 0
|
|
593
|
+
|
|
594
|
+
let unverified = verifyTree(proof, this.crypto, batch.nodes)
|
|
595
|
+
|
|
596
|
+
if (proof.upgrade) {
|
|
597
|
+
if (verifyUpgrade(proof, unverified, batch)) {
|
|
598
|
+
unverified = null
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return batch
|
|
603
|
+
}
|
|
604
|
+
|
|
583
605
|
async verify (proof) {
|
|
584
606
|
const batch = new MerkleTreeBatch(this)
|
|
585
607
|
|
|
@@ -594,7 +616,7 @@ module.exports = class MerkleTree {
|
|
|
594
616
|
if (unverified) {
|
|
595
617
|
const verified = await this.get(unverified.index)
|
|
596
618
|
if (!b4a.equals(verified.hash, unverified.hash)) {
|
|
597
|
-
throw
|
|
619
|
+
throw INVALID_CHECKSUM('Invalid checksum at node ' + unverified.index)
|
|
598
620
|
}
|
|
599
621
|
}
|
|
600
622
|
|
|
@@ -613,10 +635,10 @@ module.exports = class MerkleTree {
|
|
|
613
635
|
const node = normalizeIndexed(block, hash)
|
|
614
636
|
|
|
615
637
|
if (from >= to || to > head) {
|
|
616
|
-
throw
|
|
638
|
+
throw INVALID_OPERATION('Invalid upgrade')
|
|
617
639
|
}
|
|
618
640
|
if (seek && upgrade && node !== null && node.index >= from) {
|
|
619
|
-
throw
|
|
641
|
+
throw INVALID_OPERATION('Cannot both do a seek and block/hash request when upgrading')
|
|
620
642
|
}
|
|
621
643
|
|
|
622
644
|
let subTree = head
|
|
@@ -713,7 +735,7 @@ module.exports = class MerkleTree {
|
|
|
713
735
|
async byteRange (index) {
|
|
714
736
|
const head = 2 * this.length
|
|
715
737
|
if (((index & 1) === 0 ? index : flat.rightSpan(index)) >= head) {
|
|
716
|
-
throw
|
|
738
|
+
throw BAD_ARGUMENT('Index is out of bounds')
|
|
717
739
|
}
|
|
718
740
|
return [await this.byteOffset(index), (await this.get(index)).size]
|
|
719
741
|
}
|
|
@@ -862,7 +884,7 @@ function verifyUpgrade ({ fork, upgrade }, blockRoot, batch) {
|
|
|
862
884
|
const node = extra[i++]
|
|
863
885
|
|
|
864
886
|
while (node.index !== ite.index) {
|
|
865
|
-
if (ite.factor === 2) throw
|
|
887
|
+
if (ite.factor === 2) throw INVALID_OPERATION('Unexpected node: ' + node.index)
|
|
866
888
|
ite.leftChild()
|
|
867
889
|
}
|
|
868
890
|
|
|
@@ -923,14 +945,14 @@ async function seekTrustedTree (tree, root, bytes) {
|
|
|
923
945
|
async function seekUntrustedTree (tree, root, bytes) {
|
|
924
946
|
const offset = await tree.byteOffset(root)
|
|
925
947
|
|
|
926
|
-
if (offset > bytes) throw
|
|
948
|
+
if (offset > bytes) throw INVALID_OPERATION('Invalid seek')
|
|
927
949
|
if (offset === bytes) return root
|
|
928
950
|
|
|
929
951
|
bytes -= offset
|
|
930
952
|
|
|
931
953
|
const node = await tree.get(root)
|
|
932
954
|
|
|
933
|
-
if (node.size <= bytes) throw
|
|
955
|
+
if (node.size <= bytes) throw INVALID_OPERATION('Invalid seek')
|
|
934
956
|
|
|
935
957
|
return seekTrustedTree(tree, root, bytes)
|
|
936
958
|
}
|
|
@@ -1061,7 +1083,7 @@ function nodesToRoot (index, nodes, head) {
|
|
|
1061
1083
|
|
|
1062
1084
|
for (let i = 0; i < nodes; i++) {
|
|
1063
1085
|
ite.parent()
|
|
1064
|
-
if (ite.contains(head)) throw
|
|
1086
|
+
if (ite.contains(head)) throw BAD_ARGUMENT('Nodes is out of bounds')
|
|
1065
1087
|
}
|
|
1066
1088
|
|
|
1067
1089
|
return ite.index
|
package/lib/messages.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const c = require('compact-encoding')
|
|
2
2
|
const b4a = require('b4a')
|
|
3
|
+
const { INVALID_OPLOG_VERSION } = require('./errors')
|
|
3
4
|
|
|
4
5
|
const EMPTY = b4a.alloc(0)
|
|
5
6
|
|
|
@@ -637,7 +638,7 @@ oplog.header = {
|
|
|
637
638
|
const version = c.uint.decode(state)
|
|
638
639
|
|
|
639
640
|
if (version !== 0) {
|
|
640
|
-
throw
|
|
641
|
+
throw INVALID_OPLOG_VERSION('Invalid header version. Expected 0, got ' + version)
|
|
641
642
|
}
|
|
642
643
|
|
|
643
644
|
return {
|
package/lib/oplog.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const cenc = require('compact-encoding')
|
|
2
2
|
const b4a = require('b4a')
|
|
3
3
|
const { crc32 } = require('crc-universal')
|
|
4
|
+
const { OPLOG_CORRUPT } = require('./errors')
|
|
4
5
|
|
|
5
6
|
module.exports = class Oplog {
|
|
6
7
|
constructor (storage, { pageSize = 4096, headerEncoding = cenc.raw, entryEncoding = cenc.raw } = {}) {
|
|
@@ -77,7 +78,7 @@ module.exports = class Oplog {
|
|
|
77
78
|
this._headers[1] = 0
|
|
78
79
|
|
|
79
80
|
if (buffer.byteLength >= this._entryOffset) {
|
|
80
|
-
throw
|
|
81
|
+
throw OPLOG_CORRUPT()
|
|
81
82
|
}
|
|
82
83
|
return result
|
|
83
84
|
}
|
package/lib/replicator.js
CHANGED
|
@@ -250,6 +250,8 @@ class Peer {
|
|
|
250
250
|
this.protomux = protomux
|
|
251
251
|
this.remotePublicKey = this.stream.remotePublicKey
|
|
252
252
|
|
|
253
|
+
this.paused = false
|
|
254
|
+
|
|
253
255
|
this.session = session
|
|
254
256
|
|
|
255
257
|
this.channel = channel
|
|
@@ -518,7 +520,32 @@ class Peer {
|
|
|
518
520
|
})
|
|
519
521
|
}
|
|
520
522
|
|
|
523
|
+
_checkIfConflict (err) {
|
|
524
|
+
this.paused = true
|
|
525
|
+
|
|
526
|
+
const length = Math.min(this.core.tree.length, this.remoteLength)
|
|
527
|
+
if (length === 0) throw err
|
|
528
|
+
|
|
529
|
+
this.wireRequest.send({
|
|
530
|
+
id: 0, // TODO: use an more explicit id for this eventually...
|
|
531
|
+
fork: this.remoteFork,
|
|
532
|
+
block: null,
|
|
533
|
+
hash: null,
|
|
534
|
+
seek: null,
|
|
535
|
+
upgrade: {
|
|
536
|
+
start: 0,
|
|
537
|
+
length
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
521
542
|
async ondata (data) {
|
|
543
|
+
// always allow a fork conflict proof to be sent
|
|
544
|
+
if (data.request === 0 && data.upgrade && data.upgrade.start === 0) {
|
|
545
|
+
if (await this.core.checkConflict(data, this)) return
|
|
546
|
+
this.paused = false
|
|
547
|
+
}
|
|
548
|
+
|
|
522
549
|
const req = data.request > 0 ? this.replicator._inflight.get(data.request) : null
|
|
523
550
|
const reorg = data.fork > this.core.tree.fork
|
|
524
551
|
|
|
@@ -542,8 +569,11 @@ class Peer {
|
|
|
542
569
|
return
|
|
543
570
|
}
|
|
544
571
|
} catch (err) {
|
|
572
|
+
safetyCatch(err)
|
|
573
|
+
// might be a fork, verify
|
|
574
|
+
this._checkIfConflict(err)
|
|
545
575
|
this.replicator._onnodata(this, req)
|
|
546
|
-
|
|
576
|
+
return
|
|
547
577
|
} finally {
|
|
548
578
|
this.dataProcessing--
|
|
549
579
|
}
|
|
@@ -597,6 +627,25 @@ class Peer {
|
|
|
597
627
|
this.replicator.updatePeer(this)
|
|
598
628
|
}
|
|
599
629
|
|
|
630
|
+
async _onconflict () {
|
|
631
|
+
this.protomux.cork()
|
|
632
|
+
if (this.remoteLength > 0 && this.core.tree.fork === this.remoteFork) {
|
|
633
|
+
await this.onrequest({
|
|
634
|
+
id: 0,
|
|
635
|
+
fork: this.core.tree.fork,
|
|
636
|
+
block: null,
|
|
637
|
+
hash: null,
|
|
638
|
+
seek: null,
|
|
639
|
+
upgrade: {
|
|
640
|
+
start: 0,
|
|
641
|
+
length: Math.min(this.core.tree.length, this.remoteLength)
|
|
642
|
+
}
|
|
643
|
+
})
|
|
644
|
+
}
|
|
645
|
+
this.channel.close()
|
|
646
|
+
this.protomux.uncork()
|
|
647
|
+
}
|
|
648
|
+
|
|
600
649
|
_makeRequest (needsUpgrade) {
|
|
601
650
|
if (needsUpgrade === true && this.replicator._shouldUpgrade(this) === false) {
|
|
602
651
|
return null
|
|
@@ -901,6 +950,15 @@ module.exports = class Replicator {
|
|
|
901
950
|
if (this._ranges.length !== 0 || this._seeks.length !== 0) this._updateNonPrimary()
|
|
902
951
|
}
|
|
903
952
|
|
|
953
|
+
// Called externally when a conflict has been detected and verified
|
|
954
|
+
async onconflict (from) {
|
|
955
|
+
const all = []
|
|
956
|
+
for (const peer of this.peers) {
|
|
957
|
+
all.push(peer._onconflict())
|
|
958
|
+
}
|
|
959
|
+
await Promise.allSettled(all)
|
|
960
|
+
}
|
|
961
|
+
|
|
904
962
|
addUpgrade (session) {
|
|
905
963
|
if (this._upgrade !== null) {
|
|
906
964
|
const ref = this._upgrade.attach(session)
|
|
@@ -973,10 +1031,10 @@ module.exports = class Replicator {
|
|
|
973
1031
|
ref.context.detach(ref, null)
|
|
974
1032
|
}
|
|
975
1033
|
|
|
976
|
-
clearRequests (session) {
|
|
1034
|
+
clearRequests (session, err = null) {
|
|
977
1035
|
while (session.length > 0) {
|
|
978
1036
|
const ref = session[session.length - 1]
|
|
979
|
-
ref.context.detach(ref,
|
|
1037
|
+
ref.context.detach(ref, err)
|
|
980
1038
|
}
|
|
981
1039
|
|
|
982
1040
|
this.updateAll()
|
|
@@ -1418,7 +1476,7 @@ module.exports = class Replicator {
|
|
|
1418
1476
|
}
|
|
1419
1477
|
|
|
1420
1478
|
_updatePeer (peer) {
|
|
1421
|
-
if (peer.inflight >= peer.maxInflight) {
|
|
1479
|
+
if (peer.paused || peer.inflight >= peer.maxInflight) {
|
|
1422
1480
|
return false
|
|
1423
1481
|
}
|
|
1424
1482
|
|
|
@@ -1444,7 +1502,7 @@ module.exports = class Replicator {
|
|
|
1444
1502
|
}
|
|
1445
1503
|
|
|
1446
1504
|
_updatePeerNonPrimary (peer) {
|
|
1447
|
-
if (peer.inflight >= peer.maxInflight) {
|
|
1505
|
+
if (peer.paused || peer.inflight >= peer.maxInflight) {
|
|
1448
1506
|
return false
|
|
1449
1507
|
}
|
|
1450
1508
|
|
|
@@ -1523,6 +1581,13 @@ module.exports = class Replicator {
|
|
|
1523
1581
|
})
|
|
1524
1582
|
}
|
|
1525
1583
|
|
|
1584
|
+
destroy () {
|
|
1585
|
+
for (const peer of this.peers) {
|
|
1586
|
+
peer.protomux.unpair({ protocol: 'hypercore/alpha', id: this.discoveryKey })
|
|
1587
|
+
peer.channel.close()
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1526
1591
|
_makePeer (protomux, session) {
|
|
1527
1592
|
if (protomux.opened({ protocol: 'hypercore/alpha', id: this.discoveryKey })) return onnochannel()
|
|
1528
1593
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypercore",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.5.1",
|
|
4
4
|
"description": "Hypercore is a secure, distributed append-only log",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@hyperswarm/secret-stream": "^6.0.0",
|
|
37
37
|
"b4a": "^1.1.0",
|
|
38
|
-
"big-sparse-array": "^1.0.
|
|
38
|
+
"big-sparse-array": "^1.0.3",
|
|
39
39
|
"compact-encoding": "^2.11.0",
|
|
40
40
|
"crc-universal": "^1.0.2",
|
|
41
41
|
"events": "^3.3.0",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"brittle": "^3.0.0",
|
|
57
57
|
"hyperswarm": "^4.3.0",
|
|
58
|
-
"random-access-memory": "^6.
|
|
58
|
+
"random-access-memory": "^6.1.0",
|
|
59
59
|
"random-access-memory-overlay": "^3.0.0",
|
|
60
60
|
"standard": "^17.0.0",
|
|
61
61
|
"tmp-promise": "^3.0.2"
|