@cero-base/core 1.10.1 → 1.12.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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "cero p2p primitives — identity, storage, network, database, blobs, rpc, pairing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
},
|
|
145
145
|
"dependencies": {
|
|
146
146
|
"@hyperswarm/secret-stream": "^6.9.1",
|
|
147
|
-
"autobee": "2.0.0-rc.
|
|
147
|
+
"autobee": "2.0.0-rc.19",
|
|
148
148
|
"autobee-encryption": "0.1.3",
|
|
149
149
|
"b4a": "^1.8.1",
|
|
150
150
|
"bare-crypto": "^1.15.3",
|
|
@@ -152,18 +152,18 @@
|
|
|
152
152
|
"bare-path": "^3.1.1",
|
|
153
153
|
"bip39-mnemonic": "^2.5.0",
|
|
154
154
|
"blind-pairing": "^2.3.1",
|
|
155
|
-
"blind-peering": "^2.6.
|
|
156
|
-
"compact-encoding": "^3.3.
|
|
155
|
+
"blind-peering": "^2.6.3",
|
|
156
|
+
"compact-encoding": "^3.3.2",
|
|
157
157
|
"corestore": "^7.12.0",
|
|
158
158
|
"framed-stream": "^1.0.1",
|
|
159
|
-
"hrpc": "^4.3.
|
|
159
|
+
"hrpc": "^4.3.1",
|
|
160
160
|
"hyperblobs": "^2.12.1",
|
|
161
|
-
"hypercore": "^11.35.
|
|
161
|
+
"hypercore": "^11.35.2",
|
|
162
162
|
"hypercore-blob-server": "^1.15.0",
|
|
163
163
|
"hypercore-crypto": "^3.7.0",
|
|
164
164
|
"hypercore-id-encoding": "^1.3.0",
|
|
165
|
-
"hypercore-storage": "^3.2.
|
|
166
|
-
"hyperdb": "^6.
|
|
165
|
+
"hypercore-storage": "^3.2.1",
|
|
166
|
+
"hyperdb": "^6.9.0",
|
|
167
167
|
"hyperdispatch": "^1.6.0",
|
|
168
168
|
"hyperschema": "^1.22.0",
|
|
169
169
|
"hyperswarm": "^4.17.0",
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
"bare-fetch": "^3.2.0",
|
|
183
183
|
"bare-process": "^4.5.1",
|
|
184
184
|
"bare-url": "^2.5.2",
|
|
185
|
-
"blind-peer": "^3.13.
|
|
185
|
+
"blind-peer": "^3.13.3",
|
|
186
186
|
"brittle": "^4.1.0",
|
|
187
187
|
"typescript": "^5.9.3",
|
|
188
188
|
"which-runtime": "^1.4.0"
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import b4a from 'b4a'
|
|
2
1
|
import Hypercore from 'hypercore'
|
|
3
2
|
import { Identity } from '../identity/index.js'
|
|
4
3
|
import { CeroError } from '../lib/errors.js'
|
|
5
4
|
import { toId, addWriterPayload } from '../lib/utils.js'
|
|
5
|
+
import { wrap } from './envelope.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* First-run device provisioning: mint a device writer keypair, persist it as a
|
|
9
9
|
* writer, then swap the autobee's local core over to it. With `recovering`,
|
|
10
|
-
*
|
|
10
|
+
* the swap happens FIRST and admission rides an optimistic append — the
|
|
11
|
+
* backfilled genesis core is never appended to (autobee's contract: a writable
|
|
12
|
+
* core has exactly one author, ever; resuming a replicated core is unsupported
|
|
13
|
+
* and drops appends silently).
|
|
11
14
|
*
|
|
12
15
|
* @param {import('./index.js').Database} db
|
|
13
16
|
* @param {{ name?: string | null, isMobile?: boolean, recovering?: boolean }} [opts]
|
|
@@ -22,87 +25,64 @@ export async function bootstrap(db, { name, isMobile, recovering = false, timeou
|
|
|
22
25
|
const writerKey = Hypercore.key(manifest)
|
|
23
26
|
|
|
24
27
|
if (recovering) {
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
await
|
|
30
|
-
|
|
31
|
-
if (recovering) {
|
|
32
|
-
// gate the swap on the admission actually applying: write() can resolve
|
|
33
|
-
// with the ops stalled in the drain (update() bumps heal that) or dropped
|
|
34
|
-
// outright when they were stamped behind a mid-replication core — each
|
|
35
|
-
// retry stamps past the collision, and the ops are idempotent
|
|
36
|
-
await admitWriter(db, writerKey, { name, isMobile }, timeout)
|
|
28
|
+
// swap before anything else: the genesis core then replicates as a REMOTE
|
|
29
|
+
// writer, so its backfilled ops apply through the normal download path
|
|
30
|
+
await swapWriter(db, keyPair, manifest)
|
|
31
|
+
await waitForBackfill(db, timeout)
|
|
32
|
+
await claimWriter(db, writerKey, timeout)
|
|
33
|
+
await saveDevice(db, writerKey, { name, isMobile })
|
|
37
34
|
} else {
|
|
38
35
|
await saveWriter(db, writerKey, { name, isMobile })
|
|
36
|
+
await swapWriter(db, keyPair, manifest)
|
|
39
37
|
}
|
|
40
|
-
await swapWriter(db, keyPair, manifest)
|
|
41
38
|
|
|
42
39
|
return { id: writerKey, writer: keyPair }
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// the first block may not be the whole core — writes stamped below the
|
|
50
|
-
// length the system already attributes to this writer are silently dropped
|
|
51
|
-
const info = await db.bee.system.get(db.bee.local.key).catch(() => null)
|
|
52
|
-
if (info && info.length > db.bee.local.length) await waitForLength(db, info.length, timeout)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function waitForLength(db, length, timeout) {
|
|
56
|
-
return new Promise((resolve, reject) => {
|
|
57
|
-
let done = false
|
|
58
|
-
const finish = (err) => {
|
|
59
|
-
if (done) return
|
|
60
|
-
done = true
|
|
61
|
-
clearTimeout(timer)
|
|
62
|
-
db.bee.local.off('append', onAppend)
|
|
63
|
-
db.off('close', onClose)
|
|
64
|
-
if (err) reject(err)
|
|
65
|
-
else resolve()
|
|
66
|
-
}
|
|
67
|
-
// timeout + close handling so a peer that never replicates can't hang the
|
|
68
|
-
// recovery forever, and the append listener is always removed.
|
|
69
|
-
const onAppend = () => db.bee.local.length >= length && finish()
|
|
70
|
-
const onClose = () => finish(CeroError.CLOSED('Database'))
|
|
71
|
-
const timer = setTimeout(
|
|
72
|
-
() => finish(CeroError.TIMED_OUT('recovery — no peer append')),
|
|
73
|
-
timeout
|
|
74
|
-
)
|
|
75
|
-
db.bee.local.on('append', onAppend)
|
|
76
|
-
db.on('close', onClose)
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function admitWriter(db, writerKey, opts, timeout) {
|
|
81
|
-
const deadline = Date.now() + timeout
|
|
82
|
-
const id = toId(writerKey)
|
|
83
|
-
while (true) {
|
|
84
|
-
await saveWriter(db, writerKey, opts)
|
|
85
|
-
if (await deviceApplied(db, id, Math.min(3000, deadline - Date.now()))) return
|
|
86
|
-
if (Date.now() >= deadline) throw CeroError.TIMED_OUT('recovery — writer not admitted')
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function deviceApplied(db, id, timeout) {
|
|
42
|
+
// recovery has state to recover by definition — wait until replication has
|
|
43
|
+
// delivered and applied it (the genesis device row is the first thing every
|
|
44
|
+
// db writes), so a peer that never replicates fails loud instead of hanging
|
|
45
|
+
async function waitForBackfill(db, timeout) {
|
|
91
46
|
const deadline = Date.now() + timeout
|
|
92
47
|
while (Date.now() < deadline) {
|
|
93
|
-
|
|
94
|
-
|
|
48
|
+
if (db.closing || db.closed) throw CeroError.CLOSED('Database')
|
|
49
|
+
const { data } = await db.get('devices')
|
|
50
|
+
if (data.length > 0) return
|
|
95
51
|
await db.bee.update()
|
|
96
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
97
53
|
}
|
|
98
|
-
|
|
54
|
+
throw CeroError.TIMED_OUT('recovery — no peer replicated')
|
|
99
55
|
}
|
|
100
56
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
57
|
+
// self-admission from the fresh core: the same master-signed add-writer op a
|
|
58
|
+
// live device would write, appended optimistically so an unadmitted writer
|
|
59
|
+
// can carry it — apply verifies the attestation and admits the core
|
|
60
|
+
async function claimWriter(db, writerKey, timeout) {
|
|
61
|
+
const encoded = db.spec.dispatch.encode(`@${db.ns}/add-writer`, {
|
|
62
|
+
master: db.identity.publicKey,
|
|
63
|
+
writer: writerKey,
|
|
64
|
+
sig: db.identity.sign(addWriterPayload(db.key, writerKey, db.writerKey)),
|
|
65
|
+
ts: Date.now()
|
|
66
|
+
})
|
|
67
|
+
await db.bee.append(wrap(db.version, encoded), { optimistic: true })
|
|
68
|
+
await db.bee.update()
|
|
69
|
+
if (!db.bee.writable) await db.whenWritable({ timeout })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function saveDevice(db, writerKey, { name, isMobile }) {
|
|
73
|
+
const ts = Date.now()
|
|
74
|
+
await db.write([
|
|
75
|
+
[
|
|
76
|
+
'set-device',
|
|
77
|
+
{
|
|
78
|
+
id: toId(writerKey),
|
|
79
|
+
name: name || null,
|
|
80
|
+
isMobile: isMobile === true,
|
|
81
|
+
createdAt: ts,
|
|
82
|
+
updatedAt: ts
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
])
|
|
106
86
|
}
|
|
107
87
|
|
|
108
88
|
async function saveWriter(db, writerKey, { name, isMobile }) {
|
package/src/database/dispatch.js
CHANGED
|
@@ -114,7 +114,11 @@ export function makeDispatcher(
|
|
|
114
114
|
if (!isKey(op.master) || !isKey(op.writer) || !isSig(op.sig)) return
|
|
115
115
|
if (!Identity.verify(op.master, addWriterPayload(ctx.dbKey, op.writer, ctx.key), op.sig)) return
|
|
116
116
|
if (!(await isGenesis(ctx.view)) && !can(await getRole(ctx.view, op.master), INVITE)) return
|
|
117
|
-
|
|
117
|
+
// always an indexer, like claim-writer below. The op's optional isIndexer
|
|
118
|
+
// bool decodes to false when absent (nothing sets it), which silently made
|
|
119
|
+
// every device a weight-1 writer — autobee 2.0 gc's caught-up non-indexer
|
|
120
|
+
// sessions, and a gc'd writer misses later appends until a wakeup revives it
|
|
121
|
+
await ctx.host.addWriter(op.writer, { isIndexer: true })
|
|
118
122
|
const ts = op.ts || 0
|
|
119
123
|
await insert(ctx.view, 'devices', `@${ns}/devices`, {
|
|
120
124
|
id: toId(op.writer),
|
|
@@ -230,54 +230,36 @@ export class EpochAutobee extends Autobee {
|
|
|
230
230
|
await primeKeyring(this.keyring, this.local)
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
// instead of crashing the bee.
|
|
233
|
+
// A writer whose next blocks sit at an epoch we haven't learned yet parks:
|
|
234
|
+
// its next() reports "nothing to offer" instead of throwing, so upstream's
|
|
235
|
+
// drain keeps selecting among the other writers — the announcement carrying
|
|
236
|
+
// the missing epoch lives in one of THEIR cores and unlocks the parked
|
|
237
|
+
// writer on a later bump (the scheduled retry pokes the drain). The outer
|
|
238
|
+
// catch backstops an UNKNOWN_EPOCH surfacing from batch processing itself.
|
|
240
239
|
async _bumpPendingWriters() {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
for (const w of this.writers.pending) this._parkOnUnknownEpoch(w)
|
|
241
|
+
try {
|
|
242
|
+
return await super._bumpPendingWriters()
|
|
243
|
+
} catch (err) {
|
|
244
|
+
if (err?.code !== 'UNKNOWN_EPOCH') throw err
|
|
245
|
+
this._scheduleEpochRetry()
|
|
246
|
+
return false
|
|
244
247
|
}
|
|
248
|
+
}
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const w = pending[i]
|
|
252
|
-
|
|
250
|
+
_parkOnUnknownEpoch(w) {
|
|
251
|
+
if (w._epochParking) return
|
|
252
|
+
w._epochParking = true
|
|
253
|
+
const next = w.next.bind(w)
|
|
254
|
+
w.next = async () => {
|
|
253
255
|
try {
|
|
254
|
-
|
|
255
|
-
if (batch === null) continue
|
|
256
|
-
|
|
257
|
-
if (w.isAdded || (w.isRemoved && w.hasReferrals())) {
|
|
258
|
-
await this._processBatch(batch)
|
|
259
|
-
w.notify(batch)
|
|
260
|
-
updated = true
|
|
261
|
-
continue
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (this.optimistic && !w.isRemoved && batch[0].optimistic) {
|
|
265
|
-
if (!(await this._optimisticBatch(batch))) {
|
|
266
|
-
w.removePending()
|
|
267
|
-
continue
|
|
268
|
-
}
|
|
269
|
-
w.notify(batch)
|
|
270
|
-
updated = true
|
|
271
|
-
continue
|
|
272
|
-
}
|
|
256
|
+
return await next()
|
|
273
257
|
} catch (err) {
|
|
274
258
|
if (err?.code !== 'UNKNOWN_EPOCH') throw err
|
|
275
259
|
this._scheduleEpochRetry()
|
|
276
|
-
|
|
260
|
+
return null
|
|
277
261
|
}
|
|
278
262
|
}
|
|
279
|
-
|
|
280
|
-
return updated
|
|
281
263
|
}
|
|
282
264
|
|
|
283
265
|
// a parked writer produces no wake-up of its own — poke the bee until the
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* First-run device provisioning: mint a device writer keypair, persist it as a
|
|
3
3
|
* writer, then swap the autobee's local core over to it. With `recovering`,
|
|
4
|
-
*
|
|
4
|
+
* the swap happens FIRST and admission rides an optimistic append — the
|
|
5
|
+
* backfilled genesis core is never appended to (autobee's contract: a writable
|
|
6
|
+
* core has exactly one author, ever; resuming a replicated core is unsupported
|
|
7
|
+
* and drops appends silently).
|
|
5
8
|
*
|
|
6
9
|
* @param {import('./index.js').Database} db
|
|
7
10
|
* @param {{ name?: string | null, isMobile?: boolean, recovering?: boolean }} [opts]
|
|
@@ -11,13 +11,20 @@ export function blobEpochKey(entropy: Uint8Array): Uint8Array;
|
|
|
11
11
|
* Wire codec for a rotation announcement's envelope list — one sealed box
|
|
12
12
|
* per remaining member, addressed by member id.
|
|
13
13
|
*/
|
|
14
|
-
export const wraps: any
|
|
14
|
+
export const wraps: c.Encoder<any[], {
|
|
15
|
+
id: string;
|
|
16
|
+
box: Uint8Array<ArrayBufferLike>;
|
|
17
|
+
}[]>;
|
|
15
18
|
/**
|
|
16
19
|
* Wire codec for locally persisted / pairing-delivered epoch secrets.
|
|
17
20
|
* `epoch` is the apply-order sequence; `stamp` is the content-addressed
|
|
18
21
|
* uint32 written into block headers.
|
|
19
22
|
*/
|
|
20
|
-
export const epochEntries: any
|
|
23
|
+
export const epochEntries: c.Encoder<any[], {
|
|
24
|
+
epoch: number;
|
|
25
|
+
stamp: number;
|
|
26
|
+
entropy: Uint8Array<ArrayBufferLike>;
|
|
27
|
+
}[]>;
|
|
21
28
|
/**
|
|
22
29
|
* Per-database registry of rotation epochs. Stamp 0 is the base key era
|
|
23
30
|
* (no entry needed — it derives from the upstream GENESIS_ENTROPY path).
|
|
@@ -83,8 +90,9 @@ export class EpochAutobee {
|
|
|
83
90
|
_epochRetryDelay: number;
|
|
84
91
|
_epochRetrySeen: number;
|
|
85
92
|
_bootState(): Promise<void>;
|
|
86
|
-
_bumpPendingWriters(): Promise<
|
|
87
|
-
|
|
93
|
+
_bumpPendingWriters(): Promise<any>;
|
|
94
|
+
_parkOnUnknownEpoch(w: any): void;
|
|
88
95
|
_scheduleEpochRetry(): void;
|
|
89
96
|
_close(): Promise<any>;
|
|
90
97
|
}
|
|
98
|
+
import c from 'compact-encoding';
|