@optimystic/db-core 0.25.1 → 0.27.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/dist/src/cohort-topic/membership/verifier.d.ts +8 -0
- package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -1
- package/dist/src/cohort-topic/membership/verifier.js +17 -0
- package/dist/src/cohort-topic/membership/verifier.js.map +1 -1
- package/dist/src/cohort-topic/promotion.d.ts +25 -0
- package/dist/src/cohort-topic/promotion.d.ts.map +1 -1
- package/dist/src/cohort-topic/promotion.js +58 -11
- package/dist/src/cohort-topic/promotion.js.map +1 -1
- package/dist/src/cohort-topic/wire/primitives.d.ts +6 -0
- package/dist/src/cohort-topic/wire/primitives.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/primitives.js +6 -0
- package/dist/src/cohort-topic/wire/primitives.js.map +1 -1
- package/dist/src/cohort-topic/wire/types.d.ts +5 -0
- package/dist/src/cohort-topic/wire/types.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/validate.js +10 -19
- package/dist/src/cohort-topic/wire/validate.js.map +1 -1
- package/dist/src/collection/collection.d.ts +8 -1
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +19 -3
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/matchmaking/wire.d.ts +2 -2
- package/dist/src/matchmaking/wire.d.ts.map +1 -1
- package/dist/src/matchmaking/wire.js +3 -3
- package/dist/src/matchmaking/wire.js.map +1 -1
- package/dist/src/network/struct.d.ts +5 -5
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/testing/index.d.ts +1 -0
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +1 -0
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/testing/refresh-probe.d.ts +31 -0
- package/dist/src/testing/refresh-probe.d.ts.map +1 -0
- package/dist/src/testing/refresh-probe.js +39 -0
- package/dist/src/testing/refresh-probe.js.map +1 -0
- package/dist/src/testing/test-transactor.d.ts +58 -5
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +87 -4
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +90 -6
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +311 -90
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/errors.d.ts +42 -0
- package/dist/src/transaction/errors.d.ts.map +1 -1
- package/dist/src/transaction/errors.js +50 -0
- package/dist/src/transaction/errors.js.map +1 -1
- package/dist/src/transaction/index.d.ts +1 -1
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js +1 -1
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transaction/session.d.ts +7 -3
- package/dist/src/transaction/session.d.ts.map +1 -1
- package/dist/src/transaction/session.js +7 -3
- package/dist/src/transaction/session.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +12 -2
- package/dist/src/transaction/transaction.d.ts.map +1 -1
- package/dist/src/transaction/transaction.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +6 -26
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +4 -6
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +4 -6
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/atomic.d.ts.map +1 -1
- package/dist/src/transform/atomic.js +7 -0
- package/dist/src/transform/atomic.js.map +1 -1
- package/dist/src/transform/base-pins.d.ts +48 -0
- package/dist/src/transform/base-pins.d.ts.map +1 -0
- package/dist/src/transform/base-pins.js +65 -0
- package/dist/src/transform/base-pins.js.map +1 -0
- package/dist/src/transform/digest.d.ts +4 -2
- package/dist/src/transform/digest.d.ts.map +1 -1
- package/dist/src/transform/digest.js +14 -19
- package/dist/src/transform/digest.js.map +1 -1
- package/dist/src/transform/tracker.d.ts +31 -3
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +105 -13
- package/dist/src/transform/tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/cohort-topic/membership/verifier.ts +487 -461
- package/src/cohort-topic/promotion.ts +86 -14
- package/src/cohort-topic/wire/primitives.ts +195 -188
- package/src/cohort-topic/wire/types.ts +480 -475
- package/src/cohort-topic/wire/validate.ts +504 -512
- package/src/collection/collection.ts +19 -3
- package/src/matchmaking/wire.ts +605 -603
- package/src/network/struct.ts +5 -5
- package/src/testing/index.ts +1 -0
- package/src/testing/refresh-probe.ts +52 -0
- package/src/testing/test-transactor.ts +100 -4
- package/src/transaction/coordinator.ts +312 -99
- package/src/transaction/errors.ts +141 -91
- package/src/transaction/index.ts +1 -1
- package/src/transaction/session.ts +7 -3
- package/src/transaction/transaction.ts +12 -2
- package/src/transactor/network-transactor.ts +1025 -1045
- package/src/transactor/transactor-source.ts +170 -172
- package/src/transform/atomic.ts +6 -0
- package/src/transform/base-pins.ts +83 -0
- package/src/transform/digest.ts +14 -19
- package/src/transform/tracker.ts +108 -15
|
@@ -1,475 +1,480 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cohort-topic substrate — V1 wire message types.
|
|
3
|
-
*
|
|
4
|
-
* Transcribed from `docs/cohort-topic.md` §Wire formats. These interfaces are the canonical
|
|
5
|
-
* on-the-wire shapes for the cohort-topic RPC surface; the codec in `./codec.ts` serializes
|
|
6
|
-
* them as length-prefixed UTF-8 JSON.
|
|
7
|
-
*
|
|
8
|
-
* Conventions (mandated by §Wire formats):
|
|
9
|
-
* - Every byte-typed field is carried as a base64url string (no padding) — never raw bytes.
|
|
10
|
-
* - Every timestamp is unix milliseconds.
|
|
11
|
-
* - `appPayload` is an opaque base64url string carrying application-defined bytes. The wire layer
|
|
12
|
-
* never interprets it; reactivity / matchmaking define their own structures and serialize them
|
|
13
|
-
* into this slot.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/** Registration request. Walks toward the root via FRET `RouteAndMaybeAct`. */
|
|
17
|
-
export interface RegisterV1 {
|
|
18
|
-
v: 1;
|
|
19
|
-
/** Topic id, 32 bytes, base64url. */
|
|
20
|
-
topicId: string;
|
|
21
|
-
/** Tier, 0..3. */
|
|
22
|
-
tier: number;
|
|
23
|
-
/** Current walk position `d`. */
|
|
24
|
-
treeTier: number;
|
|
25
|
-
/** Participant's ring coord, 32 bytes, base64url. */
|
|
26
|
-
participantCoord: string;
|
|
27
|
-
/** Registration TTL in ms (default 90000; Edge 60000). */
|
|
28
|
-
ttl: number;
|
|
29
|
-
/** True on a root cold-start request. */
|
|
30
|
-
bootstrap?: boolean;
|
|
31
|
-
/**
|
|
32
|
-
* Follow-on cold-start request: set on the **dedicated re-issue** the participant sends after a
|
|
33
|
-
* `Promoted` redirect target answered {@link RegisterResult} `no_state` (the redirect points at a
|
|
34
|
-
* tier-`(d+1)` child cohort that does not exist yet). It tells that cold child "I am here because
|
|
35
|
-
* your parent redirected me — instantiate", which drives the cold-start gate
|
|
36
|
-
* (`shouldInstantiate({ followOn, … })`) exactly as `bootstrap` drives it at the root. Constraints:
|
|
37
|
-
* always `treeTier >= 1` (a follow-on is a deeper-than-root growth point, never the root); mutually
|
|
38
|
-
* exclusive with {@link bootstrap} and {@link probe} (the walk sets at most one). Because a wire flag is
|
|
39
|
-
* participant-forgeable, safety does NOT come from its provenance: a `followOn: true` register is gated
|
|
40
|
-
* by the **same** `bootstrapEvidence` policy a `bootstrap: true` cold-root register passes (§Anti-DoS),
|
|
41
|
-
* so it carries the identical proof in {@link bootstrapEvidence} and pays the identical anti-abuse cost.
|
|
42
|
-
* Covered by `signature` (appended to `registerSigningPayload`) so a MITM cannot strip or flip it.
|
|
43
|
-
*/
|
|
44
|
-
followOn?: boolean;
|
|
45
|
-
/**
|
|
46
|
-
* Read-only lookup probe: classify + return the cohort snapshot without admitting (no record, no
|
|
47
|
-
* arrival, no promotion trigger, no topic-budget touch, and **never** a cold-start instantiation /
|
|
48
|
-
* `bootstrap`). A probe walks to the responsible cohort exactly as a register does and resolves the
|
|
49
|
-
* same {@link RegisterReplyV1} (`no_state` cold, `promoted` redirected, `accepted` snapshot served),
|
|
50
|
-
* but the terminal member action is the read-only {@link RegisterReplyV1} classify rather than an
|
|
51
|
-
* admission. Drives {@link import("../service.js").CohortTopicService.lookup}. Mutually exclusive with
|
|
52
|
-
* `bootstrap` (the walk never sets `bootstrap` on a probe).
|
|
53
|
-
*/
|
|
54
|
-
probe?: boolean;
|
|
55
|
-
/** Opaque application-defined bytes, base64url. */
|
|
56
|
-
appPayload?: string;
|
|
57
|
-
/**
|
|
58
|
-
* Cold-start bootstrap-evidence envelope, base64url (a `BootstrapEvidenceEnvelopeV1` — see
|
|
59
|
-
* `../antidos/bootstrap-evidence-envelope.js`). Present on a `bootstrap: true` root register **or** a
|
|
60
|
-
* `followOn: true` deeper-tier re-issue — both are cold-start requests gated by the identical evidence
|
|
61
|
-
* policy (§Anti-DoS), so both carry this proof; carries the tier-dependent proof a cold cohort demands
|
|
62
|
-
* (proof-of-work / reputation endorsement / signed parent reference — §Anti-DoS). This is a
|
|
63
|
-
* **dedicated** field, NOT `appPayload`: the cohort
|
|
64
|
-
* copies `appPayload` verbatim into the registration's `appState` and replicates it cluster-wide,
|
|
65
|
-
* whereas the bootstrap evidence is parsed-and-checked by the substrate, **covered by `signature`**
|
|
66
|
-
* (so a MITM cannot strip or swap it), and never stored as appState. Empty string is treated as
|
|
67
|
-
* absent (it normalizes to the same signed-image placeholder as an absent field).
|
|
68
|
-
*/
|
|
69
|
-
bootstrapEvidence?: string;
|
|
70
|
-
/** Unix ms. */
|
|
71
|
-
timestamp: number;
|
|
72
|
-
/** 16 random bytes, base64url. */
|
|
73
|
-
correlationId: string;
|
|
74
|
-
/** Participant peer-key signature, base64url. */
|
|
75
|
-
signature: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export type RegisterResult =
|
|
79
|
-
| "accepted"
|
|
80
|
-
| "no_state"
|
|
81
|
-
| "promoted"
|
|
82
|
-
| "unwilling_member"
|
|
83
|
-
| "unwilling_cohort";
|
|
84
|
-
|
|
85
|
-
/** Registration reply. Field presence is keyed by {@link RegisterResult}. */
|
|
86
|
-
export interface RegisterReplyV1 {
|
|
87
|
-
v: 1;
|
|
88
|
-
result: RegisterResult;
|
|
89
|
-
// accepted:
|
|
90
|
-
/** PeerId. */
|
|
91
|
-
primary?: string;
|
|
92
|
-
/** PeerIds, 1-2. */
|
|
93
|
-
backups?: string[];
|
|
94
|
-
/** 32 bytes, base64url. */
|
|
95
|
-
cohortEpoch?: string;
|
|
96
|
-
/** Full cohort PeerIds, for client cache. */
|
|
97
|
-
cohortMembers?: string[];
|
|
98
|
-
/** Present on `accepted` and `promoted` only. */
|
|
99
|
-
topicTraffic?: TopicTrafficV1;
|
|
100
|
-
// promoted:
|
|
101
|
-
/** `d+1` typically; may leap. */
|
|
102
|
-
targetTier?: number;
|
|
103
|
-
// unwilling_member:
|
|
104
|
-
/** PeerIds within the same cohort to try. */
|
|
105
|
-
candidateMembers?: string[];
|
|
106
|
-
// unwilling_cohort:
|
|
107
|
-
retryAfterMs?: number;
|
|
108
|
-
/** Human-readable, optional. */
|
|
109
|
-
reason?: string;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Coarse traffic barometer attached to accepted/promoted replies. */
|
|
113
|
-
export interface TopicTrafficV1 {
|
|
114
|
-
windowSeconds: number;
|
|
115
|
-
arrivalsPerMin: number;
|
|
116
|
-
queriesPerMin: number;
|
|
117
|
-
directParticipants: number;
|
|
118
|
-
childCohortCount: number;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Registration renewal (ping). */
|
|
122
|
-
export interface RenewV1 {
|
|
123
|
-
v: 1;
|
|
124
|
-
topicId: string;
|
|
125
|
-
/** PeerId. */
|
|
126
|
-
participantId: string;
|
|
127
|
-
/** Matches the original {@link RegisterV1}. */
|
|
128
|
-
correlationId: string;
|
|
129
|
-
timestamp: number;
|
|
130
|
-
/**
|
|
131
|
-
* True on a crash-failover re-attach — the participant attests it could not reach its primary and
|
|
132
|
-
* asks the contacted backup to promote itself. Absent/false on a normal ping. Signed (part of the
|
|
133
|
-
* renew body) so a member can trust the attestation and a MITM cannot flip a ping into a promotion.
|
|
134
|
-
*/
|
|
135
|
-
reattach?: boolean;
|
|
136
|
-
/**
|
|
137
|
-
* True on a withdraw tombstone — the participant attests it is leaving; the holder evicts the
|
|
138
|
-
* record immediately and gossips the eviction (freeing the cohort slot rather than holding it for a
|
|
139
|
-
* full TTL). Absent/false on a ping or reattach. Signed (part of the renew body) so a third party
|
|
140
|
-
* cannot evict someone else's registration. Mutually exclusive with {@link reattach} (the participant
|
|
141
|
-
* never sets both; on a malformed frame that sets both, the cohort side honors `withdraw`).
|
|
142
|
-
*/
|
|
143
|
-
withdraw?: boolean;
|
|
144
|
-
signature: string;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Renewal reply. */
|
|
148
|
-
export interface RenewReplyV1 {
|
|
149
|
-
v: 1;
|
|
150
|
-
/**
|
|
151
|
-
* `withdrawn` answers a {@link RenewV1.withdraw} tombstone the holder accepted (record evicted +
|
|
152
|
-
* gossiped); the participant ignores it (it is leaving), but the distinct result lets the cohort
|
|
153
|
-
* side branch unambiguously (re-touch the topic budget down, never count an arrival).
|
|
154
|
-
*/
|
|
155
|
-
result: "ok" | "unknown_registration" | "primary_moved" | "withdrawn";
|
|
156
|
-
// primary_moved:
|
|
157
|
-
newPrimary?: string;
|
|
158
|
-
newBackups?: string[];
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* `
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
tier
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
*
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
*
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
*
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* `
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
*
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
* attestation
|
|
451
|
-
*
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
|
469
|
-
|
|
|
470
|
-
|
|
|
471
|
-
|
|
|
472
|
-
|
|
|
473
|
-
|
|
|
474
|
-
|
|
|
475
|
-
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — V1 wire message types.
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Wire formats. These interfaces are the canonical
|
|
5
|
+
* on-the-wire shapes for the cohort-topic RPC surface; the codec in `./codec.ts` serializes
|
|
6
|
+
* them as length-prefixed UTF-8 JSON.
|
|
7
|
+
*
|
|
8
|
+
* Conventions (mandated by §Wire formats):
|
|
9
|
+
* - Every byte-typed field is carried as a base64url string (no padding) — never raw bytes.
|
|
10
|
+
* - Every timestamp is unix milliseconds.
|
|
11
|
+
* - `appPayload` is an opaque base64url string carrying application-defined bytes. The wire layer
|
|
12
|
+
* never interprets it; reactivity / matchmaking define their own structures and serialize them
|
|
13
|
+
* into this slot.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Registration request. Walks toward the root via FRET `RouteAndMaybeAct`. */
|
|
17
|
+
export interface RegisterV1 {
|
|
18
|
+
v: 1;
|
|
19
|
+
/** Topic id, 32 bytes, base64url. */
|
|
20
|
+
topicId: string;
|
|
21
|
+
/** Tier, 0..3. */
|
|
22
|
+
tier: number;
|
|
23
|
+
/** Current walk position `d`. */
|
|
24
|
+
treeTier: number;
|
|
25
|
+
/** Participant's ring coord, 32 bytes, base64url. */
|
|
26
|
+
participantCoord: string;
|
|
27
|
+
/** Registration TTL in ms (default 90000; Edge 60000). */
|
|
28
|
+
ttl: number;
|
|
29
|
+
/** True on a root cold-start request. */
|
|
30
|
+
bootstrap?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Follow-on cold-start request: set on the **dedicated re-issue** the participant sends after a
|
|
33
|
+
* `Promoted` redirect target answered {@link RegisterResult} `no_state` (the redirect points at a
|
|
34
|
+
* tier-`(d+1)` child cohort that does not exist yet). It tells that cold child "I am here because
|
|
35
|
+
* your parent redirected me — instantiate", which drives the cold-start gate
|
|
36
|
+
* (`shouldInstantiate({ followOn, … })`) exactly as `bootstrap` drives it at the root. Constraints:
|
|
37
|
+
* always `treeTier >= 1` (a follow-on is a deeper-than-root growth point, never the root); mutually
|
|
38
|
+
* exclusive with {@link bootstrap} and {@link probe} (the walk sets at most one). Because a wire flag is
|
|
39
|
+
* participant-forgeable, safety does NOT come from its provenance: a `followOn: true` register is gated
|
|
40
|
+
* by the **same** `bootstrapEvidence` policy a `bootstrap: true` cold-root register passes (§Anti-DoS),
|
|
41
|
+
* so it carries the identical proof in {@link bootstrapEvidence} and pays the identical anti-abuse cost.
|
|
42
|
+
* Covered by `signature` (appended to `registerSigningPayload`) so a MITM cannot strip or flip it.
|
|
43
|
+
*/
|
|
44
|
+
followOn?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Read-only lookup probe: classify + return the cohort snapshot without admitting (no record, no
|
|
47
|
+
* arrival, no promotion trigger, no topic-budget touch, and **never** a cold-start instantiation /
|
|
48
|
+
* `bootstrap`). A probe walks to the responsible cohort exactly as a register does and resolves the
|
|
49
|
+
* same {@link RegisterReplyV1} (`no_state` cold, `promoted` redirected, `accepted` snapshot served),
|
|
50
|
+
* but the terminal member action is the read-only {@link RegisterReplyV1} classify rather than an
|
|
51
|
+
* admission. Drives {@link import("../service.js").CohortTopicService.lookup}. Mutually exclusive with
|
|
52
|
+
* `bootstrap` (the walk never sets `bootstrap` on a probe).
|
|
53
|
+
*/
|
|
54
|
+
probe?: boolean;
|
|
55
|
+
/** Opaque application-defined bytes, base64url. */
|
|
56
|
+
appPayload?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Cold-start bootstrap-evidence envelope, base64url (a `BootstrapEvidenceEnvelopeV1` — see
|
|
59
|
+
* `../antidos/bootstrap-evidence-envelope.js`). Present on a `bootstrap: true` root register **or** a
|
|
60
|
+
* `followOn: true` deeper-tier re-issue — both are cold-start requests gated by the identical evidence
|
|
61
|
+
* policy (§Anti-DoS), so both carry this proof; carries the tier-dependent proof a cold cohort demands
|
|
62
|
+
* (proof-of-work / reputation endorsement / signed parent reference — §Anti-DoS). This is a
|
|
63
|
+
* **dedicated** field, NOT `appPayload`: the cohort
|
|
64
|
+
* copies `appPayload` verbatim into the registration's `appState` and replicates it cluster-wide,
|
|
65
|
+
* whereas the bootstrap evidence is parsed-and-checked by the substrate, **covered by `signature`**
|
|
66
|
+
* (so a MITM cannot strip or swap it), and never stored as appState. Empty string is treated as
|
|
67
|
+
* absent (it normalizes to the same signed-image placeholder as an absent field).
|
|
68
|
+
*/
|
|
69
|
+
bootstrapEvidence?: string;
|
|
70
|
+
/** Unix ms. */
|
|
71
|
+
timestamp: number;
|
|
72
|
+
/** 16 random bytes, base64url. */
|
|
73
|
+
correlationId: string;
|
|
74
|
+
/** Participant peer-key signature, base64url. */
|
|
75
|
+
signature: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type RegisterResult =
|
|
79
|
+
| "accepted"
|
|
80
|
+
| "no_state"
|
|
81
|
+
| "promoted"
|
|
82
|
+
| "unwilling_member"
|
|
83
|
+
| "unwilling_cohort";
|
|
84
|
+
|
|
85
|
+
/** Registration reply. Field presence is keyed by {@link RegisterResult}. */
|
|
86
|
+
export interface RegisterReplyV1 {
|
|
87
|
+
v: 1;
|
|
88
|
+
result: RegisterResult;
|
|
89
|
+
// accepted:
|
|
90
|
+
/** PeerId. */
|
|
91
|
+
primary?: string;
|
|
92
|
+
/** PeerIds, 1-2. */
|
|
93
|
+
backups?: string[];
|
|
94
|
+
/** 32 bytes, base64url. */
|
|
95
|
+
cohortEpoch?: string;
|
|
96
|
+
/** Full cohort PeerIds, for client cache. */
|
|
97
|
+
cohortMembers?: string[];
|
|
98
|
+
/** Present on `accepted` and `promoted` only. */
|
|
99
|
+
topicTraffic?: TopicTrafficV1;
|
|
100
|
+
// promoted:
|
|
101
|
+
/** `d+1` typically; may leap. */
|
|
102
|
+
targetTier?: number;
|
|
103
|
+
// unwilling_member:
|
|
104
|
+
/** PeerIds within the same cohort to try. */
|
|
105
|
+
candidateMembers?: string[];
|
|
106
|
+
// unwilling_cohort:
|
|
107
|
+
retryAfterMs?: number;
|
|
108
|
+
/** Human-readable, optional. */
|
|
109
|
+
reason?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Coarse traffic barometer attached to accepted/promoted replies. */
|
|
113
|
+
export interface TopicTrafficV1 {
|
|
114
|
+
windowSeconds: number;
|
|
115
|
+
arrivalsPerMin: number;
|
|
116
|
+
queriesPerMin: number;
|
|
117
|
+
directParticipants: number;
|
|
118
|
+
childCohortCount: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Registration renewal (ping). */
|
|
122
|
+
export interface RenewV1 {
|
|
123
|
+
v: 1;
|
|
124
|
+
topicId: string;
|
|
125
|
+
/** PeerId. */
|
|
126
|
+
participantId: string;
|
|
127
|
+
/** Matches the original {@link RegisterV1}. */
|
|
128
|
+
correlationId: string;
|
|
129
|
+
timestamp: number;
|
|
130
|
+
/**
|
|
131
|
+
* True on a crash-failover re-attach — the participant attests it could not reach its primary and
|
|
132
|
+
* asks the contacted backup to promote itself. Absent/false on a normal ping. Signed (part of the
|
|
133
|
+
* renew body) so a member can trust the attestation and a MITM cannot flip a ping into a promotion.
|
|
134
|
+
*/
|
|
135
|
+
reattach?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* True on a withdraw tombstone — the participant attests it is leaving; the holder evicts the
|
|
138
|
+
* record immediately and gossips the eviction (freeing the cohort slot rather than holding it for a
|
|
139
|
+
* full TTL). Absent/false on a ping or reattach. Signed (part of the renew body) so a third party
|
|
140
|
+
* cannot evict someone else's registration. Mutually exclusive with {@link reattach} (the participant
|
|
141
|
+
* never sets both; on a malformed frame that sets both, the cohort side honors `withdraw`).
|
|
142
|
+
*/
|
|
143
|
+
withdraw?: boolean;
|
|
144
|
+
signature: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Renewal reply. */
|
|
148
|
+
export interface RenewReplyV1 {
|
|
149
|
+
v: 1;
|
|
150
|
+
/**
|
|
151
|
+
* `withdrawn` answers a {@link RenewV1.withdraw} tombstone the holder accepted (record evicted +
|
|
152
|
+
* gossiped); the participant ignores it (it is leaving), but the distinct result lets the cohort
|
|
153
|
+
* side branch unambiguously (re-touch the topic budget down, never count an arrival).
|
|
154
|
+
*/
|
|
155
|
+
result: "ok" | "unknown_registration" | "primary_moved" | "withdrawn";
|
|
156
|
+
// primary_moved:
|
|
157
|
+
newPrimary?: string;
|
|
158
|
+
newBackups?: string[];
|
|
159
|
+
/** 32 bytes, base64url. */
|
|
160
|
+
cohortEpoch?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Child cohort → parent cohort link: "I am the tier-`d` cohort at {@link childCohortCoord}; record me as
|
|
165
|
+
* your child." A freshly cold-started deeper forwarder sends this to its tier-`(d − 1)` parent (routed the
|
|
166
|
+
* same way a participant register is, over `routeAndAct` to the parent coord) so the parent authenticates,
|
|
167
|
+
* records the child, and acks — replacing the interim plain participant-register the child used to send.
|
|
168
|
+
*/
|
|
169
|
+
export interface ChildLinkV1 {
|
|
170
|
+
v: 1;
|
|
171
|
+
/** Topic id, 32 bytes, base64url. */
|
|
172
|
+
topicId: string;
|
|
173
|
+
/**
|
|
174
|
+
* The child cohort's served coord `coord_d(childParticipantCoord, topicId)`, 32 bytes base64url — the
|
|
175
|
+
* coord the parent verifies the threshold signature against (it looks up the child cohort's
|
|
176
|
+
* {@link MembershipCertV1} at this coord).
|
|
177
|
+
*/
|
|
178
|
+
childCohortCoord: string;
|
|
179
|
+
/**
|
|
180
|
+
* A representative participant coord in the child's prefix-shard (the seed the child engine was
|
|
181
|
+
* instantiated at). Lets the parent *deterministically bind the parent-child relationship*: it recomputes
|
|
182
|
+
* `coord_childTier(childParticipantCoord, topicId) == childCohortCoord` AND
|
|
183
|
+
* `coord_(childTier-1)(childParticipantCoord, topicId) == this parent's served coord`. Any participant
|
|
184
|
+
* sharing the child's `d·log₂F`-bit prefix yields the same pair, so it is a representative, not an
|
|
185
|
+
* identity. 32 bytes base64url.
|
|
186
|
+
*/
|
|
187
|
+
childParticipantCoord: string;
|
|
188
|
+
/** Child tree tier `d` (always ≥ 1 — the root has no parent to link to). Parent serves `d − 1`. */
|
|
189
|
+
childTier: number;
|
|
190
|
+
/** Op capacity tier T0..T3 (stamped so a real parent validates the frame's tier). */
|
|
191
|
+
tier: number;
|
|
192
|
+
/** Unix ms; the parent's per-child freshness/ordering key (strictly-newer wins). */
|
|
193
|
+
effectiveAt: number;
|
|
194
|
+
/** Child cohort threshold signature over `childLinkSigningPayload`, base64url. Empty in the key-less interim. */
|
|
195
|
+
thresholdSig: string;
|
|
196
|
+
/** Signing members' PeerIds, `>= minSigs`, base64url. Empty in the key-less interim. */
|
|
197
|
+
signers: string[];
|
|
198
|
+
/** Child cohort epoch, 32 bytes base64url (the epoch the threshold sig was collected under). */
|
|
199
|
+
cohortEpoch: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Parent → child ack. `linked` flips the child `awaiting_parent → serving`; `rejected` keeps it awaiting. */
|
|
203
|
+
export interface ChildLinkReplyV1 {
|
|
204
|
+
v: 1;
|
|
205
|
+
result: "linked" | "rejected";
|
|
206
|
+
/** Human-readable, optional. */
|
|
207
|
+
reason?: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Threshold-signed promotion notice. */
|
|
211
|
+
export interface PromotionNoticeV1 {
|
|
212
|
+
v: 1;
|
|
213
|
+
topicId: string;
|
|
214
|
+
fromTier: number;
|
|
215
|
+
/** Typically `fromTier + 1`. */
|
|
216
|
+
toTier: number;
|
|
217
|
+
/**
|
|
218
|
+
* The served coord `coord_d(participantCoord, topicId)` the deciding cohort sits at, 32 bytes, base64url
|
|
219
|
+
* — the same value as that cohort's {@link MembershipCertV1.cohortCoord}, which the threshold signature
|
|
220
|
+
* verifies against. The receiver routes the notice to the local engine for exactly this coord rather than
|
|
221
|
+
* scanning for the first engine matching `(topic, tier)`, so a node serving several sibling cohorts for
|
|
222
|
+
* one `(topic, tier)` (possible at `d ≥ 1`, where each cohort has its own served coord) applies the notice
|
|
223
|
+
* to the cohort that actually produced it. Covered by the threshold signature — rewriting it breaks verification.
|
|
224
|
+
*/
|
|
225
|
+
cohortCoord: string;
|
|
226
|
+
/** Unix ms. */
|
|
227
|
+
effectiveAt: number;
|
|
228
|
+
/** Cohort threshold signature, base64url. */
|
|
229
|
+
thresholdSig: string;
|
|
230
|
+
/** PeerIds, `>= minSigs`. */
|
|
231
|
+
signers: string[];
|
|
232
|
+
/** 32 bytes, base64url. */
|
|
233
|
+
cohortEpoch: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Threshold-signed demotion notice. */
|
|
237
|
+
export interface DemotionNoticeV1 {
|
|
238
|
+
v: 1;
|
|
239
|
+
topicId: string;
|
|
240
|
+
tier: number;
|
|
241
|
+
/** 32 bytes, base64url. */
|
|
242
|
+
parentCohortCoord: string;
|
|
243
|
+
/**
|
|
244
|
+
* The served coord `coord_d(participantCoord, topicId)` the deciding cohort sits at, 32 bytes, base64url —
|
|
245
|
+
* the same served-coord concept as {@link PromotionNoticeV1.cohortCoord}. Distinct from
|
|
246
|
+
* {@link parentCohortCoord} (the tier-`(d − 1)` parent this demotion hands off to): `cohortCoord` names the
|
|
247
|
+
* *demoting* cohort and is what the receiver routes + verifies by. Covered by the threshold signature.
|
|
248
|
+
*/
|
|
249
|
+
cohortCoord: string;
|
|
250
|
+
effectiveAt: number;
|
|
251
|
+
thresholdSig: string;
|
|
252
|
+
signers: string[];
|
|
253
|
+
/** 32 bytes, base64url. */
|
|
254
|
+
cohortEpoch: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A registration record carried in cohort gossip for cross-member replication (so any member can
|
|
259
|
+
* fail over to serving it). Byte fields are base64url; mirrors the local `RegistrationRecord`.
|
|
260
|
+
*/
|
|
261
|
+
export interface GossipRecordV1 {
|
|
262
|
+
/** Topic id, 32 bytes, base64url. */
|
|
263
|
+
topicId: string;
|
|
264
|
+
/** Registering participant peer id, base64url. */
|
|
265
|
+
participantId: string;
|
|
266
|
+
/** Tier 0..3. */
|
|
267
|
+
tier: number;
|
|
268
|
+
/** Assigned primary peer id, base64url. */
|
|
269
|
+
primary: string;
|
|
270
|
+
/** 1..2 warm-failover peer ids, base64url. */
|
|
271
|
+
backups: string[];
|
|
272
|
+
/** Unix ms the registration first attached. */
|
|
273
|
+
attachedAt: number;
|
|
274
|
+
/** Unix ms of the most recent ping/touch (the convergence key — newest wins). */
|
|
275
|
+
lastPing: number;
|
|
276
|
+
/** Registration TTL in ms. */
|
|
277
|
+
ttl: number;
|
|
278
|
+
/** Opaque application-defined per-registration state, base64url. */
|
|
279
|
+
appState?: string;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Reference to a single registration in an eviction delta. */
|
|
283
|
+
export interface GossipRecordRefV1 {
|
|
284
|
+
/** Topic id, 32 bytes, base64url. */
|
|
285
|
+
topicId: string;
|
|
286
|
+
/** Participant peer id, base64url. */
|
|
287
|
+
participantId: string;
|
|
288
|
+
/**
|
|
289
|
+
* Unix ms of the evicted record's most-recent ping (mirrors {@link GossipRecordV1.lastPing}). The
|
|
290
|
+
* receiver's freshness guard for the delete: a held record newer than this stamp is a re-registration
|
|
291
|
+
* that arrived after the eviction was produced, so the stale eviction is ignored rather than deleting
|
|
292
|
+
* the fresh record. Required — every real eviction has this on hand from the held record.
|
|
293
|
+
*/
|
|
294
|
+
lastPing: number;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* A child-cohort link/unlink advertised in cohort gossip, for cross-member convergence of the child set.
|
|
299
|
+
*
|
|
300
|
+
* FRET routes a {@link ChildLinkV1} to a **single** parent member, so only that member's child registry
|
|
301
|
+
* records the child; siblings would read `childCohortCount == 0`. Gossiping the child **set** (a converged
|
|
302
|
+
* union) closes that gap: a `childLinks` ref replicates a recorded child to every parent member, and a
|
|
303
|
+
* `childUnlinks` ref replicates a released (demoted) child. Merge is **last-writer-wins by
|
|
304
|
+
* {@link effectiveAt}** per `(topicId, childCohortCoord)` — a link and a later unlink converge regardless of
|
|
305
|
+
* arrival order, and re-delivery is idempotent. The child set is keyed by child coord, not the parent's
|
|
306
|
+
* epoch, so a parent membership rotation never drops it.
|
|
307
|
+
*/
|
|
308
|
+
export interface ChildLinkRefV1 {
|
|
309
|
+
/** Topic id, 32 bytes, base64url. */
|
|
310
|
+
topicId: string;
|
|
311
|
+
/** The child cohort's served coord, 32 bytes, base64url. */
|
|
312
|
+
childCohortCoord: string;
|
|
313
|
+
/** Unix ms — the link/unlink effectiveAt; last-writer-wins per `(topicId, childCohortCoord)`. */
|
|
314
|
+
effectiveAt: number;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Intra-cohort gossip: willingness vector, load barometer, exact per-topic summaries, record deltas. */
|
|
318
|
+
export interface CohortGossipV1 {
|
|
319
|
+
v: 1;
|
|
320
|
+
/** PeerId. */
|
|
321
|
+
fromMember: string;
|
|
322
|
+
/**
|
|
323
|
+
* The cohort coord this gossip is for, 32 bytes, base64url — the inbound routing key. A node serving
|
|
324
|
+
* many cohorts fans a delivered frame to every coord engine's bus; each bus merges only the gossip
|
|
325
|
+
* naming its own coord, so a gossip for one cohort never pollutes a sibling cohort's store/view.
|
|
326
|
+
*/
|
|
327
|
+
coord: string;
|
|
328
|
+
/** 32 bytes, base64url. */
|
|
329
|
+
cohortEpoch: string;
|
|
330
|
+
/**
|
|
331
|
+
* The tree tier `d` the originating cohort sits at (the coord encodes the tier-shard, but a coord is a
|
|
332
|
+
* hash and cannot be inverted to recover `d`). Carried so a cold sibling that instantiates its engine
|
|
333
|
+
* off a co-member's frame (§Cold-start instantiation) adopts the right tier. Always well-defined: the
|
|
334
|
+
* only members that originate a frame already know their `treeTier`, and every member of a coord shares
|
|
335
|
+
* one `treeTier` by construction. Covered by {@link signature} so it cannot be spoofed.
|
|
336
|
+
*/
|
|
337
|
+
treeTier: number;
|
|
338
|
+
/** 4 bits T0..T3, hex. */
|
|
339
|
+
willingnessBits: string;
|
|
340
|
+
/** 4 entries, 0..7 per tier. */
|
|
341
|
+
loadBuckets: number[];
|
|
342
|
+
/** Cohort-wide observation window for the rate fields in `topicSummaries`. */
|
|
343
|
+
windowSeconds: number;
|
|
344
|
+
topicSummaries: CohortTopicSummary[];
|
|
345
|
+
/**
|
|
346
|
+
* Registration records this member is advertising (fresh or touched), for cross-member
|
|
347
|
+
* replication. Absent when this gossip carries no record changes. Merge is last-writer-wins by
|
|
348
|
+
* {@link GossipRecordV1.lastPing}.
|
|
349
|
+
*/
|
|
350
|
+
records?: GossipRecordV1[];
|
|
351
|
+
/** Registrations this member evicted (stale), so all members converge on the active set. */
|
|
352
|
+
evicted?: GossipRecordRefV1[];
|
|
353
|
+
/**
|
|
354
|
+
* Child cohorts this member freshly recorded (links), replicated so every parent member converges on the
|
|
355
|
+
* same child set. Absent when this gossip carries no child-link changes. Merge is last-writer-wins by
|
|
356
|
+
* {@link ChildLinkRefV1.effectiveAt} per `(topicId, childCohortCoord)`; a link sets the child linked.
|
|
357
|
+
*/
|
|
358
|
+
childLinks?: ChildLinkRefV1[];
|
|
359
|
+
/**
|
|
360
|
+
* Child cohorts this member released (a demoted child unrecorded), replicated so every parent member drops
|
|
361
|
+
* it and can shrink in turn. Absent when this gossip carries no child-unlink changes. Same last-writer-wins
|
|
362
|
+
* merge as {@link childLinks}; an unlink sets the child unlinked. A link@t1 then unlink@t2 (t2 > t1)
|
|
363
|
+
* converges to unlinked regardless of arrival order.
|
|
364
|
+
*/
|
|
365
|
+
childUnlinks?: ChildLinkRefV1[];
|
|
366
|
+
timestamp: number;
|
|
367
|
+
signature: string;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Per-topic summary inside {@link CohortGossipV1}; counts are exact, intra-cohort only. */
|
|
371
|
+
export interface CohortTopicSummary {
|
|
372
|
+
topicId: string;
|
|
373
|
+
tier: number;
|
|
374
|
+
/** Exact, intra-cohort only. */
|
|
375
|
+
directParticipants: number;
|
|
376
|
+
/** Exact, fresh + renewals over the gossip window. */
|
|
377
|
+
arrivalsPerMin: number;
|
|
378
|
+
queriesPerMin: number;
|
|
379
|
+
promoted: boolean;
|
|
380
|
+
childCohortCount: number;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* What a {@link SignRequestV1} asks a cohort member to endorse — drives the signer's endorsement policy.
|
|
385
|
+
*
|
|
386
|
+
* - `membership` / `promotion` / `demotion` — the requester and endorser must share the **current**
|
|
387
|
+
* cohort + epoch around `coord`.
|
|
388
|
+
* - `rotation` — an epoch hand-off: the endorser attests it was a member of the cohort at the **prior**
|
|
389
|
+
* epoch carried as `cohortEpoch` (the predecessor cohort threshold-signs the successor cert's payload),
|
|
390
|
+
* so the gate checks *prior*-epoch membership rather than current. See `cohort-topic-trust-anchor-rotation-production`.
|
|
391
|
+
* - `childlink` — a child cohort signs a {@link ChildLinkV1} over its **own** served coord at its current
|
|
392
|
+
* epoch (identical endorsement shape to `promotion`/`demotion`: share the current cohort + epoch around
|
|
393
|
+
* `coord`); the parent verifies it against the child cohort's cert before recording the child.
|
|
394
|
+
*/
|
|
395
|
+
export type SignKind = "membership" | "promotion" | "demotion" | "rotation" | "childlink";
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Intra-cohort sign request (`/optimystic/cohort-topic/1.0.0/sign`). A member assembling a `k − x`
|
|
399
|
+
* threshold signature dials each cohort member with this; the member endorses by Ed25519-signing the
|
|
400
|
+
* **exact** `payload` bytes (already canonicalized by the requester via `sig/payloads.ts`), so signer
|
|
401
|
+
* and verifier never re-canonicalize independently. `coord`/`cohortEpoch` scope the endorsement
|
|
402
|
+
* (the member checks it shares that cohort/epoch); `kind` selects the endorsement policy.
|
|
403
|
+
*/
|
|
404
|
+
export interface SignRequestV1 {
|
|
405
|
+
v: 1;
|
|
406
|
+
kind: SignKind;
|
|
407
|
+
/** Cohort coord the signature is for, 32 bytes, base64url. */
|
|
408
|
+
coord: string;
|
|
409
|
+
/** Cohort epoch the requester is collecting under, 32 bytes, base64url. */
|
|
410
|
+
cohortEpoch: string;
|
|
411
|
+
/** The already-canonical signing bytes (the requester's `sig/payloads.ts` image), base64url. */
|
|
412
|
+
payload: string;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** A member's endorsement of a {@link SignRequestV1}: its peer-key signature over the request payload. */
|
|
416
|
+
export interface SignReplyOkV1 {
|
|
417
|
+
v: 1;
|
|
418
|
+
/** The endorsing member's dialable id (UTF-8 peer-id string), base64url. */
|
|
419
|
+
signer: string;
|
|
420
|
+
/** Ed25519 signature over the request `payload`, base64url (64 bytes decoded). */
|
|
421
|
+
signature: string;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** A member's refusal to endorse a {@link SignRequestV1} (not a cohort member, epoch mismatch, …). */
|
|
425
|
+
export interface SignReplyRefusedV1 {
|
|
426
|
+
v: 1;
|
|
427
|
+
refused: true;
|
|
428
|
+
reason: string;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Reply to a {@link SignRequestV1}: an endorsement ({@link SignReplyOkV1}) or a refusal ({@link SignReplyRefusedV1}). */
|
|
432
|
+
export type SignReplyV1 = SignReplyOkV1 | SignReplyRefusedV1;
|
|
433
|
+
|
|
434
|
+
/** Membership certificate for a cohort. */
|
|
435
|
+
export interface MembershipCertV1 {
|
|
436
|
+
v: 1;
|
|
437
|
+
/** 32 bytes, base64url. */
|
|
438
|
+
cohortCoord: string;
|
|
439
|
+
/** 32 bytes, base64url. */
|
|
440
|
+
cohortEpoch: string;
|
|
441
|
+
/** PeerIds, sorted ascending, length `k`. */
|
|
442
|
+
members: string[];
|
|
443
|
+
/** Unix ms. */
|
|
444
|
+
stabilizedAt: number;
|
|
445
|
+
thresholdSig: string;
|
|
446
|
+
signers: string[];
|
|
447
|
+
/** Optional FRET stabilization proof, base64url. */
|
|
448
|
+
fretAttestation?: string;
|
|
449
|
+
/**
|
|
450
|
+
* Rotation attestation (epoch rotation). The three fields below are an all-or-nothing group: a cert
|
|
451
|
+
* either carries a full attestation (all three present) or none (all absent). They let a successor
|
|
452
|
+
* cohort inherit trust from its predecessor: the predecessor cohort threshold-signs **this** cert's
|
|
453
|
+
* `membershipCertSigningPayload`, so a verifier holding a trusted predecessor at {@link prevEpoch} can
|
|
454
|
+
* confirm the rotation is legitimate (the prior cohort signed off) rather than a forgery. The
|
|
455
|
+
* attestation is **not** part of `membershipCertSigningPayload` (it signs *over* that payload), so the
|
|
456
|
+
* signed image is unchanged and legacy certs (no rotation fields) still decode.
|
|
457
|
+
*/
|
|
458
|
+
/** Predecessor cohort epoch this cert rotates from (32 bytes, base64url). Present only on a rotation. */
|
|
459
|
+
prevEpoch?: string;
|
|
460
|
+
/** Predecessor cohort's threshold signature over THIS cert's `membershipCertSigningPayload`, base64url. */
|
|
461
|
+
rotationSig?: string;
|
|
462
|
+
/** Predecessor cohort signers (PeerIds, base64url) that produced {@link rotationSig}; `>= minSigs`. */
|
|
463
|
+
rotationSigners?: string[];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Discriminated union over every V1 message carried by the cohort-topic protocols. */
|
|
467
|
+
export type CohortMessageV1 =
|
|
468
|
+
| RegisterV1
|
|
469
|
+
| RegisterReplyV1
|
|
470
|
+
| RenewV1
|
|
471
|
+
| RenewReplyV1
|
|
472
|
+
| ChildLinkV1
|
|
473
|
+
| ChildLinkReplyV1
|
|
474
|
+
| PromotionNoticeV1
|
|
475
|
+
| DemotionNoticeV1
|
|
476
|
+
| CohortGossipV1
|
|
477
|
+
| MembershipCertV1
|
|
478
|
+
| SignRequestV1
|
|
479
|
+
| SignReplyOkV1
|
|
480
|
+
| SignReplyRefusedV1;
|