autobee 2.0.0-rc.8 → 2.0.0

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.
@@ -0,0 +1,457 @@
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, systemLength } = encoding.decodeAutobaseBootRecord(bootRecord)
41
+ const core = store.get({ key: key, encryption: null })
42
+ await core.ready()
43
+
44
+ const length = await findLocalSystemLength(core, systemLength)
45
+ assert(length > 0, 'Expected system block to exist locally')
46
+
47
+ const system = { key, length }
48
+
49
+ // setup encryption
50
+ const encryptionKey = await local.getUserData('autobase/encryption')
51
+ await AutobeeEncryption.setSystemEncryption(bootstrap.key, encryptionKey, core)
52
+
53
+ const session = core.session({ name: 'batch' })
54
+ await session.ready()
55
+
56
+ let catchup
57
+
58
+ try {
59
+ catchup = await getCatchupHeads(session, length)
60
+ } finally {
61
+ await session.close()
62
+ }
63
+
64
+ const info = await readLegacySystemInfo(core, length, { wait: false })
65
+
66
+ const nodes = await Promise.all(
67
+ catchup.map((n) => getWriterBatch(store, n, bootstrap.key, encryptionKey))
68
+ )
69
+
70
+ const indexerManifests = await Promise.all(
71
+ info.indexers.map((idx) => storeCoreManifest(store, idx.key))
72
+ )
73
+ const views = await Promise.all(info.views.map((v) => getPersistedView(store, v)))
74
+ const entropy = legacyEntropy(info, indexerManifests)
75
+
76
+ const getManifest = (key) => storeCoreManifest(store, key)
77
+
78
+ const mappedViews = new Map()
79
+ for (const name of legacyViews) {
80
+ const v = await findViewByName(
81
+ getManifest,
82
+ bootstrap.key,
83
+ encryptionKey,
84
+ indexerManifests,
85
+ views,
86
+ entropy,
87
+ name
88
+ )
89
+
90
+ mappedViews.set(name, v)
91
+ }
92
+
93
+ await core.close()
94
+
95
+ return {
96
+ encryptionKey,
97
+ system,
98
+ views: mappedViews,
99
+ catchup: nodes
100
+ }
101
+ }
102
+
103
+ // decode the legacy system info recorded in the system core's block at length
104
+ async function readLegacySystemInfo(core, length, opts = null) {
105
+ const block = await core.get(length - 1, opts)
106
+ assert(block !== null, 'Expected system block to exist locally')
107
+
108
+ // Decode hyperbee block using hyperbee2 compat
109
+ const node = decodeBlock(block)
110
+ assert(node.keys.length > 0, 'bad system block')
111
+
112
+ // Decode system info from hyperbee block using autobee compat
113
+ return decodeLegacySystemInfo(node.keys[0].value)
114
+ }
115
+
116
+ // read each indexer's newest legacy oplog stamp - its digest/checkpoint mark
117
+ // the newest legacy system that indexer knows. take the majority vote. an
118
+ // indexer that already moved to autobee announces its live autobee state
119
+ // instead - keep the newest of those as a direct fast-forward candidate and
120
+ // bisect down to its buried legacy stamps so its vote still counts
121
+ async function findNewestSystem(ff, info) {
122
+ const tally = new Map()
123
+
124
+ let autobee = null
125
+
126
+ for (const idx of info.indexers) {
127
+ const core = ff.auto.openCore(idx.key)
128
+ ff.cores.push(core)
129
+ await core.ready()
130
+
131
+ const length = await coreLength(core, ff.timeout)
132
+ if (!length || ff.destroyed) continue
133
+
134
+ let oplog = await flushHead(ff.auto, { key: idx.key, length }, { timeout: ff.timeout })
135
+ if (oplog === null || !oplog.op.views) continue
136
+
137
+ if (oplog.op.version > LEGACY_OPLOG_VERSION) {
138
+ if (autobee === null || oplog.op.views.flushes > autobee.op.views.flushes) {
139
+ autobee = oplog
140
+ }
141
+
142
+ const boundary = await findLegacyBoundary(ff, core, idx.length, oplog.length - 1)
143
+ if (!boundary || ff.destroyed) continue
144
+
145
+ oplog = await flushHead(ff.auto, { key: idx.key, length: boundary }, { timeout: ff.timeout })
146
+ if (oplog === null || !oplog.op.views) continue
147
+ if (oplog.op.version > LEGACY_OPLOG_VERSION) continue
148
+ }
149
+
150
+ const sys = oplog.op.views.system
151
+ if (!sys || !sys.key) continue
152
+
153
+ const hex = b4a.toString(sys.key, 'hex')
154
+ const t = tally.get(hex) || { key: sys.key, votes: 0, length: sys.length }
155
+
156
+ t.votes++
157
+ if (sys.length > t.length) t.length = sys.length
158
+ tally.set(hex, t)
159
+ }
160
+
161
+ const majority = (info.indexers.length >> 1) + 1
162
+
163
+ let best = null
164
+ for (const t of tally.values()) {
165
+ if (t.votes >= majority && (best === null || t.length > best.length)) best = t
166
+ }
167
+
168
+ return { legacy: best === null ? null : best.key, autobee }
169
+ }
170
+
171
+ // map the legacy views recorded in the system info by name
172
+ async function resolveLegacyViews(ff, info) {
173
+ const indexerManifests = await Promise.all(
174
+ info.indexers.map((idx) => ffCoreManifest(ff, idx.key, idx.length))
175
+ )
176
+ if (ff.destroyed || indexerManifests.some((m) => m === null)) return null
177
+
178
+ const entropy = legacyEntropy(info, indexerManifests)
179
+
180
+ const getManifest = (key, length) => ffCoreManifest(ff, key, length)
181
+
182
+ const views = new Map()
183
+
184
+ await Promise.all(
185
+ ff.auto.legacyViews.map(async (name) => {
186
+ const v = await findViewByName(
187
+ getManifest,
188
+ ff.auto.key,
189
+ ff.auto.encryptionKey,
190
+ indexerManifests,
191
+ info.views,
192
+ entropy,
193
+ name
194
+ )
195
+
196
+ if (v) views.set(name, v)
197
+ })
198
+ )
199
+
200
+ if (ff.destroyed) return null
201
+
202
+ return views
203
+ }
204
+
205
+ // legacy nodes always inflate, but only indexers carry the digest/checkpoint
206
+ // stamps that synthesise system info - callers must check op.views
207
+ async function inflateLegacyOplog(buf, core, seq, timeout) {
208
+ const m = encoding.decodeRawOplog(buf)
209
+
210
+ const op = {
211
+ version: m.version,
212
+ timestamp: 0,
213
+ links: m.node.heads,
214
+ batch: { start: 0, end: m.node.batch - 1 },
215
+ views: null,
216
+ optimistic: !!m.optimistic,
217
+ value: m.node.value
218
+ }
219
+
220
+ if (m.digest === null || m.checkpoint === null || !m.checkpoint.system) return op
221
+
222
+ const fetches = []
223
+
224
+ fetches.push(m.digest.pointer ? core.get(seq - m.digest.pointer, { timeout }) : buf)
225
+ fetches.push(
226
+ m.checkpoint.system.checkpointer
227
+ ? core.get(seq - m.checkpoint.system.checkpointer, { timeout })
228
+ : buf
229
+ )
230
+
231
+ const [digestNode, checkpointNode] = await Promise.all(fetches)
232
+ // no caller reads best-effort today, but stay total over a missing block
233
+ if (digestNode === null || checkpointNode === null) return op
234
+
235
+ const { digest } = encoding.decodeRawOplog(digestNode)
236
+ const { checkpoint } = encoding.decodeRawOplog(checkpointNode)
237
+
238
+ if (!checkpoint.system || !checkpoint.system.checkpoint) return op
239
+
240
+ op.views = {
241
+ system: {
242
+ key: digest.key,
243
+ start: 0,
244
+ length: checkpoint.system.checkpoint.length
245
+ },
246
+ flushes: seq
247
+ }
248
+
249
+ return op
250
+ }
251
+
252
+ function decodeLegacySystemInfo(buffer) {
253
+ const state = { start: 0, end: buffer.length, buffer }
254
+ const version = c.uint.decode(state)
255
+
256
+ state.start--
257
+
258
+ switch (version) {
259
+ case 1:
260
+ return SystemInfoV1.decode(state)
261
+ case 2:
262
+ return SystemInfoV2.decode(state)
263
+ default:
264
+ bail('Expected legacy system info')
265
+ }
266
+ }
267
+
268
+ function deriveNamespace(name, bootstrap, entropy, encryptionKey) {
269
+ const encryptionId = crypto.hash(encryptionKey || EMPTY)
270
+ const version = c.encode(c.uint, INDEX_VERSION)
271
+
272
+ return crypto.hash([
273
+ NS_SIGNER_NAMESPACE,
274
+ version,
275
+ bootstrap,
276
+ encryptionId,
277
+ entropy,
278
+ b4a.from(name)
279
+ ])
280
+ }
281
+
282
+ // the record's length can run past the signed core into the local batch
283
+ // session, and the signed core can carry verified-but-undownloaded blocks -
284
+ // scan back for the newest block the signed core actually has locally
285
+ async function findLocalSystemLength(core, systemLength) {
286
+ let length = Math.min(core.length, systemLength)
287
+ while (length > 0 && !(await core.has(length - 1))) length--
288
+ return length
289
+ }
290
+
291
+ function coreLength(core, timeout) {
292
+ if (core.length) return core.length
293
+
294
+ return new Promise((resolve) => {
295
+ core.on('append', () => resolve(core.length))
296
+ setTimeout(resolve, timeout, 0)
297
+ })
298
+ }
299
+
300
+ // a writer's oplog is a legacy prefix followed by autobee-format nodes -
301
+ // bisect for the boundary. lo is a length known to be legacy (the legacy
302
+ // system's recorded length for this writer), hi a seq known to be autobee.
303
+ // returns the legacy prefix length
304
+ async function findLegacyBoundary(ff, core, lo, hi) {
305
+ while (lo < hi) {
306
+ const mid = (lo + hi) >>> 1
307
+
308
+ const block = await core.get(mid, {
309
+ timeout: ff.timeout,
310
+ activeRequests: ff.activeRequests
311
+ })
312
+
313
+ if (encoding.decodeRawOplog(block).version > LEGACY_OPLOG_VERSION) hi = mid
314
+ else lo = mid + 1
315
+ }
316
+
317
+ return lo
318
+ }
319
+
320
+ function legacyEntropy(info, indexerManifests) {
321
+ return info.version > 1 && info.entropy ? info.entropy : indexerManifests[0].signers[0].namespace
322
+ }
323
+
324
+ async function findViewByName(
325
+ getManifest,
326
+ bootstrap,
327
+ encryptionKey,
328
+ indexerManifests,
329
+ views,
330
+ entropy,
331
+ name
332
+ ) {
333
+ if (indexerManifests.length === 0) return null
334
+
335
+ const namespace = deriveNamespace(name, bootstrap, entropy, encryptionKey)
336
+
337
+ for (const v of views) {
338
+ const manifest = await getManifest(v.key, v.length)
339
+ if (!manifest) continue
340
+
341
+ if (manifest.signers.length === 0) continue
342
+
343
+ const signer = manifest.signers[0]
344
+
345
+ if (b4a.equals(signer.namespace, namespace)) return v
346
+ }
347
+
348
+ return null
349
+ }
350
+
351
+ // request-tracked manifest read over replication
352
+ async function ffCoreManifest(ff, key, length) {
353
+ const core = ff.auto.store.get(key)
354
+ ff.cores.push(core)
355
+
356
+ await core.ready()
357
+
358
+ try {
359
+ if (!core.manifest) {
360
+ await core.get(length - 1, {
361
+ timeout: ff.timeout,
362
+ activeRequests: ff.activeRequests
363
+ })
364
+ }
365
+ } catch {
366
+ return null
367
+ }
368
+
369
+ return core.manifest
370
+ }
371
+
372
+ // local-storage manifest read
373
+ async function storeCoreManifest(store, key) {
374
+ const core = store.get(key)
375
+ await core.ready()
376
+
377
+ const manifest = core.manifest
378
+ await core.close()
379
+
380
+ return manifest
381
+ }
382
+
383
+ async function getPersistedView(store, view) {
384
+ const core = store.get(view.key)
385
+ await core.ready()
386
+
387
+ // core.length can run past what was downloaded - the system's recorded
388
+ // length is the last one we know is local
389
+ const length = Math.min(core.length, view.length)
390
+ await core.close()
391
+
392
+ return { key: view.key, length }
393
+ }
394
+
395
+ async function getCatchupHeads(session, from) {
396
+ const seen = new Map()
397
+ const nodes = []
398
+
399
+ // block 0 is the bee header, never a node
400
+ for (let i = Math.max(from, 1); i < session.length; i++) {
401
+ const node = await session.get(i, { wait: false })
402
+ if (!node) throw new Error('Expect nodes to exist locally')
403
+
404
+ const entry = decodeBlock(node).keys[0]
405
+ if (entry.key[0] !== 0x01) continue
406
+
407
+ const { key, length } = encoding.decodeSystemWriter(entry.key, entry.value)
408
+ if (!length) continue
409
+
410
+ const id = b4a.toString(key, 'hex')
411
+ const current = seen.get(id) || 0
412
+
413
+ if (current >= length) continue
414
+
415
+ seen.set(id, length)
416
+ nodes.push({ key, length })
417
+ }
418
+
419
+ return nodes
420
+ }
421
+
422
+ async function getWriterBatch(store, head, key, encryptionKey, nodes = []) {
423
+ const batch = []
424
+
425
+ const core = store.get(head.key)
426
+ await core.ready()
427
+
428
+ if (encryptionKey) {
429
+ await core.setEncryption(AutobeeEncryption.getWriterEncryption(key, encryptionKey))
430
+ }
431
+
432
+ let seq = head.length - 1
433
+ const block = await core.get(seq--, { wait: false })
434
+ if (!block) throw new Error('Expect writer node to exist locally')
435
+
436
+ const node = encoding.decodeOplog(block)
437
+
438
+ batch.unshift({ ...head, ...node, from: core })
439
+
440
+ while (seq >= 0) {
441
+ const block = await core.get(seq--, { wait: false })
442
+ if (!block) break
443
+
444
+ const node = encoding.decodeOplog(block)
445
+ if (!node.batch.end) break
446
+
447
+ batch.unshift({ key: head.key, length: seq + 2, from: core, ...node })
448
+ }
449
+
450
+ return batch
451
+ }
452
+
453
+ // lazy to avoid the require cycle - fast-forward imports this module
454
+ function flushHead(auto, head, opts) {
455
+ const FastForward = require('./fast-forward.js')
456
+ return FastForward.flushHead(auto, head, opts)
457
+ }
@@ -0,0 +1,164 @@
1
+ const b4a = require('b4a')
2
+ const encoding = require('./encoding.js')
3
+
4
+ // Owns the two promotion indexes: one hint per writer (the coordinate of the
5
+ // approval that anchored its capability - advisory, never verified against)
6
+ // and one pending entry per writer awaiting a stronger approval.
7
+ //
8
+ // An approval clears the entry as soon as it satisfies the request, so the
9
+ // index holds exactly the outstanding work. A partial approval leaves the
10
+ // entry alone: the request still wants more than the tier that answered could
11
+ // give.
12
+ //
13
+ // digest[w - 1] is true when some request needs an approver of standing w.
14
+ // The false is what makes a tier go quiet: once one tier-w peer has anchored
15
+ // w, the rest see their tier is finished instead of all offering in turn. A
16
+ // flag is enough because a satisfied request leaves the index entirely, so
17
+ // there is no "have I already seen this" watermark to keep.
18
+ module.exports = class Promotions {
19
+ constructor(bee) {
20
+ this.bee = bee
21
+ this.digest = []
22
+ this.changed = false
23
+ this._pending = null
24
+ }
25
+
26
+ reset(info) {
27
+ this.digest = info && info.pending ? info.pending : []
28
+ this._pending = null
29
+ }
30
+
31
+ async hint(key, { activeRequests } = {}) {
32
+ const node = await this.bee.get(encoding.encodeSystemGrantKey(key), { activeRequests })
33
+ return node ? encoding.decodeSystemGrantHint(node.value) : null
34
+ }
35
+
36
+ async pending(key, { activeRequests } = {}) {
37
+ if (this._pending !== null) {
38
+ const entry = this._pending.get(b4a.toString(key, 'hex'))
39
+ return entry ? entry.weight : 0
40
+ }
41
+
42
+ const node = await this.bee.get(encoding.encodeSystemPendingKey(key), { activeRequests })
43
+ return node ? encoding.decodeSystemPending(node.value).weight : 0
44
+ }
45
+
46
+ // the level a writer has already anchored is exactly what its hint records,
47
+ // so it is read from there rather than mirrored into the pending entry
48
+ async anchored(key, { activeRequests } = {}) {
49
+ const hint = await this.hint(key, { activeRequests })
50
+ return hint ? hint.weight : 0
51
+ }
52
+
53
+ async *list({ activeRequests } = {}) {
54
+ const pending = await this._load({ activeRequests })
55
+ for (const [hex, entry] of pending) {
56
+ yield { key: b4a.from(hex, 'hex'), weight: entry.weight }
57
+ }
58
+ }
59
+
60
+ async updateHint(upd) {
61
+ if (!upd.grant && !upd.isRemoved) return null
62
+
63
+ const key = encoding.encodeSystemGrantKey(upd.key)
64
+ const node = await this.bee.get(key)
65
+
66
+ if (upd.isRemoved) return node ? { key, remove: true } : null
67
+
68
+ const existing = node ? encoding.decodeSystemGrantHint(node.value) : null
69
+ if (existing && existing.weight >= upd.grant.weight) return null
70
+
71
+ return { key, value: encoding.encodeSystemGrantHint(upd.grant) }
72
+ }
73
+
74
+ // grouped and sequential on purpose: the entries are shared state, so
75
+ // per-update parallelism would interleave at the awaits
76
+ async flushPending(updates) {
77
+ let relevant = false
78
+ for (const upd of updates) {
79
+ if (upd.pending !== -1 || upd.grant !== null || upd.isRemoved) {
80
+ relevant = true
81
+ break
82
+ }
83
+ }
84
+ if (!relevant) return null
85
+
86
+ const pending = await this._load()
87
+ if (!pending.size && !updates.some((u) => u.pending !== -1)) return null
88
+
89
+ const ops = []
90
+ const anchoredBy = new Map()
91
+
92
+ for (const upd of updates) {
93
+ const hex = b4a.toString(upd.key, 'hex')
94
+ const existing = pending.get(hex) || null
95
+
96
+ if (upd.isRemoved) {
97
+ if (existing) {
98
+ pending.delete(hex)
99
+ ops.push({ key: encoding.encodeSystemPendingKey(upd.key), remove: true })
100
+ }
101
+ continue
102
+ }
103
+ if (upd.pending === -1 && upd.grant === null) continue
104
+
105
+ // the hint is still pre-write here, so this is the level already anchored
106
+ // before whatever this update confers
107
+ const prev = await this.anchored(upd.key)
108
+ const anchored = upd.grant && upd.grant.weight > prev ? upd.grant.weight : prev
109
+ anchoredBy.set(hex, anchored)
110
+
111
+ const want = Math.max(existing ? existing.weight : 0, upd.pending)
112
+
113
+ if (want > anchored) {
114
+ if (existing && want === existing.weight) continue
115
+ const entry = { weight: want }
116
+ pending.set(hex, entry)
117
+ ops.push({
118
+ key: encoding.encodeSystemPendingKey(upd.key),
119
+ value: encoding.encodeSystemPending(entry)
120
+ })
121
+ this.changed = true
122
+ } else if (existing) {
123
+ // satisfied - the approval covered everything the request asked for
124
+ pending.delete(hex)
125
+ ops.push({ key: encoding.encodeSystemPendingKey(upd.key), remove: true })
126
+ }
127
+ }
128
+
129
+ // the flags depend on the anchored level, not just on which entries exist -
130
+ // an approval that partially serves a request writes no entry at all, and
131
+ // its tier still has to go quiet
132
+ const open = new Set()
133
+ for (const [hex, entry] of pending) {
134
+ const anchored = anchoredBy.has(hex)
135
+ ? anchoredBy.get(hex)
136
+ : await this.anchored(b4a.from(hex, 'hex'))
137
+ for (let w = anchored + 1; w <= entry.weight; w++) open.add(w)
138
+ }
139
+
140
+ let max = 0
141
+ for (const w of open) if (w > max) max = w
142
+
143
+ this.digest = []
144
+ for (let w = 1; w <= max; w++) this.digest.push(open.has(w))
145
+
146
+ return ops.length ? ops : null
147
+ }
148
+
149
+ async _load({ activeRequests } = {}) {
150
+ if (this._pending !== null) return this._pending
151
+
152
+ const pending = new Map()
153
+ const range = encoding.encodeSystemPendingRange()
154
+ for await (const data of this.bee.createReadStream({ ...range, activeRequests })) {
155
+ pending.set(
156
+ b4a.toString(encoding.decodeSystemPendingKey(data.key), 'hex'),
157
+ encoding.decodeSystemPending(data.value)
158
+ )
159
+ }
160
+
161
+ this._pending = pending
162
+ return pending
163
+ }
164
+ }