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 +1 -1
- package/lib/boot.js +1 -1
- package/lib/fast-forward.js +1 -1
- package/lib/migrations.js +421 -0
- package/package.json +1 -1
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
package/lib/fast-forward.js
CHANGED
|
@@ -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
|
+
}
|