hypercore 11.0.37 → 11.0.39

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 CHANGED
@@ -40,7 +40,7 @@ Make a new Hypercore instance.
40
40
  const core = new Hypercore('./directory') // store data in ./directory
41
41
  ```
42
42
 
43
- Alternatively you can pass a [Hypercore Storage](https://github.com/holepunchto/hypercore-storage) or use a [Corestore](https://github.com/holepunchto/corestore) if you want to make many Hypercores efficiently.
43
+ Alternatively you can pass a [Hypercore Storage](https://github.com/holepunchto/hypercore-storage) or use a [Corestore](https://github.com/holepunchto/corestore) if you want to make many Hypercores efficiently. Note that `random-access-storage` is no longer supported.
44
44
 
45
45
  `key` can be set to a Hypercore key which is a hash of Hypercore's internal auth manifest, describing how to validate the Hypercore. If you do not set this, it will be loaded from storage. If nothing is previously stored, a new auth manifest will be generated giving you local write access to it.
46
46
 
@@ -50,15 +50,17 @@ Alternatively you can pass a [Hypercore Storage](https://github.com/holepunchto/
50
50
  {
51
51
  createIfMissing: true, // create a new Hypercore key pair if none was present in storage
52
52
  overwrite: false, // overwrite any old Hypercore that might already exist
53
- sparse: true, // enable sparse mode, counting unavailable blocks towards core.length and core.byteLength
54
53
  valueEncoding: 'json' | 'utf-8' | 'binary', // defaults to binary
55
54
  encodeBatch: batch => { ... }, // optionally apply an encoding to complete batches
56
55
  keyPair: kp, // optionally pass the public key and secret key as a key pair
57
- encryptionKey: k, // optionally pass an encryption key to enable block encryption
56
+ encryption: { key: buffer }, // the block encryption key
58
57
  onwait: () => {}, // hook that is called if gets are waiting for download
59
58
  timeout: 0, // wait at max some milliseconds (0 means no timeout)
60
59
  writable: true, // disable appends and truncates
61
- inflightRange: null // Advanced option. Set to [minInflight, maxInflight] to change the min and max inflight blocks per peer when downloading.
60
+ inflightRange: null, // Advanced option. Set to [minInflight, maxInflight] to change the min and max inflight blocks per peer when downloading.
61
+ ongc: (session) => { ... }, // A callback called when the session is garbage collected
62
+ notDownloadingLinger: 20000, // How many milliseconds to wait after downloading finishes keeping the connection open. Defaults to a random number between 20-40s
63
+ allowFork: true, // Enables updating core when it forks
62
64
  }
63
65
  ```
64
66
 
@@ -292,7 +294,42 @@ You must close any session you make.
292
294
 
293
295
  Options are inherited from the parent instance, unless they are re-set.
294
296
 
295
- `options` are the same as in the constructor.
297
+ `options` are the same as in the constructor with the follow additions:
298
+
299
+ ```
300
+ {
301
+ weak: false // Creates the session as a "weak ref" which closes when all non-weak sessions are closed
302
+ exclusive: false, // Create a session with exclusive access to the core. Creating an exclusive session on a core with other exclusive sessions, will wait for the session with access to close before the next exclusive session is `ready`
303
+ checkout: undefined, // A index to checkout the core at. Checkout sessions must be an atom or a named session
304
+ atom: undefined, // A storage atom for making atomic batch changes across hypercores
305
+ name: null, // Name the session creating a persisted branch of the core. Still beta so may break in the future
306
+ }
307
+ ```
308
+
309
+ Atoms allow making atomic changes across multiple hypercores. Atoms can be created using a `core`'s `storage` (eg. `const atom = core.state.storage.createAtom()`). Changes made with an atom based session is not persisted until the atom is flushed via `await atom.flush()`, but can be read at any time. When atoms flush, all changes made outside of the atom will be clobbered as the core blocks will now match the atom's blocks. For example:
310
+
311
+ ```js
312
+ const core = new Hypercore('./atom-example')
313
+ await core.ready()
314
+
315
+ await core.append('block 1')
316
+
317
+ const atom = core.state.storage.createAtom()
318
+ const atomicSession = core.session({ atom })
319
+
320
+ await core.append('block 2') // Add blocks not using the atom
321
+
322
+ await atomicSession.append('atom block 2') // Add different block to atom
323
+ await atom.flush()
324
+
325
+ console.log((await core.get(core.length - 1)).toString()) // prints 'atom block 2' not 'block 2'
326
+ ```
327
+
328
+ #### `const { byteLength, length } = core.commit(session, opts = {})`
329
+
330
+ Attempt to apply blocks from the session to the `core`. `core` must be a default core, aka a non-named session.
331
+
332
+ Returns `null` if committing failed.
296
333
 
