autobee 2.0.0-rc.21 → 2.0.0-rc.23

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')
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
@@ -436,9 +442,8 @@ module.exports = class Autobee extends ReadyResource {
436
442
  async _bootAll() {
437
443
  if (!(await this._bootReady())) return
438
444
 
439
- for await (const node of this.system.list()) {
440
- if (node.isAnchor) continue
441
- await this.writers.add(node.key)
445
+ for (const head of this.system.heads) {
446
+ await this.writers.add(head.key)
442
447
  }
443
448
  await this._bump()
444
449
  }
@@ -505,9 +510,20 @@ module.exports = class Autobee extends ReadyResource {
505
510
  this.emit('error', err)
506
511
  }
507
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
+
508
522
  async _drain() {
509
523
  if (this._updating) await this._updating
510
524
 
525
+ await this._runPreApply()
526
+
511
527
  this.stats.drains++
512
528
 
513
529
  if (this.bootFrom) {
@@ -627,6 +643,8 @@ module.exports = class Autobee extends ReadyResource {
627
643
  const hints = await this._applyWakeupHints()
628
644
  if (!hints.size) return
629
645
 
646
+ if (!this._ffEnabled) return
647
+
630
648
  // a scheduled fast-forward is applied by the drain before we look again
631
649
  if (this.fastForwardTo !== null || this.fastForwarding !== null) return
632
650
  if (this._interrupting || this.closing || this.bootFrom) return
@@ -701,7 +719,7 @@ module.exports = class Autobee extends ReadyResource {
701
719
  // legacy nodes always inflate, but only indexers carry views - callers
702
720
  // that need system info must check op.views
703
721
  if (op.version < 3) {
704
- op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
722
+ op = await migrations.inflateLegacyOplog(buf, core, target - 1, opts)
705
723
  }
706
724
 
707
725
  return {
@@ -711,49 +729,6 @@ module.exports = class Autobee extends ReadyResource {
711
729
  }
712
730
  }
713
731
 
714
- async _inflateLegacyOplog(buf, core, seq, opts = null) {
715
- const m = encoding.decodeRawOplog(buf)
716
-
717
- const op = {
718
- version: m.version,
719
- timestamp: 0,
720
- links: m.node.heads,
721
- batch: { start: 0, end: m.node.batch - 1 },
722
- views: null,
723
- optimistic: !!m.optimistic,
724
- value: m.node.value
725
- }
726
-
727
- if (m.digest === null || m.checkpoint === null || !m.checkpoint.system) return op
728
-
729
- const fetches = []
730
-
731
- fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, opts) : buf)
732
- fetches.push(
733
- m.checkpoint.pointer ? core.get(seq - m.checkpoint.system.checkpointer, opts) : buf
734
- )
735
-
736
- const [digestNode, checkpointNode] = await Promise.all(fetches)
737
- // a wait: false get we could not satisfy locally
738
- if (digestNode === null || checkpointNode === null) return op
739
-
740
- const { digest } = encoding.decodeRawOplog(digestNode)
741
- const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
742
-
743
- if (!checkpoint.system || !checkpoint.system.checkpoint) return op
744
-
745
- op.views = {
746
- system: {
747
- key: digest.key,
748
- start: 0,
749
- length: checkpoint.system.checkpoint.length
750
- },
751
- flushes: seq
752
- }
753
-
754
- return op
755
- }
756
-
757
732
  async _update(changes) {
758
733
  this._needsUpdate = false
759
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')
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')
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
- }
package/lib/writers.js CHANGED
@@ -633,7 +633,15 @@ class Writer {
633
633
  const promises = new Array(links.length)
634
634
 
635
635
  for (let i = 0; i < links.length; i++) {
636
- promises[i] = this.system.has(links[i])
636
+ const link = links[i]
637
+
638
+ // legacy batch nodes link their own previous node, satisfied by oplog order
639
+ if (link.length < node.length && b4a.equals(link.key, this.core.key)) {
640
+ promises[i] = true
641
+ continue
642
+ }
643
+
644
+ promises[i] = this.system.has(link)
637
645
  }
638
646
 
639
647
  const has = await Promise.all(promises)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.0.0-rc.21",
3
+ "version": "2.0.0-rc.23",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "brittle": "^4.1.0",
41
- "corestore": "^7.11.0",
41
+ "corestore": "^7.12.2",
42
42
  "hypercore-crypto": "^3.6.1",
43
43
  "lunte": "^1.6.0",
44
44
  "prettier": "^3.8.1",