blind-peer 0.0.3 → 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,137 +1,475 @@
1
- const AutobaseLightWriter = require('autobase-light-writer')
2
- const HyperDB = require('hyperdb')
3
1
  const Corestore = require('corestore')
4
- const definition = require('./spec/hyperdb')
5
- const schema = require('./spec/hyperschema')
6
- const path = require('path')
2
+ const Hypercore = require('hypercore')
3
+ const RocksDB = require('rocksdb-native')
4
+ const ReadyResource = require('ready-resource')
7
5
  const Hyperswarm = require('hyperswarm')
8
6
  const ProtomuxRPC = require('protomux-rpc')
9
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')
10
14
 
11
- module.exports = class BlindPeer {
12
- constructor (storage) {
13
- this.db = HyperDB.rocks(path.join(storage, 'hyperdb'), definition)
14
- this.store = new Corestore(path.join(storage, 'corestore'))
15
- this.store.on('core-open', this._oncoreopen.bind(this))
16
- this.swarm = null
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)
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)
17
40
  }
18
41
 
19
- _onconnection (connection) {
20
- this.store.replicate(connection)
42
+ _onupdate () {
43
+ this.updated = true
44
+ if (!this.record) return
21
45
 
22
- const rpc = new ProtomuxRPC(connection, {
23
- id: this.swarm.keyPair.publicKey,
24
- valueEncoding: c.none
25
- })
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)
26
74
 
27
- rpc.respond('add-mailbox', {
28
- requestEncoding: schema.resolveStruct('@blind-peer/request-mailbox'),
29
- responseEncoding: schema.resolveStruct('@blind-peer/response-mailbox')
30
- }, 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
31
87
 
32
- rpc.respond('post', {
33
- requestEncoding: schema.resolveStruct('@blind-peer/request-post'),
34
- responseEncoding: schema.resolveStruct('@blind-peer/response-post')
35
- }, 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()
36
93
  }
37
94
 
38
- async _onrpcadd (req) {
39
- const res = await this.add(req)
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
40
107
 
41
- return {
42
- autobase: res.autobase,
43
- writer: res.writer,
44
- 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
+ }
45
115
  }
46
116
  }
47
117
 
48
- async _onrpcpost (req) {
49
- return await this.post(req)
118
+ destroy () {
119
+ if (this.destroyed) return
120
+ this.destroyed = true
121
+ }
122
+ }
123
+
124
+ class WakeupHandler {
125
+ constructor (db, key, discoveryKey) {
126
+ this.db = db
127
+ this.key = key
128
+ this.discoveryKey = discoveryKey
129
+ this.active = false
50
130
  }
51
131
 
52
- 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
+
53
141
  try {
54
- const entry = await this.db.get('@blind-peer/mailbox', { autobase: core.key })
55
- 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
+ }
56
150
 
57
- const w = new AutobaseLightWriter(this.store.namespace(entry.autobase), entry.autobase, {
58
- active: false,
59
- blockEncryptionKey: entry.blockEncryptionKey
60
- })
151
+ class BlindPeer extends ReadyResource {
152
+ constructor (rocks, { swarm, store, wakeup, maxBytes = 100_000_000_000, enableGc = true, trustedPubKeys, port } = {}) {
153
+ super()
61
154
 
62
- for (const peer of core.peers) {
63
- w.local.replicate(peer.stream)
64
- }
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)
65
161
 
66
- core.on('peer-add', (peer) => {
67
- w.local.replicate(peer.stream)
68
- })
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()
69
174
 
70
- core.on('close', () => {
71
- w.close().catch(noop)
72
- })
73
- } catch (err) {
74
- console.log(err)
175
+ this.stats = {
176
+ bytesGcd: 0,
177
+ coresAdded: 0
75
178
  }
76
179
  }
77
180
 
181
+ get encryptionPublicKey () {
182
+ return this.db.encryptionKeyPair.publicKey
183
+ }
184
+
78
185
  get publicKey () {
79
- return this.swarm.server.publicKey
186
+ return this.swarm.keyPair.publicKey
80
187
  }
81
188
 
82
- async listen ({ bootstrap } = {}) {
83
- this.swarm = new Hyperswarm({
84
- keyPair: await this.store.createKeyPair('blind-mailbox'),
85
- bootstrap
86
- })
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
+ }
87
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()
88
251
  return this.swarm.listen()
89
252
  }
90
253
 
91
- async get ({ autobase }) {
92
- return await this.db.get('@blind-peer/mailbox', { autobase })
254
+ needsGc () {
255
+ return this.digest.bytesAllocated >= this.maxBytes
93
256
  }
94
257
 
95
- async add ({ autobase, blockEncryptionKey = null }) {
96
- 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
97
270
 
98
- if (prev) {
99
- if (prev.blockEncryptionKey) return prev
100
- prev.blockEncryptionKey = blockEncryptionKey
101
- await this.db.insert('@blind-peer/mailbox', prev)
102
- await this.db.flush()
103
- 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
+ }
104
287
  }
105
288
 
106
- const w = new AutobaseLightWriter(this.store.namespace(autobase), autobase, { active: false })
107
- await w.ready()
108
- const entry = { autobase, writer: w.local.key, blockEncryptionKey }
109
- await this.db.insert('@blind-peer/mailbox', entry)
110
289
  await this.db.flush()
111
- 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()
112
360
 
113
- 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))
114
375
  }
115
376
 
116
- async post ({ autobase, message }) {
117
- const entry = await this.db.get('@blind-peer/mailbox', { autobase })
118
- 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)
119
383
 
120
- const w = new AutobaseLightWriter(this.store.namespace(autobase), autobase, {
121
- active: false,
122
- 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
+ }
123
391
  })
124
- await w.append(message)
125
- const length = w.local.length
126
- await w.close()
127
392
 
128
- 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)
129
401
  }
130
402
 
131
- async close () {
132
- if (this.swarm !== null) await this.swarm.destroy()
133
- 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
+ })
134
472
  }
135
473
  }
136
474
 
137
- function noop () {}
475
+ module.exports = BlindPeer