297
334
  #### `const snapshot = core.snapshot([options])`
298
335
 
@@ -394,7 +431,13 @@ Buffer containing the optional block encryption key of this core. Will be `null`
394
431
 
395
432
  #### `core.length`
396
433
 
397
- How many blocks of data are available on this core? If `sparse: false`, this will equal `core.contiguousLength`.
434
+ How many blocks of data are available on this core.
435
+
436
+ Populated after `ready` has been emitted. Will be `0` before the event.
437
+
438
+ #### `core.signedLength`
439
+
440
+ How many blocks of data are available on this core that have been signed by a quorum. This is equal to `core.length` for Hypercores's with a single signer.
398
441
 
399
442
  Populated after `ready` has been emitted. Will be `0` before the event.
400
443
 
package/lib/audit.js CHANGED
@@ -1,121 +1,150 @@
1
- const hypercoreCrypto = require('hypercore-crypto')
1
+ const crypto = require('hypercore-crypto')
2
2
  const flat = require('flat-tree')
3
3
  const b4a = require('b4a')
4
+ const { MerkleTree } = require('./merkle-tree')
4
5
 
5
- const BitInterlude = require('./bit-interlude')
6
+ module.exports = async function auditCore (core, { tree = true, blocks = true, bitfield = true, dryRun = false } = {}) {
7
+ const length = core.state.length
8
+ const stats = { treeNodes: 0, blocks: 0, bits: 0, droppedTreeNodes: 0, droppedBlocks: 0, droppedBits: 0, corrupt: false }
6
9
 
7
- // this is optimised for speed over mem atm
8
- // can be tweaked in the future
10
+ // audit the tree
11
+ if (tree) {
12
+ let tx = null
9
13
 
10
- module.exports = async function auditCore (core, storage) {
11
- const corrections = {
12
- tree: 0,
13
- blocks: 0
14
- }
14
+ const roots = await MerkleTree.getRootsFromStorage(core.state.storage, length)
15
+ const stack = []
15
16
 
16
- const length = core.header.tree.length
17
+ for (const r of roots) {
18
+ if (r === null) {
19
+ if (!dryRun) {
20
+ const storage = core.state.storage
21
+ await storage.store.deleteCore(storage.core)
22
+ return null
23
+ }
17
24
 
18
- const bitfield = new BitInterlude()
25
+ stats.corrupt = true
26
+ }
19
27
 
20
- const data = await readAllBlocks(core.storage)
21
- const tree = await readAllTreeNodes(core.tree.storage)
28
+ stack.push(r)
29
+ }
22
30
 
23
- const valid = new Uint8Array(Math.ceil(tree.byteLength / 40))
24
- const stack = []
31
+ stats.treeNodes += roots.length
25
32
 
26
- for (const r of core.tree.roots) {
27
- valid[r.index] = 1
28
- stack.push(r)
29
- }
33
+ while (stack.length > 0) {
34
+ const node = stack.pop()
30
35
 
31
- while (stack.length > 0) {
32
- const node = stack.pop()
33
- if ((node.index & 1) === 0) continue
36
+ if ((node.index & 1) === 0) continue
34
37
 
35
- const [left, right] = flat.children(node.index)
36
- const leftNode = tree.get(left)
37
- const rightNode = tree.get(right)
38
+ const [left, right] = flat.children(node.index)
38
39
 
39
- if (!rightNode && !leftNode) continue
40
+ const rx = core.state.storage.read()
41
+ const leftNodePromise = rx.getTreeNode(left)
42
+ const rightNodePromise = rx.getTreeNode(right)
40
43
 
41
- stack.push(leftNode, rightNode)
44
+ rx.tryFlush()
42
45
 
43
- if (valid[node.index]) {
44
- const hash = hypercoreCrypto.parent(leftNode, rightNode)
45
- if (b4a.equals(hash, node.hash) && node.size === (leftNode.size + rightNode.size)) {
46
- valid[leftNode.index] = 1
47
- valid[rightNode.index] = 1
46
+ const [leftNode, rightNode] = await Promise.all([leftNodePromise, rightNodePromise])
47
+
48
+ if (isBadTree(node, leftNode, rightNode)) {
49
+ if (!tx && !stats.corrupt) tx = core.state.storage.write()
50
+ const [l, r] = flat.spans(node.index)
51
+ tx.deleteTreeNodeRange(l, r + 1)
52
+ stats.droppedTreeNodes++
48
53
  continue
49
54
  }
55
+
56
+ if (!leftNode) continue
57
+
58
+ stats.treeNodes += 2
59
+ stack.push(leftNode, rightNode)
50
60
  }
51
61
 
52
- if (leftNode.size) clearNode(left)
53
- if (rightNode.size) clearNode(right)
62
+ if (tx && !dryRun) await tx.flush()
54
63
  }
55
64
 
56
- let i = 0
57
- let nextOffset = -1
58
- while (i < length) {
59
- const has = core.bitfield.get(i)
60
-
61
- if (!has) {
62
- if (i + 1 === length) break
63
- i = core.bitfield.findFirst(true, i + 1)
64
- if (i < 0) break
65
- nextOffset = -1
66
- continue
67
- }
65
+ // audit the blocks
66
+ if (blocks) {
67
+ let tx = null
68
68
 
69
- if (nextOffset === -1) {
70
- try {
71
- nextOffset = await core.tree.byteOffset(i * 2)
72
- } catch {
73
- storage.deleteBlock(i)
74
- bitfield.set(i, false)
75
- corrections.blocks++
76
- i++
77
- continue
69
+ for await (const block of core.state.storage.createBlockStream()) {
70
+ if (!core.bitfield.get(block.index)) {
71
+ if (!tx && !stats.corrupt) tx = core.state.storage.write()
72
+ tx.deleteBlock(block.index)
73
+ stats.droppedBlocks++
78
74
  }
79
- }
80
75
 
81
- const node = tree.get(i * 2)
82
- const blk = data.get(i)
83
- const hash = hypercoreCrypto.data(blk)
76
+ const rx = core.state.storage.read()
77
+ const treeNodePromise = rx.getTreeNode(2 * block.index)
78
+
79
+ rx.tryFlush()
84
80
 
85
- nextOffset += blk.byteLength
81
+ const treeNode = await treeNodePromise
82
+
83
+ if (isBadBlock(treeNode, block.value)) {
84
+ if (!tx && !stats.corrupt) tx = core.state.storage.write()
85
+ tx.deleteBlock(block.index)
86
+ stats.droppedBlocks++
87
+ continue
88
+ }
86
89
 
87
- if (!b4a.equals(hash, node.hash)) {
88
- storage.deleteBlock(i)
89
- bitfield.set(i, false)
90
- corrections.blocks++
90
+ stats.blocks++
91
91
  }
92
92
 
93
- i++
93
+ if (tx && !dryRun) await tx.flush()
94
94
  }
95
95
 
96
- bitfield.flush(storage, core.bitfield)
96
+ if (bitfield) {
97
+ let tx = null
97
98
 
98
- return corrections
99
+ for (const index of allBits(core.bitfield)) {
100
+ const rx = core.state.storage.read()
101
+ const blockPromise = rx.getBlock(index)
99
102
 
100
- function clearNode (node) {
101
- valid[node.index] = 0
102
- storage.deleteTreeNode(node.index)
103
- corrections.tree++
103
+ rx.tryFlush()
104
+
105
+ const block = await blockPromise
106
+ if (!block) {
107
+ stats.droppedBits++
108
+ if (dryRun) continue
109
+
110
+ if (!tx && !stats.corrupt) tx = core.state.storage.write()
111
+
112
+ core.bitfield.set(index, false)
113
+
114
+ const page = core.bitfield.getBitfield(index)
115
+ if (page.bitfield) tx.setBitfieldPage(page.index, page.bitfield)
116
+ else tx.deleteBitfieldPage(page.idnex)
117
+ continue
118
+ }
119
+
120
+ stats.bits++
121
+ }
122
+
123
+ if (tx && !dryRun) await tx.flush()
104
124
  }
125
+
126
+ return stats
105
127
  }
106
128
 
107
- async function readAllBlocks (storage) {
108
- const data = new Map()
109
- for await (const block of storage.createBlockStream()) {
110
- data.set(block.index, block.value)
111
- }
112
- return data
129
+ function isBadBlock (node, block) {
130
+ if (!node) return true
131
+ const hash = crypto.data(block)
132
+ return !b4a.equals(hash, node.hash) || node.size !== block.byteLength
113
133
  }
114
134
 
115
- async function readAllTreeNodes (storage) {
116
- const nodes = new Map()
117
- for await (const node of storage.createTreeNodeStream()) {
118
- nodes.set(node.index, node)
135
+ function isBadTree (parent, left, right) {
136
+ if (!left && !right) return false
137
+ if (!left || !right) return true
138
+ const hash = crypto.parent(left, right)
139
+ return !b4a.equals(hash, parent.hash) || parent.size !== (left.size + right.size)
140
+ }
141
+
142
+ function * allBits (bitfield) {
143
+ let i = 0
144
+ if (bitfield.get(0)) yield 0
145
+ while (true) {
146
+ i = bitfield.findFirst(true, i + 1)
147
+ if (i === -1) break
148
+ yield i
119
149
  }
120
- return nodes
121
150
  }
package/lib/core.js CHANGED
@@ -286,19 +286,11 @@ module.exports = class Core {
286
286
  this._manifestFlushed = !!header.manifest
287
287
  }
288
288
 
289
- async audit () {
289
+ async audit (opts) {
290
290
  await this.state.mutex.lock()
291
291
 
292
292
  try {
293
- const tx = this.state.createWriteBatch()
294
-
295
- // TODO: refactor audit
296
- const corrections = await audit(this, tx)
297
- if (corrections.blocks || corrections.tree) {
298
- await this.state.flushUpdate(tx)
299
- }
300
-
301
- return corrections
293
+ return await audit(this, opts)
302
294
  } finally {
303
295
  this.state._unlock()
304
296
  }
package/lib/replicator.js CHANGED
@@ -351,11 +351,13 @@ class ProofRequest {
351
351
  async fulfill () {
352
352
  if (this.proof === null) return null
353
353
 
354
- const proof = await this.proof.settle()
354
+ const [proof, block] = await Promise.all([this.proof.settle(), this.block])
355
355
 
356
356
  if (this.manifest) proof.manifest = this.manifest
357
- if (this.block) proof.block.value = await this.block
358
357
 
358
+ if (!block && proof.block) return null
359
+
360
+ if (block) proof.block.value = block
359
361
  return proof
360
362
  }
361
363
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypercore",
3
- "version": "11.0.37",
3
+ "version": "11.0.39",
4
4
  "description": "Hypercore is a secure, distributed append-only log",
5
5
  "main": "index.js",
6
6
  "scripts": {