hypercore 9.12.0 → 10.0.0-alpha.11

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.
Files changed (86) hide show
  1. package/.github/workflows/test-node.yml +3 -4
  2. package/README.md +131 -404
  3. package/__snapshots__/test/storage.js.snapshot.cjs +15 -0
  4. package/examples/announce.js +19 -0
  5. package/examples/basic.js +10 -0
  6. package/examples/http.js +123 -0
  7. package/examples/lookup.js +20 -0
  8. package/index.js +365 -1600
  9. package/lib/bitfield.js +113 -285
  10. package/lib/block-encryption.js +68 -0
  11. package/lib/block-store.js +58 -0
  12. package/lib/core.js +468 -0
  13. package/lib/extensions.js +76 -0
  14. package/lib/merkle-tree.js +1110 -0
  15. package/lib/messages.js +571 -0
  16. package/lib/mutex.js +39 -0
  17. package/lib/oplog.js +224 -0
  18. package/lib/protocol.js +525 -0
  19. package/lib/random-iterator.js +46 -0
  20. package/lib/remote-bitfield.js +24 -0
  21. package/lib/replicator.js +857 -0
  22. package/lib/streams.js +39 -0
  23. package/package.json +44 -45
  24. package/test/basic.js +59 -471
  25. package/test/bitfield.js +48 -133
  26. package/test/core.js +290 -0
  27. package/test/encodings.js +18 -0
  28. package/test/encryption.js +123 -0
  29. package/test/extension.js +71 -0
  30. package/test/helpers/index.js +23 -0
  31. package/test/merkle-tree.js +518 -0
  32. package/test/mutex.js +137 -0
  33. package/test/oplog.js +399 -0
  34. package/test/preload.js +72 -0
  35. package/test/replicate.js +227 -824
  36. package/test/sessions.js +173 -0
  37. package/test/storage.js +31 -0
  38. package/test/streams.js +39 -146
  39. package/test/user-data.js +47 -0
  40. package/bench/all.sh +0 -65
  41. package/bench/copy-64kb-blocks.js +0 -51
  42. package/bench/helpers/read-throttled.js +0 -27
  43. package/bench/helpers/read.js +0 -47
  44. package/bench/helpers/write.js +0 -29
  45. package/bench/read-16kb-blocks-proof-throttled.js +0 -1
  46. package/bench/read-16kb-blocks-proof.js +0 -1
  47. package/bench/read-16kb-blocks-throttled.js +0 -1
  48. package/bench/read-16kb-blocks.js +0 -1
  49. package/bench/read-512b-blocks.js +0 -1
  50. package/bench/read-64kb-blocks-linear-batch.js +0 -18
  51. package/bench/read-64kb-blocks-linear.js +0 -18
  52. package/bench/read-64kb-blocks-proof.js +0 -1
  53. package/bench/read-64kb-blocks.js +0 -1
  54. package/bench/replicate-16kb-blocks.js +0 -19
  55. package/bench/replicate-64kb-blocks.js +0 -19
  56. package/bench/write-16kb-blocks.js +0 -1
  57. package/bench/write-512b-blocks.js +0 -1
  58. package/bench/write-64kb-blocks-static.js +0 -1
  59. package/bench/write-64kb-blocks.js +0 -1
  60. package/example.js +0 -23
  61. package/lib/cache.js +0 -26
  62. package/lib/crypto.js +0 -5
  63. package/lib/replicate.js +0 -829
  64. package/lib/safe-buffer-equals.js +0 -6
  65. package/lib/storage.js +0 -421
  66. package/lib/tree-index.js +0 -183
  67. package/test/ack.js +0 -306
  68. package/test/audit.js +0 -36
  69. package/test/cache.js +0 -93
  70. package/test/compat.js +0 -209
  71. package/test/copy.js +0 -377
  72. package/test/default-storage.js +0 -51
  73. package/test/extensions.js +0 -137
  74. package/test/get.js +0 -64
  75. package/test/head.js +0 -65
  76. package/test/helpers/create-tracking-ram.js +0 -27
  77. package/test/helpers/create.js +0 -6
  78. package/test/helpers/replicate.js +0 -4
  79. package/test/seek.js +0 -234
  80. package/test/selections.js +0 -95
  81. package/test/set-uploading-downloading.js +0 -91
  82. package/test/stats.js +0 -77
  83. package/test/timeouts.js +0 -22
  84. package/test/tree-index.js +0 -841
  85. package/test/update.js +0 -156
  86. package/test/value-encoding.js +0 -52
