blind-peer 0.0.4 → 2.7.5

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/index.js CHANGED
@@ -1,146 +1,514 @@
1
- const { EventEmitter } = require('events')
2
- const AutobaseLightWriter = require('autobase-light-writer')
3
- const HyperDB = require('hyperdb')
4
1
  const Corestore = require('corestore')
5
- const definition = require('./spec/hyperdb')
6
- const schema = require('./spec/hyperschema')
7
- const path = require('path')
2
+ const Hypercore = require('hypercore')
3
+ const RocksDB = require('rocksdb-native')
4
+ const ReadyResource = require('ready-resource')
8
5
  const Hyperswarm = require('hyperswarm')
9
6
  const ProtomuxRPC = require('protomux-rpc')
10
7
  const c = require('compact-encoding')
8
+ const b4a = require('b4a')
9
+ const crypto = require('hypercore-crypto')
10
+ const safetyCatch = require('safety-catch')
11
+ const Wakeup = require('protomux-wakeup')
12
+ const ScopeLock = require('scope-lock')
13
+ const IdEnc = require('hypercore-id-encoding')
11
14
 
12
- module.exports = class BlindPeer extends EventEmitter {
13
- constructor (storage) {
14
- super()
15
+ const BlindPeerDB = require('./lib/db.js')
16
+
17
+ const { AddCoreEncoding } = require('blind-peer-encodings')
18
+
19
+ class CoreTracker {
20
+ constructor (blindPeer, core) {
21
+ this.blindPeer = blindPeer
22
+ this.core = core
23
+ this.destroyed = false
24
+ this.record = null
25
+ this.activated = false
26
+ this.updated = false
27
+ this.id = null
28
+ this.referrerDiscoveryKey = null
29
+ this.channel = null
30
+ this.downloadRange = null
31
+ this.announceToReferrerBound = this.announceToReferrer.bind(this)
15
32
 
16
- this.db = HyperDB.rocks(path.join(storage, 'hyperdb'), definition)
17
- this.store = new Corestore(path.join(storage, 'corestore'))
18
- this.store.on('core-open', this._oncoreopen.bind(this))
19
- this.swarm = null
33
+ const onupdate = this._onupdate.bind(this)
34
+ const onactive = this._onactive.bind(this)
35
+
36
+ this.core.on('upload', onactive)
37
+ this.core.on('download', onactive)
38
+ this.core.on('truncate', onupdate)
39
+ this.core.on('append', onupdate)
20
40
  }
21
41
 
22
- _onconnection (connection) {
23
- this.store.replicate(connection)
42
+ _onupdate () {
43
+ this.updated = true
44
+ if (!this.record) return
24
45
 
25
- const rpc = new ProtomuxRPC(connection, {
26
- id: this.swarm.keyPair.publicKey,
27
- valueEncoding: c.none
28
- })
46
+ this.record.length = this.core.length
47
+ this.record.bytesAllocated = this.core.byteLength - this.record.bytesCleared
48
+ this.blindPeer.db.updateCore(this.record, this.id)
49
+ this.blindPeer.flush().then(this.announceToReferrerBound, safetyCatch)
50
+ }
51
+
52
+ _onactive () {
53
+ this.activated = true
29
54
 
30
- rpc.respond('add-mailbox', {
31
- requestEncoding: schema.resolveStruct('@blind-peer/request-mailbox'),
32
- responseEncoding: schema.resolveStruct('@blind-peer/response-mailbox')
33
- }, this._onrpcadd.bind(this))
55
+ if (this.record) {
56
+ this.blindPeer.db.updateCore(this.record, this.id)
57
+ }
34
58
 
35
- rpc.respond('post', {
36
- requestEncoding: schema.resolveStruct('@blind-peer/request-post'),
37
- responseEncoding: schema.resolveStruct('@blind-peer/response-post')
38
- }, this._onrpcpost.bind(this))
59
+ this.blindPeer.emit('core-activity', this.core, this.record)
39
60
  }
40
61
 
41
- async _onrpcadd (req) {
42
- this.emit('add-request', req)
43
- const res = await this.add(req)
44
- this.emit('add-response', req, res)
62
+ gc () { // TODO: support gc-ing till less than last block (required hypercore to support getting byteLength at arbitrary versions)
63
+ const bytesCleared = this.core.byteLength
64
+ const blocksCleared = this.core.length
65
+ this.record.bytesAllocated = this.core.byteLength - bytesCleared
66
+ this.record.blocksCleared = blocksCleared
67
+ this.record.bytesCleared = bytesCleared
68
+
69
+ if (this.downloadRange) this.downloadRange.destroy()
70
+ this.downloadRange = this.core.download({ start: this.record.blocksCleared, end: -1 })
71
+
72
+ this.core.clear(0, blocksCleared).catch(safetyCatch)
73
+ this.blindPeer.db.updateCore(this.record, this.id)
74
+
75
+ return bytesCleared
76
+ }
77
+
78
+ async refresh () {
79
+ await this.core.ready()
80
+ if (this.destroyed) return
81
+
82
+ this.id = this.core.id
83
+ this.channel = 'hypercore/alpha##' + b4a.toString(this.core.discoveryKey, 'hex')
84
+
85
+ const record = await this.blindPeer.db.get('@blind-peer/cores', { key: this.core.key })
86
+ if (this.destroyed || this.record || !record) return
87
+
88
+ this.record = record
89
+ this.core.download({ start: this.record.blocksCleared, end: -1 })
45
90
 
46
- return {
47
- autobase: res.autobase,
48
- writer: res.writer,
49
- open: !!res.blockEncryptionKey // TODO: get rid of the encryption for these guys with a manifest upgrade, then no attacks cause self-described
91
+ if (this.updated) this._onupdate()
92
+ if (this.activated) this._onactive()
93
+ }
94
+
95
+ announceToReferrer () {
96
+ if (!this.record || !this.record.referrer) return
97
+ if (!this.referrerDiscoveryKey) this.referrerDiscoveryKey = crypto.discoveryKey(this.record.referrer)
98
+
99
+ const sessions = this.blindPeer.wakeup.getSessions(null, { discoveryKey: this.referrerDiscoveryKey })
100
+ if (sessions.length === 0) return
101
+
102
+ const wakeup = [{ key: this.core.key, length: this.core.length }]
103
+
104
+ for (const s of sessions) {
105
+ for (const peer of s.peers) {
106
+ const mux = peer.stream.userData
107
+
108
+ // already replicating with that peer
109
+ if (mux._infos.get(this.channel)) {
110
+ continue
111
+ }
112
+
113
+ s.announceByStream(peer.stream, wakeup)
114
+ }
50
115
  }
51
116
  }
52
117
 
53
- async _onrpcpost (req) {
54
- this.emit('post-request', req)
55
- const res = await this.post(req)
56
- this.emit('post-response', req, res)
118
+ destroy () {
119
+ if (this.destroyed) return
120
+ this.destroyed = true
121
+ }
122
+ }
57
123
 
58
- return res
124
+ class WakeupHandler {
125
+ constructor (db, key, discoveryKey) {
126
+ this.db = db
127
+ this.key = key
128
+ this.discoveryKey = discoveryKey
129
+ this.active = false
59
130
  }
60
131
 
61
- async _oncoreopen (core) {
132
+ async onpeeractive (peer, session) {
133
+ const referrer = this.key
134
+ const query = {
135
+ gte: { referrer },
136
+ lte: { referrer },
137
+ reverse: true,
138
+ limit: 32
139
+ }
140
+
62
141
  try {
63
- const entry = await this.db.get('@blind-peer/mailbox', { autobase: core.key })
64
- if (!entry || !entry.blockEncryptionKey) return
142
+ const latest = await this.db.find('@blind-peer/cores-by-referrer', query).toArray()
143
+ if (peer.removed) return
144
+ session.announce(peer, latest)
145
+ } catch {
146
+ // do nothing
147
+ }
148
+ }
149
+ }
65
150
 
66
- const w = new AutobaseLightWriter(this.store.namespace(entry.autobase), entry.autobase, {
67
- active: false,
68
- blockEncryptionKey: entry.blockEncryptionKey
69
- })
151
+ class BlindPeer extends ReadyResource {
152
+ constructor (rocks, { swarm, store, wakeup, maxBytes = 100_000_000_000, enableGc = true, trustedPubKeys, port } = {}) {
153
+ super()
70
154
 
71
- for (const peer of core.peers) {
72
- w.local.replicate(peer.stream)
73
- }
155
+ this.rocks = typeof rocks === 'string' ? new RocksDB(rocks) : rocks
156
+ this.store = store || new Corestore(this.rocks, { active: false })
157
+ this.swarm = swarm || null
158
+ this._port = port || null
159
+ this.trustedPubKeys = new Set()
160
+ for (const k of trustedPubKeys || []) this.addTrustedPubKey(k)
74
161
 
75
- core.on('peer-add', (peer) => {
76
- w.local.replicate(peer.stream)
77
- })
162
+ this.wakeup = wakeup || new Wakeup(this._onwakeup.bind(this))
163
+ this.ownsWakeup = !wakeup
164
+ this.ownsSwarm = !swarm
165
+ this.ownsStore = !store
166
+ this.db = null
167
+ this.activeReplication = new Map()
168
+ this.activeWakeup = new Map()
169
+ this.flushInterval = null
170
+ this.maxBytes = maxBytes
171
+ this.enableGc = enableGc
172
+ this.lock = new ScopeLock({ debounce: true })
173
+ this.announcedCores = new Map()
78
174
 
79
- core.on('close', () => {
80
- w.close().catch(noop)
81
- })
82
- } catch (err) {
83
- console.log(err)
175
+ this.stats = {
176
+ bytesGcd: 0,
177
+ coresAdded: 0,
178
+ activations: 0,
179
+ wakeups: 0
84
180
  }
85
181
  }
86
182
 
183
+ get encryptionPublicKey () {
184
+ return this.db.encryptionKeyPair.publicKey
185
+ }
186
+
87
187
  get publicKey () {
88
- return this.swarm.server.publicKey
188
+ return this.swarm.keyPair.publicKey
89
189
  }
90
190
 
91
- async listen ({ bootstrap } = {}) {
92
- this.swarm = new Hyperswarm({
93
- keyPair: await this.store.createKeyPair('blind-mailbox'),
94
- bootstrap
95
- })
191
+ get digest () {
192
+ return this.db.digest
193
+ }
194
+
195
+ addTrustedPubKey (key) {
196
+ this.trustedPubKeys.add(IdEnc.normalize(key))
197
+ }
198
+
199
+ _isTrustedPeer (key) {
200
+ return this.trustedPubKeys.has(IdEnc.normalize(key))
201
+ }
202
+
203
+ async _open () {
204
+ await this.store.ready()
205
+ // legacy, we can remove once current ones are upgraded
206
+ const { secretKey } = await this.store.createKeyPair('blind-mirror-swarm')
207
+ this.db = new BlindPeerDB(this.rocks.session(), { swarming: secretKey.subarray(0, 32), encryption: null })
208
+ await this.db.ready()
209
+
210
+ if (this.swarm === null) {
211
+ const swarmOpts = { keyPair: this.db.swarmingKeyPair }
212
+ if (this._port) swarmOpts.port = this._port
213
+ this.swarm = new Hyperswarm(swarmOpts)
214
+ }
96
215
  this.swarm.on('connection', this._onconnection.bind(this))
216
+
217
+ const announceProms = []
218
+ for await (const record of this.db.createAnnouncingCoresStream()) {
219
+ announceProms.push(this._announceCore(record.key))
220
+ }
221
+ await Promise.all(announceProms)
222
+
223
+ this.store.watch(this._oncoreopen.bind(this))
224
+
225
+ this.flushInterval = setInterval(this.flush.bind(this), 10_000)
226
+ }
227
+
228
+ async _onwakeup (discoveryKey, muxer) {
229
+ this.stats.wakeups++
230
+
231
+ const auth = await this.store.storage.getAuth(discoveryKey)
232
+ if (!auth) return
233
+
234
+ const stream = muxer.stream
235
+ const handler = new WakeupHandler(this.db, auth.key, discoveryKey)
236
+ const w = this.wakeup.session(auth.key, handler)
237
+
238
+ if (w.getPeer(stream)) {
239
+ w.destroy()
240
+ return
241
+ }
242
+
243
+ w.addStream(stream)
244
+
245
+ for (const peer of w.peers) {
246
+ if (peer.active) handler.onpeeractive(peer, w)
247
+ }
248
+
249
+ stream.setMaxListeners(0)
250
+ stream.once('close', () => w.destroy())
251
+ }
252
+
253
+ async listen () {
254
+ if (!this.opened) await this.ready()
97
255
  return this.swarm.listen()
98
256
  }
99
257
 
100
- async get ({ autobase }) {
101
- return await this.db.get('@blind-peer/mailbox', { autobase })
258
+ needsGc () {
259
+ return this.digest.bytesAllocated >= this.maxBytes
102
260
  }
103
261
 
104
- async add ({ autobase, blockEncryptionKey = null }) {
105
- const prev = await this.db.get('@blind-peer/mailbox', { autobase })
262
+ async _gc () { // Do not call directly (assumes lock)
263
+ if (!this.needsGc()) return
106
264
 
107
- if (prev) {
108
- if (prev.blockEncryptionKey) return prev
109
- prev.blockEncryptionKey = blockEncryptionKey
110
- await this.db.insert('@blind-peer/mailbox', prev)
111
- await this.db.flush()
112
- return prev
265
+ const bytesToClear = this.digest.bytesAllocated - this.maxBytes
266
+ let bytesCleared = 0
267
+ this.emit('gc-start', { bytesToClear })
268
+
269
+ for await (const record of this.db.createGcCandidateReadStream()) {
270
+ if (this.closing) return
271
+ if (bytesCleared >= bytesToClear) break
272
+ if (record.bytesAllocated === 0) continue
273
+ if (record.announce) continue // We never clear these ATM, since we do no book keeping on the cleared length of announced cores
274
+
275
+ const { key } = record
276
+
277
+ // Explicitly opening the core ensures an active replication
278
+ // session exists
279
+ const core = this.store.get({ key })
280
+ await core.ready()
281
+ if (this.closing) return
282
+ const id = b4a.toString(core.discoveryKey, 'hex')
283
+
284
+ try {
285
+ const tracker = this.activeReplication.get(id)
286
+ const coreBytesCleared = tracker.gc()
287
+ bytesCleared += coreBytesCleared
288
+ } finally {
289
+ await core.close().catch(safetyCatch)
290
+ }
113
291
  }
114
292
 
115
- const w = new AutobaseLightWriter(this.store.namespace(autobase), autobase, { active: false })
116
- await w.ready()
117
- const entry = { autobase, writer: w.local.key, blockEncryptionKey }
118
- await this.db.insert('@blind-peer/mailbox', entry)
119
293
  await this.db.flush()
120
- await w.close()
294
+ if (this.closing) return
295
+ this.stats.bytesGcd += bytesCleared
296
+ this.emit('gc-done', { bytesCleared })
297
+ }
298
+
299
+ _onreferrerupdates (updates) {
300
+ const pending = new Set()
301
+
302
+ for (const u of updates) {
303
+ const id = b4a.toString(u.referrer, 'hex')
304
+ const w = this.activeWakeup.get(id)
305
+ if (!w) continue
121
306
 
122
- return entry
307
+ w.queued.push(u)
308
+ pending.add(w)
309
+ }
310
+
311
+ for (const w of pending) {
312
+ w.flush()
313
+ }
314
+ }
315
+
316
+ _oncoreopen (core) {
317
+ const session = new Hypercore({ core, weak: true })
318
+ const id = b4a.toString(core.discoveryKey, 'hex')
319
+ const tracker = new CoreTracker(this, session)
320
+
321
+ this.activeReplication.set(id, tracker)
322
+ tracker.refresh().catch(safetyCatch)
323
+
324
+ session.on('close', () => {
325
+ tracker.destroy()
326
+ if (this.activeReplication.get(id) === tracker) {
327
+ this.activeReplication.delete(id)
328
+ }
329
+ })
123
330
  }
124
331
 
125
- async post ({ autobase, message }) {
126
- const entry = await this.db.get('@blind-peer/mailbox', { autobase })
127
- if (!entry || !entry.blockEncryptionKey) return false
332
+ async flush () { // not allowed to throw
333
+ if (!(await this.lock.lock())) return
334
+ try {
335
+ if (this.enableGc && this.needsGc()) await this._gc()
336
+ if (this.db.updated()) await this.db.flush()
337
+ } catch (e) {
338
+ this.emit('flush-error', e)
339
+ safetyCatch(e)
340
+ } finally {
341
+ this.lock.unlock()
342
+ }
343
+ }
344
+
345
+ _onconnection (conn) {
346
+ if (this.closing) {
347
+ conn.destroy()
348
+ return
349
+ }
128
350
 
129
- const w = new AutobaseLightWriter(this.store.namespace(autobase), autobase, {
130
- active: false,
131
- blockEncryptionKey: entry.blockEncryptionKey
351
+ if (this.ownsStore) this.store.replicate(conn)
352
+ if (this.ownsWakeup) this.wakeup.addStream(conn)
353
+
354
+ const rpc = new ProtomuxRPC(conn, {
355
+ id: this.swarm.keyPair.publicKey,
356
+ valueEncoding: c.none
132
357
  })
133
- await w.append(message)
134
- const length = w.local.length
135
- await w.close()
136
358
 
137
- return { length }
359
+ rpc.respond('add-core', AddCoreEncoding, this._onaddcore.bind(this, conn))
138
360
  }
139
361
 
140
- async close () {
141
- if (this.swarm !== null) await this.swarm.destroy()
142
- await this.store.close()
362
+ async _activateCore (stream, record) {
363
+ this.stats.activations++
364
+
365
+ const core = this.store.get({ key: record.key })
366
+ await core.ready()
367
+
368
+ const tracker = this.activeReplication.get(b4a.toString(core.discoveryKey, 'hex'))
369
+ if (tracker && !tracker.record) await tracker.refresh()
370
+
371
+ if (record.announce) {
372
+ await this._announceCore(core.key)
373
+ }
374
+
375
+ if (stream.destroying) {
376
+ await core.close()
377
+ return
378
+ }
379
+
380
+ core.replicate(stream)
381
+ stream.on('close', () => core.close().catch(safetyCatch))
382
+ }
383
+
384
+ async _announceCore (key) {
385
+ const coreId = IdEnc.normalize(key)
386
+ if (this.announcedCores.has(coreId)) return
387
+
388
+ const core = this.store.get({ key })
389
+ this.announcedCores.set(coreId, core)
390
+
391
+ core.on('append', () => {
392
+ this.emit('core-append', core)
393
+ })
394
+ core.on('download', () => {
395
+ if (core.length === core.contiguousLength) {
396
+ this.emit('core-downloaded', core)
397
+ }
398
+ })
399
+
400
+ await core.ready()
401
+ this.swarm.join(core.discoveryKey, { server: true, client: false })
402
+
403
+ // WARNING: we do not yet handle the case where
404
+ // data of an announced core is cleared
405
+ core.download({ start: 0, end: -1 })
406
+
407
+ this.emit('announce-core', core)
408
+ }
409
+
410
+ async _onaddcore (stream, record) {
411
+ if (!this.opened) await this.ready()
412
+
413
+ record.priority = Math.min(record.priority, 1) // 2 is reserved for trusted peers
414
+ if (record.announce !== false && !this._isTrustedPeer(stream.remotePublicKey)) {
415
+ this.emit('downgrade-announce', { record, remotePublicKey: stream.remotePublicKey })
416
+ record.announce = false
417
+ }
418
+
419
+ this.db.addCore(record)
420
+ await this.flush() // flush now as important data
421
+
422
+ if (record.referrer) {
423
+ // ensure referrer is allocated...
424
+ // TODO: move to a dedicated wakeup collection, insted of using a core since we moved away from that
425
+ // still works atm, cause dkey
426
+ const muxer = stream.userData
427
+ const core = this.store.get({ key: record.referrer })
428
+ await core.ready()
429
+ const discoveryKey = core.discoveryKey
430
+ await core.close()
431
+
432
+ await this._onwakeup(discoveryKey, muxer)
433
+ }
434
+
435
+ this.stats.coresAdded++
436
+ this.emit('add-core', record, true)
437
+
438
+ await this._activateCore(stream, record)
439
+
440
+ const coreRecord = await this.db.getCoreRecord(record.key)
441
+ return coreRecord
442
+ }
443
+
444
+ async _close () {
445
+ clearInterval(this.flushInterval)
446
+ if (this.ownsWakeup) this.wakeup.destroy()
447
+ if (this.ownsSwarm) await this.swarm.destroy()
448
+ await this.flush()
449
+ await this.db.close()
450
+ if (this.ownsStore) await this.store.close()
451
+ await this.rocks.close()
452
+ }
453
+
454
+ registerMetrics (promClient) {
455
+ const self = this
456
+ new promClient.Gauge({ // eslint-disable-line no-new
457
+ name: 'blind_peer_bytes_allocated',
458
+ help: 'The amount of bytes allocated by the hyperdb (as reported in its digest)',
459
+ collect () {
460
+ this.set(self.digest.bytesAllocated)
461
+ }
462
+ })
463
+
464
+ new promClient.Gauge({ // eslint-disable-line no-new
465
+ name: 'blind_peer_cores',
466
+ help: 'The amount of cores (as reported in its digest)',
467
+ collect () {
468
+ this.set(self.digest.cores)
469
+ }
470
+ })
471
+
472
+ new promClient.Gauge({ // eslint-disable-line no-new
473
+ name: 'blind_peer_cores_added',
474
+ help: 'The total amount of add-core RPC requests that have been processed',
475
+ collect () {
476
+ this.set(self.stats.coresAdded)
477
+ }
478
+ })
479
+
480
+ new promClient.Gauge({ // eslint-disable-line no-new
481
+ name: 'blind_peer_bytes_gcd',
482
+ help: 'The total amount of bytes garbage collected since the process started',
483
+ collect () {
484
+ this.set(self.stats.bytesGcd)
485
+ }
486
+ })
487
+
488
+ new promClient.Gauge({ // eslint-disable-line no-new
489
+ name: 'blind_peer_core_activations',
490
+ help: 'The total amount of hypercore activations since the process started',
491
+ collect () {
492
+ this.set(self.stats.activations)
493
+ }
494
+ })
495
+
496
+ new promClient.Gauge({ // eslint-disable-line no-new
497
+ name: 'blind_peer_wakeups',
498
+ help: 'The total amount of hypercore wakeups since the process started',
499
+ collect () {
500
+ this.set(self.stats.wakeups)
501
+ }
502
+ })
503
+
504
+ new promClient.Gauge({ // eslint-disable-line no-new
505
+ name: 'blind_peer_db_flushes',
506
+ help: 'The total amount of database flushes since the process started',
507
+ collect () {
508
+ this.set(self.db.stats.flushes)
509
+ }
510
+ })
143
511
  }
144
512
  }
145
513
 
146
- function noop () {}
514
+ module.exports = BlindPeer