autobee 2.0.0-rc.22 → 2.0.0-rc.25

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
@@ -22,6 +22,7 @@ const { ActiveWriters } = require('./lib/writers.js')
22
22
  const TrustedPeers = require('./lib/trusted.js')
23
23
  const ApplyView = require('./lib/apply-view.js')
24
24
  const UpdateChanges = require('./lib/updates.js')
25
+ const migrations = require('./lib/migrations.js')
25
26
 
26
27
  const EMPTY_HEAD = { length: 0, key: null }
27
28
  const INTERRUPT = new Error('Apply interrupted')
@@ -78,13 +79,16 @@ module.exports = class Autobee extends ReadyResource {
78
79
  this.writers = null
79
80
  this.bumping = 0
80
81
 
81
- const fastForward = handlers.fastForward || {}
82
+ // fastForward: false disables all fast-forwards - both wakeup and boot
83
+ const fastForward = handlers.fastForward === false ? null : handlers.fastForward || {}
84
+
85
+ this._ffEnabled = fastForward !== null
82
86
 
83
87
  // oplog head to boot from: migrates or fast-forwards depending on its version
84
- this.bootFrom = fastForward.boot || null
88
+ this.bootFrom = (fastForward && fastForward.boot) || null
85
89
 
86
90
  // conservative (default on): only fast-forward onto a head someone can serve whole
87
- this._conservativeFF = fastForward.conservative !== false
91
+ this._conservativeFF = !fastForward || fastForward.conservative !== false
88
92
 
89
93
  this.trusted = new TrustedPeers(handlers)
90
94
 
@@ -112,6 +116,8 @@ module.exports = class Autobee extends ReadyResource {
112
116
  this._bootingAll = null
113
117
 
114
118
  this._now = handlers.now || Date.now // overridable for clock-drift tests
119
+ this._preapply = handlers.preapply || null
120
+ this._preApplied = false
115
121
  this._hasApply = !!handlers.apply
116
122
  this._hasUpdate = !!handlers.update
117
123
  this._needsUpdate = false
@@ -504,9 +510,20 @@ module.exports = class Autobee extends ReadyResource {
504
510
  this.emit('error', err)
505
511
  }
506
512
 
513
+ // one-shot user gate: nothing applies until the host has resolved whatever
514
+ // state apply depends on (e.g. legacy views recorded by a migration)
515
+ async _runPreApply() {
516
+ if (this._preapply === null || this._preApplied) return
517
+
518
+ this._preApplied = true
519
+ await this._preapply(this.view)
520
+ }
521
+
507
522
  async _drain() {
508
523
  if (this._updating) await this._updating
509
524
 
525
+ await this._runPreApply()
526
+
510
527
  this.stats.drains++
511
528
 
512
529
  if (this.bootFrom) {
@@ -626,6 +643,8 @@ module.exports = class Autobee extends ReadyResource {
626
643
  const hints = await this._applyWakeupHints()
627
644
  if (!hints.size) return
628
645
 
646
+ if (!this._ffEnabled) return
647
+
629
648
  // a scheduled fast-forward is applied by the drain before we look again
630
649
  if (this.fastForwardTo !== null || this.fastForwarding !== null) return
631
650
  if (this._interrupting || this.closing || this.bootFrom) return
@@ -700,7 +719,7 @@ module.exports = class Autobee extends ReadyResource {
700
719
  // legacy nodes always inflate, but only indexers carry views - callers
701
720
  // that need system info must check op.views
702
721
  if (op.version < 3) {
703
- op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
722
+ op = await migrations.inflateLegacyOplog(buf, core, target - 1, opts)
704
723
  }
705
724
 
706
725
  return {
@@ -710,49 +729,6 @@ module.exports = class Autobee extends ReadyResource {
710
729
  }
711
730
  }
712
731
 
713
- async _inflateLegacyOplog(buf, core, seq, opts = null) {
714
- const m = encoding.decodeRawOplog(buf)
715
-
716
- const op = {
717
- version: m.version,
718
- timestamp: 0,
719
- links: m.node.heads,
720
- batch: { start: 0, end: m.node.batch - 1 },
721
- views: null,
722
- optimistic: !!m.optimistic,
723
- value: m.node.value
724
- }
725
-
726
- if (m.digest === null || m.checkpoint === null || !m.checkpoint.system) return op
727
-
728
- const fetches = []
729
-
730
- fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, opts) : buf)
731
- fetches.push(
732
- m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer, opts) : buf
733
- )
734
-
735
- const [digestNode, checkpointNode] = await Promise.all(fetches)
736
- // a wait: false get we could not satisfy locally
737
- if (digestNode === null || checkpointNode === null) return op
738
-
739
- const { digest } = encoding.decodeRawOplog(digestNode)
740
- const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
741
-
742
- if (!checkpoint.system || !checkpoint.system.checkpoint) return op
743
-
744
- op.views = {
745
- system: {
746
- key: digest.key,
747
- start: 0,
748
- length: checkpoint.system.checkpoint.length
749
- },
750
- flushes: seq
751
- }
752
-
753
- return op
754
- }
755
-
756
732
  async _update(changes) {
757
733
  this._needsUpdate = false
758
734
  this.bee.update(this._workingBee.root)
package/lib/boot.js CHANGED
@@ -1,20 +1,5 @@
1
- const c = require('compact-encoding')
2
- const b4a = require('b4a')
3
- const crypto = require('hypercore-crypto')
4
- const { AutobeeEncryption } = require('autobee-encryption')
5
- const { decodeBlock } = require('hyperbee2/lib/encoding.js')
6
-
7
1
  const encoding = require('./encoding.js')
8
- const { assert, bail } = require('./asserts.js')
9
-
10
- const { getEncoding } = require('../encoding/spec/autobee')
11
-
12
- const SystemInfoV1 = getEncoding('@autobase-compat/info-v1')
13
- const SystemInfoV2 = getEncoding('@autobase-compat/info-v2')
14
-
15
- const EMPTY = b4a.alloc(0)
16
- const INDEX_VERSION = 1
17
- const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
2
+ const { checkAutobaseMigration } = require('./migrations.js')
18
3
 
19
4
  module.exports = async function boot(
20
5
  corestore,
@@ -153,64 +138,6 @@ module.exports = async function boot(
153
138
  return result
154
139
  }
155
140
 
156
- async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
157
- const bootRecord = await local.getUserData('autobase/boot')
158
- if (!bootRecord) return null
159
-
160
- const { key } = encoding.decodeAutobaseBootRecord(bootRecord)
161
- const core = store.get({ key: key, encryption: null })
162
- await core.ready()
163
-
164
- const system = { key, length: core.length }
165
-
166
- // setup encryption
167
- const encryptionKey = await local.getUserData('autobase/encryption')
168
- await AutobeeEncryption.setSystemEncryption(bootstrap.key, encryptionKey, core)
169
-
170
- const catchup = await getCatchupHeads(core)
171
- const nodes = await Promise.all(
172
- catchup.map((n) => getWriterBatch(store, n, bootstrap.key, encryptionKey))
173
- )
174
-
175
- // Decode hyperbee block using hyperbee2 compat
176
- const node = decodeBlock(await core.get(core.length - 1))
177
- assert(node.keys.length > 0, 'bad system block')
178
-
179
- // Decode system info from hyperbee block using autobee compat
180
- const info = decodeLegacySystemInfo(node.keys[0].value)
181
-
182
- const indexerManifests = await Promise.all(
183
- info.indexers.map((idx) => getCoreManifest(store, idx.key))
184
- )
185
- const views = await Promise.all(info.views.map((v) => getPersistedView(store, v)))
186
- const entropy =
187
- info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
188
-
189
- const mappedViews = new Map()
190
- for (const name of legacyViews) {
191
- const v = await findViewByName(
192
- store,
193
- bootstrap.key,
194
- encryptionKey,
195
- indexerManifests,
196
- views,
197
- entropy,
198
- name
199
- )
200
-
201
- mappedViews.set(name, v)
202
- }
203
-
204
- await core.close()
205
-
206
- return {
207
- encryptionKey,
208
- system,
209
- views: mappedViews,
210
- catchup: nodes
211
- }
212
- }
213
-
214
141
  async function getLocalKey(bootstrap) {
215
142
  const [legacy, current] = await Promise.all([
216
143
  bootstrap.getUserData('autobase/local'),
@@ -220,121 +147,3 @@ async function getLocalKey(bootstrap) {
220
147
  if (current) return current
221
148
  return legacy
222
149
  }
223
-
224
- async function getPersistedView(store, view) {
225
- const core = store.get(view.key)
226
- await core.ready()
227
-
228
- const length = core.length
229
- await core.close()
230
-
231
- return { key: view.key, length }
232
- }
233
-
234
- async function getCatchupHeads(core) {
235
- const session = core.session({ name: 'batch' })
236
- await session.ready()
237
-
238
- const nodes = []
239
- for (let i = core.length; i < session.length; i++) {
240
- const node = await session.get(i, { wait: false })
241
- if (!node) throw new Error('Expect nodes to exist locally')
242
-
243
- const entry = decodeBlock(node).keys[0]
244
- if (entry.key[0] !== 0x01) continue
245
-
246
- const { key, length } = encoding.decodeSystemWriter(entry.key, entry.value)
247
- if (!length) continue
248
- nodes.push({ key, length })
249
- }
250
-
251
- return nodes
252
- }
253
-
254
- async function getWriterBatch(store, head, key, encryptionKey, nodes = []) {
255
- const batch = []
256
-
257
- const core = store.get(head.key)
258
- await core.ready()
259
-
260
- if (encryptionKey) {
261
- await core.setEncryption(AutobeeEncryption.getWriterEncryption(key, encryptionKey))
262
- }
263
-
264
- let seq = head.length - 1
265
- const block = await core.get(seq--, { wait: false })
266
- if (!block) throw new Error('Expect writer node to exist locally')
267
-
268
- const node = encoding.decodeOplog(block)
269
-
270
- batch.unshift({ ...head, ...node, from: core })
271
-
272
- while (seq >= 0) {
273
- const block = await core.get(seq--, { wait: false })
274
- if (!block) break
275
-
276
- const node = encoding.decodeOplog(block)
277
- if (!node.batch.end) break
278
-
279
- batch.unshift({ key: head.key, length: seq + 2, from: core, ...node })
280
- }
281
-
282
- return batch
283
- }
284
-
285
- async function getCoreManifest(store, key) {
286
- const core = store.get(key)
287
- await core.ready()
288
-
289
- const manifest = core.manifest
290
- await core.close()
291
-
292
- return manifest
293
- }
294
-
295
- function decodeLegacySystemInfo(buffer) {
296
- const state = { start: 0, end: buffer.length, buffer }
297
- const version = c.uint.decode(state)
298
-
299
- state.start--
300
-
301
- switch (version) {
302
- case 1:
303
- return SystemInfoV1.decode(state)
304
- case 2:
305
- return SystemInfoV2.decode(state)
306
- default:
307
- bail('Expected legacy system info')
308
- }
309
- }
310
-
311
- function deriveNamespace(name, bootstrap, entropy, encryptionKey) {
312
- const encryptionId = crypto.hash(encryptionKey || EMPTY)
313
- const version = c.encode(c.uint, INDEX_VERSION)
314
-
315
- return crypto.hash([
316
- NS_SIGNER_NAMESPACE,
317
- version,
318
- bootstrap,
319
- encryptionId,
320
- entropy,
321
- b4a.from(name)
322
- ])
323
- }
324
-
325
- async function findViewByName(store, key, encryptionKey, indexerManifests, views, entropy, name) {
326
- if (indexerManifests.length === 0) return null
327
-
328
- const namespace = deriveNamespace(name, key, entropy, encryptionKey)
329
-
330
- for (const v of views) {
331
- const manifest = await getCoreManifest(store, v.key)
332
- if (manifest.signers.length === 0) continue
333
-
334
- const signer = manifest.signers[0]
335
-
336
- if (b4a.equals(signer.namespace, namespace)) return v
337
- }
338
-
339
- return null
340
- }
@@ -1,23 +1,11 @@
1
1
  const safetyCatch = require('safety-catch')
2
2
  const b4a = require('b4a')
3
3
  const Hypercore = require('hypercore')
4
- const c = require('compact-encoding')
5
- const crypto = require('hypercore-crypto')
6
-
7
- const { decodeBlock } = require('hyperbee2/lib/encoding.js')
8
4
 
9
5
  const System = require('./system.js')
10
- const { assert } = require('./asserts.js')
6
+ const migrations = require('./migrations.js')
11
7
  const { LEGACY_AUTOBASE_VERSION } = require('./constants.js')
12
- const { getEncoding } = require('../encoding/spec/autobee')
13
-
14
- const SystemInfoV1 = getEncoding('@autobase-compat/info-v1')
15
- const SystemInfoV2 = getEncoding('@autobase-compat/info-v2')
16
8
 
17
- const INDEX_VERSION = 1
18
- const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
19
-
20
- const EMPTY = b4a.alloc(0)
21
9
  const DEFAULT_OP_TIMEOUT = 5_000
22
10
  const MIN_FF_GAP = 32
23
11
 
@@ -243,7 +231,25 @@ module.exports = class FastForward {
243
231
  return null
244
232
  }
245
233
 
246
- return this._migrate()
234
+ const result = await this._migrate()
235
+ if (result === null || result.autobee === null) return result
236
+
237
+ // an indexer of the newest legacy generation already moved to autobee -
238
+ // fast-forward straight onto its announced head instead of migrating,
239
+ // falling back to the legacy migration if the head cannot be booted
240
+ try {
241
+ this.head = result.autobee
242
+ await this.system.boot(this.head, { timeout: this.timeout })
243
+ if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
244
+ throw new Error('Expected an autobee system')
245
+ }
246
+ } catch (err) {
247
+ safetyCatch(err)
248
+ this.head = result.head
249
+ return { head: result.head, tip: null, migrate: result.migrate }
250
+ }
251
+
252
+ // fall through to the plain fast-forward below
247
253
  }
248
254
 
249
255
  const promises = []
@@ -280,47 +286,58 @@ module.exports = class FastForward {
280
286
  // Upgrade a legacy (pre-AUTOBEE_VERSION) system in place. The system already
281
287
  // booted above (coerced to the current struct), but the original v1/v2 struct
282
288
  // is needed to recover the indexers/views/entropy, so decode the raw block.
289
+ //
290
+ // A head may be arbitrarily old: an indexer rotation freezes the system core
291
+ // and moves the indexers to a successor. The frozen core cannot reveal its
292
+ // successor (the promotion only lands in its unsigned tail), but the
293
+ // indexers' newest oplog digests stamp the newest system they know - chase
294
+ // those so the migration lands on the newest generation instead of the head
295
+ // we were given.
283
296
  async _migrate() {
284
- const core = this.auto.store.get({ key: this.head.key, encryption: null })
285
- this.cores.push(core)
286
- await core.ready()
297
+ let head = { key: this.head.key, length: this.head.length }
298
+ const seen = new Set()
287
299
 
288
- // setup encryption
289
- await core.setEncryption(this.auto.getSystemEncryption())
300
+ for (let hops = 0; hops < migrations.MAX_MIGRATE_HOPS; hops++) {
301
+ seen.add(b4a.toString(head.key, 'hex'))
290
302
 
291
- // Decode hyperbee block using hyperbee2 compat
292
- const node = decodeBlock(
293
- await core.get(this.head.length - 1, {
303
+ const core = this.auto.store.get({ key: head.key, encryption: null })
304
+ this.cores.push(core)
305
+ await core.ready()
306
+
307
+ // setup encryption
308
+ await core.setEncryption(this.auto.getSystemEncryption())
309
+
310
+ // successors are resolved by key only - wait for the replicated length
311
+ if (!head.length) head.length = await migrations.coreLength(core, this.timeout)
312
+ if (!head.length || this.destroyed) return null
313
+
314
+ const info = await migrations.readLegacySystemInfo(core, head.length, {
294
315
  timeout: this.timeout,
295
316
  activeRequests: this.activeRequests
296
317
  })
297
- )
298
- assert(node.keys.length > 0, 'bad system block')
299
-
300
- // Decode system info from hyperbee block using autobee compat
301
- const info = decodeLegacySystemInfo(node.keys[0].value)
302
318
 
303
- const indexerManifests = await Promise.all(
304
- info.indexers.map((idx) => this.getCoreManifest(idx.key, idx.length))
305
- )
306
- const entropy =
307
- info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
319
+ const { legacy, autobee } = await migrations.findNewestSystem(this, info)
320
+ if (this.destroyed) return null
308
321
 
309
- const views = new Map()
310
- const viewProms = []
322
+ if (legacy !== null && !seen.has(b4a.toString(legacy, 'hex'))) {
323
+ head = { key: legacy, length: 0 }
324
+ continue
325
+ }
311
326
 
312
- for (const name of this.auto.legacyViews) {
313
- viewProms.push(this.findViewByName(views, indexerManifests, info.views, entropy, name))
314
- }
327
+ this.head = head
315
328
 
316
- await Promise.all(viewProms)
317
- if (this.destroyed) return null
329
+ const views = await migrations.resolveLegacyViews(this, info)
330
+ if (views === null || this.destroyed) return null
318
331
 
319
- return {
320
- head: this.head,
321
- tip: null,
322
- migrate: views
332
+ return {
333
+ head,
334
+ tip: null,
335
+ migrate: views,
336
+ autobee: autobee === null ? null : batchToHead(autobee.op.views.system)
337
+ }
323
338
  }
339
+
340
+ return null
324
341
  }
325
342
 
326
343
  async _resolveLength() {
@@ -329,47 +346,7 @@ module.exports = class FastForward {
329
346
 
330
347
  await core.ready()
331
348
 
332
- return coreLength(core, this.timeout)
333
- }
334
-
335
- async getCoreManifest(key, length) {
336
- const core = this.auto.store.get(key)
337
- this.cores.push(core)
338
-
339
- await core.ready()
340
-
341
- try {
342
- if (!core.manifest) {
343
- await core.get(length - 1, {
344
- timeout: this.timeout,
345
- activeRequests: this.activeRequests
346
- })
347
- }
348
- } catch {
349
- return null
350
- }
351
-
352
- return core.manifest
353
- }
354
-
355
- async findViewByName(result, indexerManifests, views, entropy, name) {
356
- if (indexerManifests.length === 0) return null
357
-
358
- const namespace = deriveNamespace(name, this.auto.key, entropy, this.auto.encryptionKey)
359
-
360
- for (const v of views) {
361
- const manifest = await this.getCoreManifest(v.key, v.length)
362
- if (!manifest) continue
363
-
364
- if (manifest.signers.length === 0) continue
365
-
366
- const signer = manifest.signers[0]
367
-
368
- if (b4a.equals(signer.namespace, namespace)) {
369
- result.set(name, v)
370
- return
371
- }
372
- }
349
+ return migrations.coreLength(core, this.timeout)
373
350
  }
374
351
 
375
352
  // cancels every read in flight and fails any that come after
@@ -391,42 +368,3 @@ function batchToHead(b) {
391
368
  length: b.start + b.length
392
369
  }
393
370
  }
394
-
395
- function decodeLegacySystemInfo(buffer) {
396
- const state = { start: 0, end: buffer.length, buffer }
397
- const version = c.uint.decode(state)
398
-
399
- state.start--
400
-
401
- switch (version) {
402
- case 1:
403
- return SystemInfoV1.decode(state)
404
- case 2:
405
- return SystemInfoV2.decode(state)
406
- default:
407
- return { version }
408
- }
409
- }
410
-
411
- function coreLength(core, timeout) {
412
- if (core.length) return core.length
413
-
414
- return new Promise((resolve) => {
415
- core.on('append', () => resolve(core.length))
416
- setTimeout(resolve, timeout, 0)
417
- })
418
- }
419
-
420
- function deriveNamespace(name, bootstrap, entropy, encryptionKey) {
421
- const encryptionId = crypto.hash(encryptionKey || EMPTY)
422
- const version = c.encode(c.uint, INDEX_VERSION)
423
-
424
- return crypto.hash([
425
- NS_SIGNER_NAMESPACE,
426
- version,
427
- bootstrap,
428
- encryptionId,
429
- entropy,
430
- b4a.from(name)
431
- ])
432
- }
@@ -0,0 +1,421 @@
1
+ const c = require('compact-encoding')
2
+ const b4a = require('b4a')
3
+ const crypto = require('hypercore-crypto')
4
+ const { AutobeeEncryption } = require('autobee-encryption')
5
+ const { decodeBlock } = require('hyperbee2/lib/encoding.js')
6
+
7
+ const encoding = require('./encoding.js')
8
+ const { assert, bail } = require('./asserts.js')
9
+ const { LEGACY_OPLOG_VERSION } = require('./constants.js')
10
+
11
+ const { getEncoding } = require('../encoding/spec/autobee')
12
+
13
+ const SystemInfoV1 = getEncoding('@autobase-compat/info-v1')
14
+ const SystemInfoV2 = getEncoding('@autobase-compat/info-v2')
15
+
16
+ const EMPTY = b4a.alloc(0)
17
+ const INDEX_VERSION = 1
18
+ const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
19
+
20
+ // indexer rotations chain legacy systems - how many generations we chase
21
+ const MAX_MIGRATE_HOPS = 64
22
+
23
+ module.exports = {
24
+ MAX_MIGRATE_HOPS,
25
+ checkAutobaseMigration,
26
+ readLegacySystemInfo,
27
+ findNewestSystem,
28
+ resolveLegacyViews,
29
+ inflateLegacyOplog,
30
+ decodeLegacySystemInfo,
31
+ deriveNamespace,
32
+ coreLength
33
+ }
34
+
35
+ // boot-time migration of local legacy autobase storage
36
+ async function checkAutobaseMigration(store, local, bootstrap, legacyViews) {
37
+ const bootRecord = await local.getUserData('autobase/boot')
38
+ if (!bootRecord) return null
39
+
40
+ const { key } = encoding.decodeAutobaseBootRecord(bootRecord)
41
+ const core = store.get({ key: key, encryption: null })
42
+ await core.ready()
43
+
44
+ const system = { key, length: core.length }
45
+
46
+ // setup encryption
47
+ const encryptionKey = await local.getUserData('autobase/encryption')
48
+ await AutobeeEncryption.setSystemEncryption(bootstrap.key, encryptionKey, core)
49
+
50
+ const catchup = await getCatchupHeads(core)
51
+ const nodes = await Promise.all(
52
+ catchup.map((n) => getWriterBatch(store, n, bootstrap.key, encryptionKey))
53
+ )
54
+
55
+ const info = await readLegacySystemInfo(core, core.length)
56
+
57
+ const indexerManifests = await Promise.all(
58
+ info.indexers.map((idx) => storeCoreManifest(store, idx.key))
59
+ )
60
+ const views = await Promise.all(info.views.map((v) => getPersistedView(store, v)))
61
+ const entropy = legacyEntropy(info, indexerManifests)
62
+
63
+ const getManifest = (key) => storeCoreManifest(store, key)
64
+
65
+ const mappedViews = new Map()
66
+ for (const name of legacyViews) {
67
+ const v = await findViewByName(
68
+ getManifest,
69
+ bootstrap.key,
70
+ encryptionKey,
71
+ indexerManifests,
72
+ views,
73
+ entropy,
74
+ name
75
+ )
76
+
77
+ mappedViews.set(name, v)
78
+ }
79
+
80
+ await core.close()
81
+
82
+ return {
83
+ encryptionKey,
84
+ system,
85
+ views: mappedViews,
86
+ catchup: nodes
87
+ }
88
+ }
89
+
90
+ // decode the legacy system info recorded in the system core's block at length
91
+ async function readLegacySystemInfo(core, length, opts = null) {
92
+ // Decode hyperbee block using hyperbee2 compat
93
+ const node = decodeBlock(await core.get(length - 1, opts))
94
+ assert(node.keys.length > 0, 'bad system block')
95
+
96
+ // Decode system info from hyperbee block using autobee compat
97
+ return decodeLegacySystemInfo(node.keys[0].value)
98
+ }
99
+
100
+ // read each indexer's newest legacy oplog stamp - its digest/checkpoint mark
101
+ // the newest legacy system that indexer knows. take the majority vote. an
102
+ // indexer that already moved to autobee announces its live autobee state
103
+ // instead - keep the newest of those as a direct fast-forward candidate and
104
+ // bisect down to its buried legacy stamps so its vote still counts
105
+ async function findNewestSystem(ff, info) {
106
+ const tally = new Map()
107
+
108
+ let autobee = null
109
+
110
+ for (const idx of info.indexers) {
111
+ const core = ff.auto.openCore(idx.key)
112
+ ff.cores.push(core)
113
+ await core.ready()
114
+
115
+ const length = await coreLength(core, ff.timeout)
116
+ if (!length || ff.destroyed) continue
117
+
118
+ let oplog = await flushHead(ff.auto, { key: idx.key, length }, { timeout: ff.timeout })
119
+ if (oplog === null || !oplog.op.views) continue
120
+
121
+ if (oplog.op.version > LEGACY_OPLOG_VERSION) {
122
+ if (autobee === null || oplog.op.views.flushes > autobee.op.views.flushes) {
123
+ autobee = oplog
124
+ }
125
+
126
+ const boundary = await findLegacyBoundary(ff, core, idx.length, oplog.length - 1)
127
+ if (!boundary || ff.destroyed) continue
128
+
129
+ oplog = await flushHead(ff.auto, { key: idx.key, length: boundary }, { timeout: ff.timeout })
130
+ if (oplog === null || !oplog.op.views) continue
131
+ if (oplog.op.version > LEGACY_OPLOG_VERSION) continue
132
+ }
133
+
134
+ const sys = oplog.op.views.system
135
+ if (!sys || !sys.key) continue
136
+
137
+ const hex = b4a.toString(sys.key, 'hex')
138
+ const t = tally.get(hex) || { key: sys.key, votes: 0, length: sys.length }
139
+
140
+ t.votes++
141
+ if (sys.length > t.length) t.length = sys.length
142
+ tally.set(hex, t)
143
+ }
144
+
145
+ const majority = (info.indexers.length >> 1) + 1
146
+
147
+ let best = null
148
+ for (const t of tally.values()) {
149
+ if (t.votes >= majority && (best === null || t.length > best.length)) best = t
150
+ }
151
+
152
+ return { legacy: best === null ? null : best.key, autobee }
153
+ }
154
+
155
+ // map the legacy views recorded in the system info by name
156
+ async function resolveLegacyViews(ff, info) {
157
+ const indexerManifests = await Promise.all(
158
+ info.indexers.map((idx) => ffCoreManifest(ff, idx.key, idx.length))
159
+ )
160
+ if (ff.destroyed || indexerManifests.some((m) => m === null)) return null
161
+
162
+ const entropy = legacyEntropy(info, indexerManifests)
163
+
164
+ const getManifest = (key, length) => ffCoreManifest(ff, key, length)
165
+
166
+ const views = new Map()
167
+
168
+ await Promise.all(
169
+ ff.auto.legacyViews.map(async (name) => {
170
+ const v = await findViewByName(
171
+ getManifest,
172
+ ff.auto.key,
173
+ ff.auto.encryptionKey,
174
+ indexerManifests,
175
+ info.views,
176
+ entropy,
177
+ name
178
+ )
179
+
180
+ if (v) views.set(name, v)
181
+ })
182
+ )
183
+
184
+ if (ff.destroyed) return null
185
+
186
+ return views
187
+ }
188
+
189
+ // legacy nodes always inflate, but only indexers carry the digest/checkpoint
190
+ // stamps that synthesise system info - callers must check op.views
191
+ async function inflateLegacyOplog(buf, core, seq, opts = null) {
192
+ const m = encoding.decodeRawOplog(buf)
193
+
194
+ const op = {
195
+ version: m.version,
196
+ timestamp: 0,
197
+ links: m.node.heads,
198
+ batch: { start: 0, end: m.node.batch - 1 },
199
+ views: null,
200
+ optimistic: !!m.optimistic,
201
+ value: m.node.value
202
+ }
203
+
204
+ if (m.digest === null || m.checkpoint === null || !m.checkpoint.system) return op
205
+
206
+ const fetches = []
207
+
208
+ fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, opts) : buf)
209
+ fetches.push(m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer, opts) : buf)
210
+
211
+ const [digestNode, checkpointNode] = await Promise.all(fetches)
212
+ // a wait: false get we could not satisfy locally
213
+ if (digestNode === null || checkpointNode === null) return op
214
+
215
+ const { digest } = encoding.decodeRawOplog(digestNode)
216
+ const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
217
+
218
+ if (!checkpoint.system || !checkpoint.system.checkpoint) return op
219
+
220
+ op.views = {
221
+ system: {
222
+ key: digest.key,
223
+ start: 0,
224
+ length: checkpoint.system.checkpoint.length
225
+ },
226
+ flushes: seq
227
+ }
228
+
229
+ return op
230
+ }
231
+
232
+ function decodeLegacySystemInfo(buffer) {
233
+ const state = { start: 0, end: buffer.length, buffer }
234
+ const version = c.uint.decode(state)
235
+
236
+ state.start--
237
+
238
+ switch (version) {
239
+ case 1:
240
+ return SystemInfoV1.decode(state)
241
+ case 2:
242
+ return SystemInfoV2.decode(state)
243
+ default:
244
+ bail('Expected legacy system info')
245
+ }
246
+ }
247
+
248
+ function deriveNamespace(name, bootstrap, entropy, encryptionKey) {
249
+ const encryptionId = crypto.hash(encryptionKey || EMPTY)
250
+ const version = c.encode(c.uint, INDEX_VERSION)
251
+
252
+ return crypto.hash([
253
+ NS_SIGNER_NAMESPACE,
254
+ version,
255
+ bootstrap,
256
+ encryptionId,
257
+ entropy,
258
+ b4a.from(name)
259
+ ])
260
+ }
261
+
262
+ function coreLength(core, timeout) {
263
+ if (core.length) return core.length
264
+
265
+ return new Promise((resolve) => {
266
+ core.on('append', () => resolve(core.length))
267
+ setTimeout(resolve, timeout, 0)
268
+ })
269
+ }
270
+
271
+ // a writer's oplog is a legacy prefix followed by autobee-format nodes -
272
+ // bisect for the boundary. lo is a length known to be legacy (the legacy
273
+ // system's recorded length for this writer), hi a seq known to be autobee.
274
+ // returns the legacy prefix length
275
+ async function findLegacyBoundary(ff, core, lo, hi) {
276
+ while (lo < hi) {
277
+ const mid = (lo + hi) >>> 1
278
+
279
+ const block = await core.get(mid, {
280
+ timeout: ff.timeout,
281
+ activeRequests: ff.activeRequests
282
+ })
283
+
284
+ if (encoding.decodeRawOplog(block).version > LEGACY_OPLOG_VERSION) hi = mid
285
+ else lo = mid + 1
286
+ }
287
+
288
+ return lo
289
+ }
290
+
291
+ function legacyEntropy(info, indexerManifests) {
292
+ return info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
293
+ }
294
+
295
+ async function findViewByName(
296
+ getManifest,
297
+ bootstrap,
298
+ encryptionKey,
299
+ indexerManifests,
300
+ views,
301
+ entropy,
302
+ name
303
+ ) {
304
+ if (indexerManifests.length === 0) return null
305
+
306
+ const namespace = deriveNamespace(name, bootstrap, entropy, encryptionKey)
307
+
308
+ for (const v of views) {
309
+ const manifest = await getManifest(v.key, v.length)
310
+ if (!manifest) continue
311
+
312
+ if (manifest.signers.length === 0) continue
313
+
314
+ const signer = manifest.signers[0]
315
+
316
+ if (b4a.equals(signer.namespace, namespace)) return v
317
+ }
318
+
319
+ return null
320
+ }
321
+
322
+ // request-tracked manifest read over replication
323
+ async function ffCoreManifest(ff, key, length) {
324
+ const core = ff.auto.store.get(key)
325
+ ff.cores.push(core)
326
+
327
+ await core.ready()
328
+
329
+ try {
330
+ if (!core.manifest) {
331
+ await core.get(length - 1, {
332
+ timeout: ff.timeout,
333
+ activeRequests: ff.activeRequests
334
+ })
335
+ }
336
+ } catch {
337
+ return null
338
+ }
339
+
340
+ return core.manifest
341
+ }
342
+
343
+ // local-storage manifest read
344
+ async function storeCoreManifest(store, key) {
345
+ const core = store.get(key)
346
+ await core.ready()
347
+
348
+ const manifest = core.manifest
349
+ await core.close()
350
+
351
+ return manifest
352
+ }
353
+
354
+ async function getPersistedView(store, view) {
355
+ const core = store.get(view.key)
356
+ await core.ready()
357
+
358
+ const length = core.length
359
+ await core.close()
360
+
361
+ return { key: view.key, length }
362
+ }
363
+
364
+ async function getCatchupHeads(core) {
365
+ const session = core.session({ name: 'batch' })
366
+ await session.ready()
367
+
368
+ const nodes = []
369
+ for (let i = core.length; i < session.length; i++) {
370
+ const node = await session.get(i, { wait: false })
371
+ if (!node) throw new Error('Expect nodes to exist locally')
372
+
373
+ const entry = decodeBlock(node).keys[0]
374
+ if (entry.key[0] !== 0x01) continue
375
+
376
+ const { key, length } = encoding.decodeSystemWriter(entry.key, entry.value)
377
+ if (!length) continue
378
+ nodes.push({ key, length })
379
+ }
380
+
381
+ await session.close()
382
+
383
+ return nodes
384
+ }
385
+
386
+ async function getWriterBatch(store, head, key, encryptionKey, nodes = []) {
387
+ const batch = []
388
+
389
+ const core = store.get(head.key)
390
+ await core.ready()
391
+
392
+ if (encryptionKey) {
393
+ await core.setEncryption(AutobeeEncryption.getWriterEncryption(key, encryptionKey))
394
+ }
395
+
396
+ let seq = head.length - 1
397
+ const block = await core.get(seq--, { wait: false })
398
+ if (!block) throw new Error('Expect writer node to exist locally')
399
+
400
+ const node = encoding.decodeOplog(block)
401
+
402
+ batch.unshift({ ...head, ...node, from: core })
403
+
404
+ while (seq >= 0) {
405
+ const block = await core.get(seq--, { wait: false })
406
+ if (!block) break
407
+
408
+ const node = encoding.decodeOplog(block)
409
+ if (!node.batch.end) break
410
+
411
+ batch.unshift({ key: head.key, length: seq + 2, from: core, ...node })
412
+ }
413
+
414
+ return batch
415
+ }
416
+
417
+ // lazy to avoid the require cycle - fast-forward imports this module
418
+ function flushHead(auto, head, opts) {
419
+ const FastForward = require('./fast-forward.js')
420
+ return FastForward.flushHead(auto, head, opts)
421
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.0.0-rc.22",
3
+ "version": "2.0.0-rc.25",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",