hypercore 10.0.0-alpha.3 → 10.0.0-alpha.30
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 +45 -3
- package/index.js +390 -156
- package/lib/bitfield.js +9 -5
- package/lib/block-encryption.js +68 -0
- package/lib/block-store.js +3 -1
- package/lib/caps.js +34 -0
- package/lib/core.js +32 -13
- package/lib/merkle-tree.js +184 -109
- package/lib/messages.js +245 -167
- package/lib/oplog.js +4 -3
- package/lib/replicator.js +1355 -593
- package/lib/streams.js +56 -0
- package/package.json +17 -8
- package/.github/workflows/test-node.yml +0 -24
- package/UPGRADE.md +0 -9
- package/examples/announce.js +0 -19
- package/examples/basic.js +0 -10
- package/examples/http.js +0 -123
- package/examples/lookup.js +0 -20
- package/lib/extensions.js +0 -76
- package/lib/protocol.js +0 -522
- package/lib/random-iterator.js +0 -46
- package/test/basic.js +0 -78
- package/test/bitfield.js +0 -71
- package/test/core.js +0 -290
- package/test/encodings.js +0 -18
- package/test/extension.js +0 -71
- package/test/helpers/index.js +0 -23
- package/test/merkle-tree.js +0 -518
- package/test/mutex.js +0 -137
- package/test/oplog.js +0 -399
- package/test/preload.js +0 -72
- package/test/replicate.js +0 -296
- package/test/sessions.js +0 -173
- package/test/user-data.js +0 -47
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) {
|
|
@@ -40,8 +41,9 @@ module.exports = class Bitfield {
|
|
|
40
41
|
this.pages = new BigSparseArray()
|
|
41
42
|
this.unflushed = []
|
|
42
43
|
this.storage = storage
|
|
44
|
+
this.resumed = !!(buf && buf.byteLength >= 4)
|
|
43
45
|
|
|
44
|
-
const all =
|
|
46
|
+
const all = this.resumed
|
|
45
47
|
? new Uint32Array(buf.buffer, buf.byteOffset, Math.floor(buf.byteLength / 4))
|
|
46
48
|
: new Uint32Array(1024)
|
|
47
49
|
|
|
@@ -92,8 +94,10 @@ module.exports = class Bitfield {
|
|
|
92
94
|
clear () {
|
|
93
95
|
return new Promise((resolve, reject) => {
|
|
94
96
|
this.storage.del(0, Infinity, (err) => {
|
|
95
|
-
if (err) reject(err)
|
|
96
|
-
|
|
97
|
+
if (err) return reject(err)
|
|
98
|
+
this.pages = new BigSparseArray()
|
|
99
|
+
this.unflushed = []
|
|
100
|
+
resolve()
|
|
97
101
|
})
|
|
98
102
|
})
|
|
99
103
|
}
|
|
@@ -116,9 +120,9 @@ module.exports = class Bitfield {
|
|
|
116
120
|
let error = null
|
|
117
121
|
|
|
118
122
|
for (const page of this.unflushed) {
|
|
119
|
-
const
|
|
123
|
+
const buf = b4a.from(page.bitfield.buffer, page.bitfield.byteOffset, page.bitfield.byteLength)
|
|
120
124
|
page.dirty = false
|
|
121
|
-
this.storage.write(page.index * 4096,
|
|
125
|
+
this.storage.write(page.index * 4096, buf, done)
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
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/caps.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const crypto = require('hypercore-crypto')
|
|
2
|
+
const sodium = require('sodium-universal')
|
|
3
|
+
const b4a = require('b4a')
|
|
4
|
+
const c = require('compact-encoding')
|
|
5
|
+
|
|
6
|
+
// TODO: rename this to "crypto" and move everything hashing related etc in here
|
|
7
|
+
// Also lets move the tree stuff from hypercore-crypto here, and loose the types
|
|
8
|
+
// from the hashes there - they are not needed since we lock the indexes in the tree
|
|
9
|
+
// hash and just makes alignment etc harder in other languages
|
|
10
|
+
|
|
11
|
+
const [TREE, REPLICATE_INITIATOR, REPLICATE_RESPONDER] = crypto.namespace('hypercore', 3)
|
|
12
|
+
|
|
13
|
+
exports.replicate = function (isInitiator, key, handshakeHash) {
|
|
14
|
+
const out = b4a.allocUnsafe(32)
|
|
15
|
+
sodium.crypto_generichash_batch(out, [isInitiator ? REPLICATE_INITIATOR : REPLICATE_RESPONDER, key], handshakeHash)
|
|
16
|
+
return out
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
exports.treeSignable = function (hash, length, fork) {
|
|
20
|
+
const state = { start: 0, end: 80, buffer: b4a.allocUnsafe(80) }
|
|
21
|
+
c.raw.encode(state, TREE)
|
|
22
|
+
c.raw.encode(state, hash)
|
|
23
|
+
c.uint64.encode(state, length)
|
|
24
|
+
c.uint64.encode(state, fork)
|
|
25
|
+
return state.buffer
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
exports.treeSignableLegacy = function (hash, length, fork) {
|
|
29
|
+
const state = { start: 0, end: 48, buffer: b4a.allocUnsafe(48) }
|
|
30
|
+
c.raw.encode(state, hash)
|
|
31
|
+
c.uint64.encode(state, length)
|
|
32
|
+
c.uint64.encode(state, fork)
|
|
33
|
+
return state.buffer
|
|
34
|
+
}
|
package/lib/core.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
const hypercoreCrypto = require('hypercore-crypto')
|
|
2
|
+
const b4a = require('b4a')
|
|
2
3
|
const Oplog = require('./oplog')
|
|
3
4
|
const Mutex = require('./mutex')
|
|
4
5
|
const MerkleTree = require('./merkle-tree')
|
|
5
6
|
const BlockStore = require('./block-store')
|
|
6
7
|
const Bitfield = require('./bitfield')
|
|
7
|
-
const
|
|
8
|
+
const m = require('./messages')
|
|
8
9
|
|
|
9
10
|
module.exports = class Core {
|
|
10
|
-
constructor (header, crypto, oplog, tree, blocks, bitfield, sign, onupdate) {
|
|
11
|
+
constructor (header, crypto, oplog, tree, blocks, bitfield, sign, legacy, onupdate) {
|
|
11
12
|
this.onupdate = onupdate
|
|
12
13
|
this.header = header
|
|
13
14
|
this.crypto = crypto
|
|
@@ -23,6 +24,7 @@ module.exports = class Core {
|
|
|
23
24
|
this._verifies = null
|
|
24
25
|
this._verifiesFlushed = null
|
|
25
26
|
this._mutex = new Mutex()
|
|
27
|
+
this._legacy = legacy
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
static async open (storage, opts = {}) {
|
|
@@ -56,18 +58,24 @@ module.exports = class Core {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
static async resume (oplogFile, treeFile, bitfieldFile, dataFile, opts) {
|
|
59
|
-
|
|
61
|
+
let overwrite = opts.overwrite === true
|
|
62
|
+
|
|
63
|
+
const force = opts.force === true
|
|
60
64
|
const createIfMissing = opts.createIfMissing !== false
|
|
61
65
|
const crypto = opts.crypto || hypercoreCrypto
|
|
62
66
|
|
|
63
67
|
const oplog = new Oplog(oplogFile, {
|
|
64
|
-
headerEncoding:
|
|
65
|
-
entryEncoding:
|
|
68
|
+
headerEncoding: m.oplog.header,
|
|
69
|
+
entryEncoding: m.oplog.entry
|
|
66
70
|
})
|
|
67
71
|
|
|
68
72
|
let { header, entries } = await oplog.open()
|
|
69
73
|
|
|
70
|
-
if (
|
|
74
|
+
if (force && opts.keyPair && header && header.signer && !b4a.equals(header.signer.publicKey, opts.keyPair.publicKey)) {
|
|
75
|
+
overwrite = true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!header || overwrite) {
|
|
71
79
|
if (!createIfMissing) {
|
|
72
80
|
throw new Error('No hypercore is stored here')
|
|
73
81
|
}
|
|
@@ -90,7 +98,7 @@ module.exports = class Core {
|
|
|
90
98
|
await oplog.flush(header)
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
if (opts.keyPair && !header.signer.publicKey
|
|
101
|
+
if (opts.keyPair && !b4a.equals(header.signer.publicKey, opts.keyPair.publicKey)) {
|
|
94
102
|
throw new Error('Another hypercore is stored here')
|
|
95
103
|
}
|
|
96
104
|
|
|
@@ -102,6 +110,10 @@ module.exports = class Core {
|
|
|
102
110
|
await tree.clear()
|
|
103
111
|
await blocks.clear()
|
|
104
112
|
await bitfield.clear()
|
|
113
|
+
entries = []
|
|
114
|
+
} else if (bitfield.resumed && header.tree.length === 0) {
|
|
115
|
+
// If this was an old bitfield, reset it since it loads based on disk size atm (TODO: change that)
|
|
116
|
+
await bitfield.clear()
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
const sign = opts.sign || (header.signer.secretKey ? this.createSigner(crypto, header.signer) : null)
|
|
@@ -118,7 +130,7 @@ module.exports = class Core {
|
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
if (e.bitfield) {
|
|
121
|
-
bitfield.setRange(e.bitfield.start, e.bitfield.length)
|
|
133
|
+
bitfield.setRange(e.bitfield.start, e.bitfield.length, !e.bitfield.drop)
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
if (e.treeUpgrade) {
|
|
@@ -135,7 +147,7 @@ module.exports = class Core {
|
|
|
135
147
|
}
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
return new this(header, crypto, oplog, tree, blocks, bitfield, sign, opts.onupdate || noop)
|
|
150
|
+
return new this(header, crypto, oplog, tree, blocks, bitfield, sign, !!opts.legacy, opts.onupdate || noop)
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
_shouldFlush () {
|
|
@@ -176,7 +188,7 @@ module.exports = class Core {
|
|
|
176
188
|
|
|
177
189
|
for (const u of this.header.userData) {
|
|
178
190
|
if (u.key !== key) continue
|
|
179
|
-
if (value &&
|
|
191
|
+
if (value && b4a.equals(u.value, value)) return
|
|
180
192
|
empty = false
|
|
181
193
|
break
|
|
182
194
|
}
|
|
@@ -214,17 +226,19 @@ module.exports = class Core {
|
|
|
214
226
|
}
|
|
215
227
|
}
|
|
216
228
|
|
|
217
|
-
async append (values, sign = this.defaultSign) {
|
|
229
|
+
async append (values, sign = this.defaultSign, hooks = {}) {
|
|
218
230
|
await this._mutex.lock()
|
|
219
231
|
|
|
220
232
|
try {
|
|
233
|
+
if (hooks.preappend) await hooks.preappend(values)
|
|
234
|
+
|
|
221
235
|
if (!values.length) return this.tree.length
|
|
222
236
|
|
|
223
237
|
const batch = this.tree.batch()
|
|
224
238
|
for (const val of values) batch.append(val)
|
|
225
239
|
|
|
226
240
|
const hash = batch.hash()
|
|
227
|
-
batch.signature = await sign(batch.signable(hash))
|
|
241
|
+
batch.signature = await sign(this._legacy ? batch.signableLegacy(hash) : batch.signable(hash))
|
|
228
242
|
|
|
229
243
|
const entry = {
|
|
230
244
|
userData: null,
|
|
@@ -256,10 +270,15 @@ module.exports = class Core {
|
|
|
256
270
|
}
|
|
257
271
|
}
|
|
258
272
|
|
|
273
|
+
_signed (batch, hash) {
|
|
274
|
+
const signable = this._legacy ? batch.signableLegacy(hash) : batch.signable(hash)
|
|
275
|
+
return this.crypto.verify(signable, batch.signature, this.header.signer.publicKey)
|
|
276
|
+
}
|
|
277
|
+
|
|
259
278
|
async _verifyExclusive ({ batch, bitfield, value, from }) {
|
|
260
279
|
// TODO: move this to tree.js
|
|
261
280
|
const hash = batch.hash()
|
|
262
|
-
if (!batch.signature || !this.
|
|
281
|
+
if (!batch.signature || !this._signed(batch, hash)) {
|
|
263
282
|
throw new Error('Remote signature does not match')
|
|
264
283
|
}
|
|
265
284
|
|