blind-peer 0.0.4 → 2.7.4

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,475 @@
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)
32
+
33
+ const onupdate = this._onupdate.bind(this)
34
+ const onactive = this._onactive.bind(this)
15
35
 
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
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
54
+
55
+ if (this.record) {
56
+ this.blindPeer.db.updateCore(this.record, this.id)
57
+ }
58
+
59
+ this.blindPeer.emit('core-activity', this.core, this.record)
60
+ }
61
+
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)
29
74
 
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))
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
34
87
 
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))
88
+ this.record = record
89
+ this.core.download({ start: this.record.blocksCleared, end: -1 })
90
+
91
+ if (this.updated) this._onupdate()
92
+ if (this.activated) this._onactive()
39
93
  }
40
94
 
41
- async _onrpcadd (req) {
42
- this.emit('add-request', req)
43
- const res = await this.add(req)
44
- this.emit('add-response', req, res)
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
45
107
 
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
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
84
178
  }
85
179
  }
86
180
 
181
+ get encryptionPublicKey () {
182
+ return this.db.encryptionKeyPair.publicKey
183
+ }
184
+
87
185
  get publicKey () {
88
- return this.swarm.server.publicKey
186
+ return this.swarm.keyPair.publicKey
89
187
  }
90
188
 
91
- async listen ({ bootstrap } = {}) {
92
- this.swarm = new Hyperswarm({
93
- keyPair: await this.store.createKeyPair('blind-mailbox'),
94
- bootstrap
95
- })
189
+ get digest () {
190
+ return this.db.digest
191
+ }
192
+
193
+ addTrustedPubKey (key) {
194
+ this.trustedPubKeys.add(IdEnc.normalize(key))
195
+ }
196
+
197
+ _isTrustedPeer (key) {
198
+ return this.trustedPubKeys.has(IdEnc.normalize(key))
199
+ }
200
+
201
+ async _open () {
202
+ await this.store.ready()
203
+ // legacy, we can remove once current ones are upgraded
204
+ const { secretKey } = await this.store.createKeyPair('blind-mirror-swarm')
205
+ this.db = new BlindPeerDB(this.rocks.session(), { swarming: secretKey.subarray(0, 32), encryption: null })
206
+ await this.db.ready()
207
+
208
+ if (this.swarm === null) {
209
+ const swarmOpts = { keyPair: this.db.swarmingKeyPair }
210
+ if (this._port) swarmOpts.port = this._port
211
+ this.swarm = new Hyperswarm(swarmOpts)
212
+ }
96
213
  this.swarm.on('connection', this._onconnection.bind(this))
214
+
215
+ const announceProms = []
216
+ for await (const record of this.db.createAnnouncingCoresStream()) {
217
+ announceProms.push(this._announceCore(record.key))
218
+ }
219
+ await Promise.all(announceProms)
220
+
221
+ this.store.watch(this._oncoreopen.bind(this))
222
+
223
+ this.flushInterval = setInterval(this.flush.bind(this), 10_000)
224
+ }
225
+
226
+ async _onwakeup (discoveryKey, muxer) {
227
+ const auth = await this.store.storage.getAuth(discoveryKey)
228
+ if (!auth) return
229
+
230
+ const stream = muxer.stream
231
+ const handler = new WakeupHandler(this.db, auth.key, discoveryKey)
232
+ const w = this.wakeup.session(auth.key, handler)
233
+
234
+ if (w.getPeer(stream)) {
235
+ w.destroy()
236
+ return
237
+ }
238
+
239
+ w.addStream(stream)
240
+
241
+ for (const peer of w.peers) {
242
+ if (peer.active) handler.onpeeractive(peer, w)
243
+ }
244
+
245
+ stream.setMaxListeners(0)
246
+ stream.once('close', () => w.destroy())
247
+ }
248
+
249
+ async listen () {
250
+ if (!this.opened) await this.ready()
97
251
  return this.swarm.listen()
98
252
  }
99
253
 
100
- async get ({ autobase }) {
101
- return await this.db.get('@blind-peer/mailbox', { autobase })
254
+ needsGc () {
255
+ return this.digest.bytesAllocated >= this.maxBytes
102
256
  }
103
257
 
104
- async add ({ autobase, blockEncryptionKey = null }) {
105
- const prev = await this.db.get('@blind-peer/mailbox', { autobase })
258
+ async _gc () { // Do not call directly (assumes lock)
259
+ if (!this.needsGc()) return
260
+
261
+ const bytesToClear = this.digest.bytesAllocated - this.maxBytes
262
+ let bytesCleared = 0
263
+ this.emit('gc-start', { bytesToClear })
264
+
265
+ for await (const record of this.db.createGcCandidateReadStream()) {
266
+ if (this.closing) return
267
+ if (bytesCleared >= bytesToClear) break
268
+ if (record.bytesAllocated === 0) continue
269
+ if (record.announce) continue // We never clear these ATM, since we do no book keeping on the cleared length of announced cores
106
270
 
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
271
+ const { key } = record
272
+
273
+ // Explicitly opening the core ensures an active replication
274
+ // session exists
275
+ const core = this.store.get({ key })
276
+ await core.ready()
277
+ if (this.closing) return
278
+ const id = b4a.toString(core.discoveryKey, 'hex')
279
+
280
+ try {
281
+ const tracker = this.activeReplication.get(id)
282
+ const coreBytesCleared = tracker.gc()
283
+ bytesCleared += coreBytesCleared
284
+ } finally {
285
+ await core.close().catch(safetyCatch)
286
+ }
113
287
  }
114
288
 
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
289
  await this.db.flush()
120
- await w.close()
290
+ if (this.closing) return
291
+ this.stats.bytesGcd += bytesCleared
292
+ this.emit('gc-done', { bytesCleared })
293
+ }
294
+
295
+ _onreferrerupdates (updates) {
296
+ const pending = new Set()
297
+
298
+ for (const u of updates) {
299
+ const id = b4a.toString(u.referrer, 'hex')
300
+ const w = this.activeWakeup.get(id)
301
+ if (!w) continue
302
+
303
+ w.queued.push(u)
304
+ pending.add(w)
305
+ }
306
+
307
+ for (const w of pending) {
308
+ w.flush()
309
+ }
310
+ }
311
+
312
+ _oncoreopen (core) {
313
+ const session = new Hypercore({ core, weak: true })
314
+ const id = b4a.toString(core.discoveryKey, 'hex')
315
+ const tracker = new CoreTracker(this, session)
316
+
317
+ this.activeReplication.set(id, tracker)
318
+ tracker.refresh().catch(safetyCatch)
319
+
320
+ session.on('close', () => {
321
+ tracker.destroy()
322
+ if (this.activeReplication.get(id) === tracker) {
323
+ this.activeReplication.delete(id)
324
+ }
325
+ })
326
+ }
327
+
328
+ async flush () { // not allowed to throw
329
+ if (!(await this.lock.lock())) return
330
+ try {
331
+ if (this.enableGc && this.needsGc()) await this._gc()
332
+ if (this.db.updated()) await this.db.flush()
333
+ } catch (e) {
334
+ safetyCatch(e)
335
+ } finally {
336
+ this.lock.unlock()
337
+ }
338
+ }
339
+
340
+ _onconnection (conn) {
341
+ if (this.closing) {
342
+ conn.destroy()
343
+ return
344
+ }
345
+
346
+ if (this.ownsStore) this.store.replicate(conn)
347
+ if (this.ownsWakeup) this.wakeup.addStream(conn)
348
+
349
+ const rpc = new ProtomuxRPC(conn, {
350
+ id: this.swarm.keyPair.publicKey,
351
+ valueEncoding: c.none
352
+ })
353
+
354
+ rpc.respond('add-core', AddCoreEncoding, this._onaddcore.bind(this, conn))
355
+ }
356
+
357
+ async _activateCore (stream, record) {
358
+ const core = this.store.get({ key: record.key })
359
+ await core.ready()
121
360
 
122
- return entry
361
+ const tracker = this.activeReplication.get(b4a.toString(core.discoveryKey, 'hex'))
362
+ if (tracker && !tracker.record) await tracker.refresh()
363
+
364
+ if (record.announce) {
365
+ await this._announceCore(core.key)
366
+ }
367
+
368
+ if (stream.destroying) {
369
+ await core.close()
370
+ return
371
+ }
372
+
373
+ core.replicate(stream)
374
+ stream.on('close', () => core.close().catch(safetyCatch))
123
375
  }
124
376
 
125
- async post ({ autobase, message }) {
126
- const entry = await this.db.get('@blind-peer/mailbox', { autobase })
127
- if (!entry || !entry.blockEncryptionKey) return false
377
+ async _announceCore (key) {
378
+ const coreId = IdEnc.normalize(key)
379
+ if (this.announcedCores.has(coreId)) return
380
+
381
+ const core = this.store.get({ key })
382
+ this.announcedCores.set(coreId, core)
128
383
 
129
- const w = new AutobaseLightWriter(this.store.namespace(autobase), autobase, {
130
- active: false,
131
- blockEncryptionKey: entry.blockEncryptionKey
384
+ core.on('append', () => {
385
+ this.emit('core-append', core)
386
+ })
387
+ core.on('download', () => {
388
+ if (core.length === core.contiguousLength) {
389
+ this.emit('core-downloaded', core)
390
+ }
132
391
  })
133
- await w.append(message)
134
- const length = w.local.length
135
- await w.close()
136
392
 
137
- return { length }
393
+ await core.ready()
394
+ this.swarm.join(core.discoveryKey, { server: true, client: false })
395
+
396
+ // WARNING: we do not yet handle the case where
397
+ // data of an announced core is cleared
398
+ core.download({ start: 0, end: -1 })
399
+
400
+ this.emit('announce-core', core)
138
401
  }
139
402
 
140
- async close () {
141
- if (this.swarm !== null) await this.swarm.destroy()
142
- await this.store.close()
403
+ async _onaddcore (stream, record) {
404
+ if (!this.opened) await this.ready()
405
+
406
+ record.priority = Math.min(record.priority, 1) // 2 is reserved for trusted peers
407
+ if (record.announce !== false && !this._isTrustedPeer(stream.remotePublicKey)) {
408
+ this.emit('downgrade-announce', { record, remotePublicKey: stream.remotePublicKey })
409
+ record.announce = false
410
+ }
411
+
412
+ this.db.addCore(record)
413
+ await this.flush() // flush now as important data
414
+
415
+ if (record.referrer) {
416
+ // ensure referrer is allocated...
417
+ // TODO: move to a dedicated wakeup collection, insted of using a core since we moved away from that
418
+ // still works atm, cause dkey
419
+ const muxer = stream.userData
420
+ const core = this.store.get({ key: record.referrer })
421
+ await core.ready()
422
+ const discoveryKey = core.discoveryKey
423
+ await core.close()
424
+
425
+ await this._onwakeup(discoveryKey, muxer)
426
+ }
427
+
428
+ this.stats.coresAdded++
429
+ this.emit('add-core', record, true)
430
+
431
+ await this._activateCore(stream, record)
432
+
433
+ const coreRecord = await this.db.getCoreRecord(record.key)
434
+ return coreRecord
435
+ }
436
+
437
+ async _close () {
438
+ clearInterval(this.flushInterval)
439
+ if (this.ownsWakeup) this.wakeup.destroy()
440
+ if (this.ownsSwarm) await this.swarm.destroy()
441
+ await this.flush()
442
+ await this.db.close()
443
+ if (this.ownsStore) await this.store.close()
444
+ await this.rocks.close()
445
+ }
446
+
447
+ registerMetrics (promClient) {
448
+ const self = this
449
+ new promClient.Gauge({ // eslint-disable-line no-new
450
+ name: 'blind_peer_bytes_allocated',
451
+ help: 'The amount of bytes allocated by the hyperdb (as reported in its digest)',
452
+ collect () {
453
+ this.set(self.digest.bytesAllocated)
454
+ }
455
+ })
456
+
457
+ new promClient.Gauge({ // eslint-disable-line no-new
458
+ name: 'blind_peer_cores_added',
459
+ help: 'The total amount of add-core RPC requests that have been processed',
460
+ collect () {
461
+ this.set(self.stats.coresAdded)
462
+ }
463
+ })
464
+
465
+ new promClient.Gauge({ // eslint-disable-line no-new
466
+ name: 'blind_peer_bytes_gcd',
467
+ help: 'The total amount of bytes garbage collected since the process started',
468
+ collect () {
469
+ this.set(self.stats.bytesGcd)
470
+ }
471
+ })
143
472
  }
144
473
  }
145
474
 
146
- function noop () {}
475
+ module.exports = BlindPeer