autobee 2.0.0-rc.23 → 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,7 +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
+ const migrations = require('./lib/migrations.js')
26
26
 
27
27
  const EMPTY_HEAD = { length: 0, key: null }
28
28
  const INTERRUPT = new Error('Apply interrupted')
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/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.25",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",