autobee 2.0.0-rc.23 → 2.0.0-rc.26

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
@@ -14,6 +14,7 @@ const asserts = require('./lib/asserts.js')
14
14
  const boot = require('./lib/boot.js')
15
15
  const { resolveWeight, currentWeight } = require('./lib/witness.js')
16
16
  const encoding = require('./lib/encoding.js')
17
+ const { LEGACY_OPLOG_VERSION } = require('./lib/constants.js')
17
18
  const FastForward = require('./lib/fast-forward.js')
18
19
  const System = require('./lib/system.js')
19
20
  const ApplyCalls = require('./lib/apply-calls.js')
@@ -22,7 +23,7 @@ const { ActiveWriters } = require('./lib/writers.js')
22
23
  const TrustedPeers = require('./lib/trusted.js')
23
24
  const ApplyView = require('./lib/apply-view.js')
24
25
  const UpdateChanges = require('./lib/updates.js')
25
- const migrations = require('./lib/migrations')
26
+ const migrations = require('./lib/migrations.js')
26
27
 
27
28
  const EMPTY_HEAD = { length: 0, key: null }
28
29
  const INTERRUPT = new Error('Apply interrupted')
@@ -527,12 +528,12 @@ module.exports = class Autobee extends ReadyResource {
527
528
  this.stats.drains++
528
529
 
529
530
  if (this.bootFrom) {
530
- const { head = null, legacy = null, bootCondition = null } = this.bootFrom
531
+ const { head = null, legacy = null, bootCondition = null, wait = false } = this.bootFrom
531
532
 
532
533
  this.bootFrom = null
533
534
 
534
535
  if (legacy) await this._bootFromSystem(legacy)
535
- else if (head) await this._bootFromHead(head, bootCondition)
536
+ else if (head) await this._bootFromHead(head, bootCondition, wait)
536
537
  }
537
538
 
538
539
  this._ackRequired = false
@@ -1195,9 +1196,9 @@ module.exports = class Autobee extends ReadyResource {
1195
1196
 
1196
1197
  // the boot head seeds the view every trust decision here is made against,
1197
1198
  // since our own view is still empty
1198
- async _bootFromHead(head, bootCondition) {
1199
+ async _bootFromHead(head, bootCondition, wait = false) {
1199
1200
  // just a head: one attempt, we do not wait around if it cannot be read
1200
- if (bootCondition === null) {
1201
+ if (bootCondition === null && !wait) {
1201
1202
  try {
1202
1203
  const ff = await FastForward.fromHead(this, head, null, {
1203
1204
  force: true,
@@ -1211,8 +1212,8 @@ module.exports = class Autobee extends ReadyResource {
1211
1212
  }
1212
1213
  }
1213
1214
 
1214
- // a condition needs the view at head to check against, so park until we
1215
- // can read it and something acceptable turns up
1215
+ // park until the head can be read and, if a condition is set, something
1216
+ // acceptable turns up
1216
1217
  let opened = null
1217
1218
 
1218
1219
  try {
@@ -1220,10 +1221,28 @@ module.exports = class Autobee extends ReadyResource {
1220
1221
  this._bootWait = rrp()
1221
1222
 
1222
1223
  try {
1223
- if (opened === null) opened = await this._bootReference(head)
1224
-
1225
- if (opened !== null && (await this._bootAttempt(head, bootCondition, opened.view))) {
1226
- return true
1224
+ if (bootCondition === null) {
1225
+ const ff = await FastForward.fromHead(this, head, null, {
1226
+ force: true,
1227
+ timeout: FastForward.DEFAULT_TIMEOUT
1228
+ })
1229
+
1230
+ if (ff !== null && (await this._runFastForward(ff))) return true
1231
+ } else {
1232
+ if (opened === null) opened = await this._bootReference(head)
1233
+
1234
+ if (opened !== null) {
1235
+ if (await this._bootAttempt(head, bootCondition, opened.view)) {
1236
+ return true
1237
+ }
1238
+
1239
+ // the reference may be stale - resolve it afresh next attempt
1240
+ await opened.close()
1241
+ opened = null
1242
+ } else if (await this._bootLegacyHead(head)) {
1243
+ // boot legacy head through the migration pathway
1244
+ return true
1245
+ }
1227
1246
  }
1228
1247
  } catch (err) {
1229
1248
  safetyCatch(err)
@@ -1258,6 +1277,20 @@ module.exports = class Autobee extends ReadyResource {
1258
1277
  return ff !== null && (await this._runFastForward(ff))
1259
1278
  }
1260
1279
 
1280
+ async _bootLegacyHead(head) {
1281
+ const oplog = await FastForward.flushHead(this, head, {
1282
+ timeout: FastForward.DEFAULT_TIMEOUT
1283
+ })
1284
+ if (oplog === null || oplog.op.version > LEGACY_OPLOG_VERSION) return false
1285
+
1286
+ const ff = await FastForward.fromHead(this, head, null, {
1287
+ force: true,
1288
+ timeout: FastForward.DEFAULT_TIMEOUT
1289
+ })
1290
+
1291
+ return ff !== null && (await this._runFastForward(ff))
1292
+ }
1293
+
1261
1294
  async _bootReference(head) {
1262
1295
  const oplog = await FastForward.flushHead(this, head, {
1263
1296
  timeout: FastForward.DEFAULT_TIMEOUT
package/lib/boot.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const encoding = require('./encoding.js')
2
- const { checkAutobaseMigration } = require('./migrations')
2
+ const { checkAutobaseMigration } = require('./migrations.js')
3
3
 
4
4
  module.exports = async function boot(
5
5
  corestore,
@@ -3,7 +3,7 @@ const b4a = require('b4a')
3
3
  const Hypercore = require('hypercore')
4
4
 
5
5
  const System = require('./system.js')
6
- const migrations = require('./migrations')
6
+ const migrations = require('./migrations.js')
7
7
  const { LEGACY_AUTOBASE_VERSION } = require('./constants.js')
8
8
 
9
9
  const DEFAULT_OP_TIMEOUT = 5_000
@@ -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/lib/writers.js CHANGED
@@ -59,21 +59,26 @@ class ActiveWriters {
59
59
  let w = this.active.get(id)
60
60
 
61
61
  if (w === this.localWriter) return
62
+
63
+ let added = false
62
64
  if (!w) {
63
65
  // anchors are local-only and never replicated - nothing to fetch
64
66
  const info = await this.auto.system.get(key, { unflushed: true })
65
67
  if (info && info.isAnchor) return
66
68
  w = await this.add(key)
67
69
  if (!w) return
70
+ added = true
68
71
  }
69
72
 
73
+ const advanced = length > w.hintedLength
74
+
70
75
  // remember the announced length so gc doesn't drop a writer we haven't caught up on
71
76
  w.hint(length)
72
77
 
73
78
  // prefetch the oplog head for the fast-forward that runs right after us
74
79
  if (w && length > 0) w.core.get(length - 1).catch(noop)
75
80
 
76
- this.bump()
81
+ if (added || advanced) this.bump()
77
82
  }
78
83
 
79
84
  getLatestLocalOplog() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.0.0-rc.23",
3
+ "version": "2.0.0-rc.26",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",
@@ -43,6 +43,7 @@
43
43
  "lunte": "^1.6.0",
44
44
  "prettier": "^3.8.1",
45
45
  "prettier-config-holepunch": "^2.0.0",
46
- "protomux-wakeup": "^2.9.0"
46
+ "protomux-wakeup": "^2.9.0",
47
+ "replication-simulator": "^3.0.0"
47
48
  }
48
49
  }