package/lib/core.js ADDED
@@ -0,0 +1,468 @@
1
+ const hypercoreCrypto = require('hypercore-crypto')
2
+ const Oplog = require('./oplog')
3
+ const Mutex = require('./mutex')
4
+ const MerkleTree = require('./merkle-tree')
5
+ const BlockStore = require('./block-store')
6
+ const Bitfield = require('./bitfield')
7
+ const { oplogHeader, oplogEntry } = require('./messages')
8
+
9
+ module.exports = class Core {
10
+ constructor (header, crypto, oplog, tree, blocks, bitfield, sign, onupdate) {
11
+ this.onupdate = onupdate
12
+ this.header = header
13
+ this.crypto = crypto
14
+ this.oplog = oplog
15
+ this.tree = tree
16
+ this.blocks = blocks
17
+ this.bitfield = bitfield
18
+ this.defaultSign = sign
19
+ this.truncating = 0
20
+
21
+ this._maxOplogSize = 65536
22
+ this._autoFlush = 1
23
+ this._verifies = null
24
+ this._verifiesFlushed = null
25
+ this._mutex = new Mutex()
26
+ }
27
+
28
+ static async open (storage, opts = {}) {
29
+ const oplogFile = storage('oplog')
30
+ const treeFile = storage('tree')
31
+ const bitfieldFile = storage('bitfield')
32
+ const dataFile = storage('data')
33
+
34
+ try {
35
+ return await this.resume(oplogFile, treeFile, bitfieldFile, dataFile, opts)
36
+ } catch (err) {
37
+ return new Promise((resolve, reject) => {
38
+ let missing = 4
39
+
40
+ oplogFile.close(done)
41
+ treeFile.close(done)
42
+ bitfieldFile.close(done)
43
+ dataFile.close(done)
44
+
45
+ function done () {
46
+ if (--missing === 0) reject(err)
47
+ }
48
+ })
49
+ }
50
+ }
51
+
52
+ // TODO: we should prob have a general "auth" abstraction instead somewhere?
53
+ static createSigner (crypto, { publicKey, secretKey }) {
54
+ if (!crypto.validateKeyPair({ publicKey, secretKey })) throw new Error('Invalid key pair')
55
+ return signable => crypto.sign(signable, secretKey)
56
+ }
57
+
58
+ static async resume (oplogFile, treeFile, bitfieldFile, dataFile, opts) {
59
+ const overwrite = opts.overwrite === true
60
+ const createIfMissing = opts.createIfMissing !== false
61
+ const crypto = opts.crypto || hypercoreCrypto
62
+
63
+ const oplog = new Oplog(oplogFile, {
64
+ headerEncoding: oplogHeader,
65
+ entryEncoding: oplogEntry
66
+ })
67
+
68
+ let { header, entries } = await oplog.open()
69
+
70
+ if (!header || overwrite === true) {
71
+ if (!createIfMissing) {
72
+ throw new Error('No hypercore is stored here')
73
+ }
74
+
75
+ header = {
76
+ types: { tree: 'blake2b', bitfield: 'raw', signer: 'ed25519' },
77
+ userData: [],
78
+ tree: {
79
+ fork: 0,
80
+ length: 0,
81
+ rootHash: null,
82
+ signature: null
83
+ },
84
+ signer: opts.keyPair || crypto.keyPair(),
85
+ hints: {
86
+ reorgs: []
87
+ }
88
+ }
89
+
90
+ await oplog.flush(header)
91
+ }
92
+
93
+ if (opts.keyPair && !header.signer.publicKey.equals(opts.keyPair.publicKey)) {
94
+ throw new Error('Another hypercore is stored here')
95
+ }
96
+
97
+ const tree = await MerkleTree.open(treeFile, { crypto, ...header.tree })
98
+ const bitfield = await Bitfield.open(bitfieldFile)
99
+ const blocks = new BlockStore(dataFile, tree)
100
+
101
+ if (overwrite) {
102
+ await tree.clear()
103
+ await blocks.clear()
104
+ await bitfield.clear()
105
+ }
106
+
107
+ const sign = opts.sign || (header.signer.secretKey ? this.createSigner(crypto, header.signer) : null)
108
+
109
+ for (const e of entries) {
110
+ if (e.userData) {
111
+ updateUserData(header.userData, e.userData.key, e.userData.value)
112
+ }
113
+
114
+ if (e.treeNodes) {
115
+ for (const node of e.treeNodes) {
116
+ tree.addNode(node)
117
+ }
118
+ }
119
+
120
+ if (e.bitfield) {
121
+ bitfield.setRange(e.bitfield.start, e.bitfield.length)
122
+ }
123
+
124
+ if (e.treeUpgrade) {
125
+ const batch = await tree.truncate(e.treeUpgrade.length, e.treeUpgrade.fork)
126
+ batch.ancestors = e.treeUpgrade.ancestors
127
+ batch.signature = e.treeUpgrade.signature
128
+ addReorgHint(header.hints.reorgs, tree, batch)
129
+ batch.commit()
130
+
131
+ header.tree.length = tree.length
132
+ header.tree.fork = tree.fork
133
+ header.tree.rootHash = tree.hash()
134
+ header.tree.signature = tree.signature
135
+ }
136
+ }
137
+
138
+ return new this(header, crypto, oplog, tree, blocks, bitfield, sign, opts.onupdate || noop)
139
+ }
140
+
141
+ _shouldFlush () {
142
+ // TODO: make something more fancy for auto flush mode (like fibonacci etc)
143
+ if (--this._autoFlush <= 0 || this.oplog.byteLength >= this._maxOplogSize) {
144
+ this._autoFlush = 4
145
+ return true
146
+ }
147
+
148
+ return false
149
+ }
150
+
151
+ async _flushOplog () {
152
+ // TODO: the apis using this, actually do not need to wait for the bitfields, tree etc to flush
153
+ // as their mutations are already stored in the oplog. We could potentially just run this in the
154
+ // background. Might be easier to impl that where it is called instead and keep this one simple.
155
+ await this.bitfield.flush()
156
+ await this.tree.flush()
157
+ await this.oplog.flush(this.header)
158
+ }
159
+
160
+ _appendBlocks (values) {
161
+ return this.blocks.putBatch(this.tree.length, values, this.tree.byteLength)
162
+ }
163
+
164
+ async _writeBlock (batch, index, value) {
165
+ const byteOffset = await batch.byteOffset(index * 2)
166
+ await this.blocks.put(index, value, byteOffset)
167
+ }
168
+
169
+ async userData (key, value) {
170
+ // TODO: each oplog append can set user data, so we should have a way
171
+ // to just hitch a ride on one of the other ongoing appends?
172
+ await this._mutex.lock()
173
+
174
+ try {
175
+ let empty = true
176
+
177
+ for (const u of this.header.userData) {
178
+ if (u.key !== key) continue
179
+ if (value && u.value.equals(value)) return
180
+ empty = false
181
+ break
182
+ }
183
+
184
+ if (empty && !value) return
185
+
186
+ const entry = {
187
+ userData: { key, value },
188
+ treeNodes: null,
189
+ treeUpgrade: null,
190
+ bitfield: null
191
+ }
192
+
193
+ await this.oplog.append([entry], false)
194
+
195
+ updateUserData(this.header.userData, key, value)
196
+
197
+ if (this._shouldFlush()) await this._flushOplog()
198
+ } finally {
199
+ this._mutex.unlock()
200
+ }
201
+ }
202
+
203
+ async truncate (length, fork, sign = this.defaultSign) {
204
+ this.truncating++
205
+ await this._mutex.lock()
206
+
207
+ try {
208
+ const batch = await this.tree.truncate(length, fork)
209
+ batch.signature = await sign(batch.signable())
210
+ await this._truncate(batch, null)
211
+ } finally {
212
+ this.truncating--
213
+ this._mutex.unlock()
214
+ }
215
+ }
216
+
217
+ async append (values, sign = this.defaultSign, hooks = {}) {
218
+ await this._mutex.lock()
219
+
220
+ try {
221
+ if (hooks.preappend) await hooks.preappend(values)
222
+
223
+ if (!values.length) return this.tree.length
224
+
225
+ const batch = this.tree.batch()
226
+ for (const val of values) batch.append(val)
227
+
228
+ const hash = batch.hash()
229
+ batch.signature = await sign(batch.signable(hash))
230
+
231
+ const entry = {
232
+ userData: null,
233
+ treeNodes: batch.nodes,
234
+ treeUpgrade: batch,
235
+ bitfield: {
236
+ drop: false,
237
+ start: batch.ancestors,
238
+ length: values.length
239
+ }
240
+ }
241
+
242
+ await this._appendBlocks(values)
243
+ await this.oplog.append([entry], false)
244
+
245
+ this.bitfield.setRange(batch.ancestors, batch.length - batch.ancestors, true)
246
+ batch.commit()
247
+
248
+ this.header.tree.length = batch.length
249
+ this.header.tree.rootHash = hash
250
+ this.header.tree.signature = batch.signature
251
+ this.onupdate(0b01, entry.bitfield, null, null)
252
+
253
+ if (this._shouldFlush()) await this._flushOplog()
254
+
255
+ return batch.ancestors
256
+ } finally {
257
+ this._mutex.unlock()
258
+ }
259
+ }
260
+
261
+ async _verifyExclusive ({ batch, bitfield, value, from }) {
262
+ // TODO: move this to tree.js
263
+ const hash = batch.hash()
264
+ if (!batch.signature || !this.crypto.verify(batch.signable(hash), batch.signature, this.header.signer.publicKey)) {
265
+ throw new Error('Remote signature does not match')
266
+ }
267
+
268
+ await this._mutex.lock()
269
+
270
+ try {
271
+ if (!batch.commitable()) return false
272
+
273
+ const entry = {
274
+ userData: null,
275
+ treeNodes: batch.nodes,
276
+ treeUpgrade: batch,
277
+ bitfield
278
+ }
279
+
280
+ if (bitfield) await this._writeBlock(batch, bitfield.start, value)
281
+
282
+ await this.oplog.append([entry], false)
283
+
284
+ if (bitfield) this.bitfield.set(bitfield.start, true)
285
+ batch.commit()
286
+
287
+ this.header.tree.fork = batch.fork
288
+ this.header.tree.length = batch.length
289
+ this.header.tree.rootHash = batch.rootHash
290
+ this.header.tree.signature = batch.signature
291
+ this.onupdate(0b01, bitfield, value, from)
292
+
293
+ if (this._shouldFlush()) await this._flushOplog()
294
+ } finally {
295
+ this._mutex.unlock()
296
+ }
297
+
298
+ return true
299
+ }
300
+
301
+ async _verifyShared () {
302
+ if (!this._verifies.length) return false
303
+
304
+ await this._mutex.lock()
305
+
306
+ const verifies = this._verifies
307
+ this._verifies = null
308
+ this._verified = null
309
+
310
+ try {
311
+ const entries = []
312
+
313
+ for (const { batch, bitfield, value } of verifies) {
314
+ if (!batch.commitable()) continue
315
+
316
+ if (bitfield) {
317
+ await this._writeBlock(batch, bitfield.start, value)
318
+ }
319
+
320
+ entries.push({
321
+ userData: null,
322
+ treeNodes: batch.nodes,
323
+ treeUpgrade: null,
324
+ bitfield
325
+ })
326
+ }
327
+
328
+ await this.oplog.append(entries, false)
329
+
330
+ for (let i = 0; i < verifies.length; i++) {
331
+ const { batch, bitfield, value, from } = verifies[i]
332
+
333
+ if (!batch.commitable()) {
334
+ verifies[i] = null // signal that we cannot commit this one
335
+ continue
336
+ }
337
+
338
+ if (bitfield) this.bitfield.set(bitfield.start, true)
339
+ batch.commit()
340
+ this.onupdate(0, bitfield, value, from)
341
+ }
342
+
343
+ if (this._shouldFlush()) await this._flushOplog()
344
+ } finally {
345
+ this._mutex.unlock()
346
+ }
347
+
348
+ return verifies[0] !== null
349
+ }
350
+
351
+ async verify (proof, from) {
352
+ // We cannot apply "other forks" atm.
353
+ // We should probably still try and they are likely super similar for non upgrades
354
+ // but this is easy atm (and the above layer will just retry)
355
+
356
+ if (proof.fork !== this.tree.fork) return false
357
+
358
+ const batch = await this.tree.verify(proof)
359
+ if (!batch.commitable()) return false
360
+
361
+ const value = (proof.block && proof.block.value) || null
362
+ const op = {
363
+ batch,
364
+ bitfield: value && { drop: false, start: proof.block.index, length: 1 },
365
+ value: value,
366
+ from
367
+ }
368
+
369
+ if (batch.upgraded) return this._verifyExclusive(op)
370
+
371
+ if (this._verifies !== null) {
372
+ const verifies = this._verifies
373
+ const i = verifies.push(op)
374
+ await this._verified
375
+ return verifies[i] !== null
376
+ }
377
+
378
+ this._verifies = [op]
379
+ this._verified = this._verifyShared()
380
+ return this._verified
381
+ }
382
+
383
+ async reorg (batch, from) {
384
+ if (!batch.commitable()) return false
385
+
386
+ this.truncating++
387
+ await this._mutex.lock()
388
+
389
+ try {
390
+ if (!batch.commitable()) return false
391
+ await this._truncate(batch, from)
392
+ } finally {
393
+ this.truncating--
394
+ this._mutex.unlock()
395
+ }
396
+
397
+ return true
398
+ }
399
+
400
+ async _truncate (batch, from) {
401
+ const entry = {
402
+ userData: null,
403
+ treeNodes: batch.nodes,
404
+ treeUpgrade: batch,
405
+ bitfield: {
406
+ drop: true,
407
+ start: batch.ancestors,
408
+ length: this.tree.length - batch.ancestors
409
+ }
410
+ }
411
+
412
+ await this.oplog.append([entry], false)
413
+
414
+ this.bitfield.setRange(batch.ancestors, this.tree.length - batch.ancestors, false)
415
+ addReorgHint(this.header.hints.reorgs, this.tree, batch)
416
+ batch.commit()
417
+
418
+ const appended = batch.length > batch.ancestors
419
+
420
+ this.header.tree.fork = batch.fork
421
+ this.header.tree.length = batch.length
422
+ this.header.tree.rootHash = batch.hash()
423
+ this.header.tree.signature = batch.signature
424
+ this.onupdate(appended ? 0b11 : 0b10, entry.bitfield, null, from)
425
+
426
+ // TODO: there is a bug in the merkle tree atm where it cannot handle unflushed
427
+ // truncates if we append or download anything after the truncation point later on
428
+ // This is because tree.get checks the truncated flag. We should fix this so we can do
429
+ // the later flush here as well
430
+ // if (this._shouldFlush()) await this._flushOplog()
431
+ await this._flushOplog()
432
+ }
433
+
434
+ async close () {
435
+ await this._mutex.destroy()
436
+ await Promise.allSettled([
437
+ this.oplog.close(),
438
+ this.bitfield.close(),
439
+ this.tree.close(),
440
+ this.blocks.close()
441
+ ])
442
+ }
443
+ }
444
+
445
+ function addReorgHint (list, tree, batch) {
446
+ if (tree.length === 0 || tree.fork === batch.fork) return
447
+
448
+ while (list.length >= 4) list.shift() // 4 here is arbitrary, just want it to be small (hints only)
449
+ while (list.length > 0) {
450
+ if (list[list.length - 1].ancestors > batch.ancestors) list.pop()
451
+ else break
452
+ }
453
+
454
+ list.push({ from: tree.fork, to: batch.fork, ancestors: batch.ancestors })
455
+ }
456
+
457
+ function updateUserData (list, key, value) {
458
+ for (let i = 0; i < list.length; i++) {
459
+ if (list[i].key === key) {
460
+ if (value) list[i].value = value
461
+ else list.splice(i, 1)
462
+ return
463
+ }
464
+ }
465
+ if (value) list.push({ key, value })
466
+ }
467
+
468
+ function noop () {}
@@ -0,0 +1,76 @@
1
+ class Extension {
2
+ constructor (extensions, name, handlers) {
3
+ this.extensions = extensions
4
+ this.name = name
5
+ this.encoding = handlers.encoding
6
+ this.destroyed = false
7
+ // TODO: should avoid the bind here by calling directly on handlers instead?
8
+ this.onmessage = (handlers.onmessage || noop).bind(handlers)
9
+ this.onremotesupports = (handlers.onremotesupports || noop).bind(handlers)
10
+ }
11
+
12
+ send (message, peer) {
13
+ if (this.destroyed) return
14
+ const ext = peer.extensions.get(this.name)
15
+ if (ext) ext.send(message)
16
+ }
17
+
18
+ broadcast (message) {
19
+ if (this.extensions.replicator === null || this.destroyed) return
20
+ for (const peer of this.extensions.replicator.peers) this.send(message, peer)
21
+ }
22
+
23
+ destroy () {
24
+ if (this.destroyed) return
25
+ this.destroyed = true
26
+ this.extensions.all.delete(this.name)
27
+ if (this.extensions.replicator === null) return
28
+ for (const peer of this.extensions.replicator.peers) {
29
+ const ext = peer.extensions.get(this.name)
30
+ if (ext) ext.destroy()
31
+ }
32
+ }
33
+ }
34
+
35
+ module.exports = class Extensions {
36
+ constructor () {
37
+ this.replicator = null
38
+ this.all = new Map()
39
+ }
40
+
41
+ [Symbol.iterator] () {
42
+ return this.all[Symbol.iterator]()
43
+ }
44
+
45
+ attach (replicator) {
46
+ if (replicator === this.replicator) return
47
+ this.replicator = replicator
48
+
49
+ for (const [name, ext] of this.all) {
50
+ for (const peer of this.replicator.peers) {
51
+ peer.registerExtension(name, ext)
52
+ }
53
+ }
54
+ }
55
+
56
+ register (name, handlers, ext = new Extension(this, name, handlers)) {
57
+ if (this.all.has(name)) this.all.get(name).destroy()
58
+ this.all.set(name, ext)
59
+
60
+ if (this.replicator !== null) {
61
+ for (const peer of this.replicator.peers) {
62
+ peer.registerExtension(name, ext)
63
+ }
64
+ }
65
+
66
+ return ext
67
+ }
68
+
69
+ update (peer) {
70
+ for (const ext of this.all.values()) {
71
+ peer.registerExtension(ext.name, ext)
72
+ }
73
+ }
74
+ }
75
+
76
+ function noop () {}