@coffre/vault 0.14.5 → 0.15.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.
Files changed (43) hide show
  1. package/README.md +6 -4
  2. package/dist/cloudflare.d.ts +341 -4
  3. package/dist/cloudflare.js +802 -6
  4. package/dist/index.js +1 -1
  5. package/dist/log-entry-BYWqj3O5.d.ts +364 -0
  6. package/dist/node.d.ts +181 -6
  7. package/dist/node.js +358 -16
  8. package/dist/{src-Bg0ZeMJW.js → src-DheG92EM.js} +1 -1
  9. package/dist/vault-kmtojYnW.js +14623 -0
  10. package/package.json +4 -5
  11. package/src/bulk.ts +89 -0
  12. package/src/changes.ts +478 -0
  13. package/src/checkpointer-node.ts +342 -0
  14. package/src/checkpointer-workers.ts +514 -0
  15. package/src/checkpointer.ts +1165 -0
  16. package/src/cloudflare.ts +207 -13
  17. package/src/config.ts +3 -2
  18. package/src/credentials.postgres.ts +78 -0
  19. package/src/credentials.sqlite.ts +240 -0
  20. package/src/credentials.ts +630 -0
  21. package/src/cutover.ts +275 -0
  22. package/src/decide.ts +437 -2
  23. package/src/inputs.ts +499 -0
  24. package/src/local.ts +20 -4
  25. package/src/log-checkpointer.ts +116 -0
  26. package/src/log-entry.ts +113 -0
  27. package/src/log-replay.ts +188 -0
  28. package/src/log-store.ts +695 -0
  29. package/src/node.ts +73 -9
  30. package/src/objects.ts +141 -0
  31. package/src/reads.ts +502 -0
  32. package/src/retention.ts +229 -0
  33. package/src/snapshot.postgres.ts +39 -9
  34. package/src/snapshot.sqlite.ts +28 -3
  35. package/src/snapshot.ts +64 -10
  36. package/src/store.ts +832 -36
  37. package/src/use.postgres.ts +111 -0
  38. package/src/use.sqlite.ts +542 -0
  39. package/src/use.ts +1060 -0
  40. package/src/vault.ts +1366 -220
  41. package/src/verifier.ts +136 -0
  42. package/src/witness-workers.ts +163 -0
  43. package/dist/vault-Cbi45kLB.js +0 -5197
@@ -0,0 +1,1165 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import {
4
+ decodePair,
5
+ Edge,
6
+ EMPTY_ROOT,
7
+ encodePair,
8
+ entryLeaf,
9
+ inclusionProof,
10
+ leafData,
11
+ leafHash,
12
+ loadEdges,
13
+ nodesFrom,
14
+ openPair,
15
+ pairSigners,
16
+ pairVerifiers,
17
+ proveConsistency,
18
+ samePair,
19
+ signPair,
20
+ tileMap,
21
+ tilesFor,
22
+ TREES,
23
+ verifyConsistency,
24
+ verifyInclusion,
25
+ witnessPair,
26
+ type Hash,
27
+ type Heads,
28
+ type LogEntry,
29
+ type LogReader,
30
+ type NoteSigner,
31
+ type Pair,
32
+ type PairNotes,
33
+ type PairProofs,
34
+ type PairSigners,
35
+ type PairVerifiers,
36
+ type StoredPair,
37
+ type Tile,
38
+ type TreeName,
39
+ type WitnessAnswer,
40
+ type WitnessState,
41
+ } from '@coffre/core/merkle';
42
+ import { UNSEALED, type Sealing } from '@coffre/core/vault';
43
+ import { isUnavailable } from '@coffre/db/dialect';
44
+
45
+ /**
46
+ * 0.15's checkpointer (docs/design/audit-tree.md): every tick it seals what
47
+ * has committed into the two trees, signs both heads as one pair, remembers
48
+ * the pair outside the database, writes it, and hands it to the witnesses.
49
+ * The rest of the vault sees one seam: `tick()` (the window in, a signed pair
50
+ * out), `sealThrough(number)` for a change that waits for its seal (R51), and
51
+ * `prove(number)`. The trees' shape stays behind it.
52
+ *
53
+ * What it stands on, each a protection formal/Seal.tla shows necessary:
54
+ * - the barrier (`close`): every number at or below the `upto` it returns has
55
+ * ended, committed or not, so a window is final once read;
56
+ * - the signer's memory (`Memory`, core, R54): the last pair it signed, kept
57
+ * where a database writer cannot reach, written before the database
58
+ * (RememberFirst) and checked at every open, so neither a tail cut nor a
59
+ * rollback gets a second history signed;
60
+ * - each tick compares the database's newest row with the pair it holds
61
+ * (RowChecked), so a deleted row shows within a tick, traffic or not;
62
+ * - nothing is sealed over an entry that fails its MAC (`authentic`), is out
63
+ * of its window, or is much older than the pair it holds (a replay).
64
+ *
65
+ * And nothing it awaits is unbounded (T169): a tick that runs past
66
+ * `TICK_BOUND_MS` stalls the checkpointer, closed for good behind its
67
+ * `Fence`, so that its host opens another while the database still holds
68
+ * the old one's statement (Seal.tla's Fenced); a change waits at most
69
+ * `SEAL_WAIT_MS` for its seal, then answers `Unsealed`.
70
+ */
71
+
72
+ /** What the checkpointer needs of the database. Postgres and SQLite implement it; so do the tests. */
73
+ export type CheckpointStore = LogReader & {
74
+ /**
75
+ * The barrier, then the window: `upto`, such that every number at or below
76
+ * it has ended; the entries in (after, upto], by number; and the newest
77
+ * pair, as `newest()` reads it. The window is read after the barrier's lock
78
+ * is let go, in a snapshot of its own (`close_one`, then one read): a
79
+ * snapshot from before the lock would miss what committed while it waited.
80
+ *
81
+ * The barrier also raises the floor of `at` (`close_one`): `floor.found` is
82
+ * the floor it found, null if none, and `floor.set` the one it left. The
83
+ * window's uses are read from `from`, the floor the signer holds, as the
84
+ * last barrier set it. `floor.ceiling` is the database's clock less the
85
+ * use deadline, read once the barrier has run: the highest floor any
86
+ * barrier before it can have left (HotPathOne.tla's BarrierAcceptsLate).
87
+ *
88
+ * With `limit` (T169), the window's first `limit` entries by number, and
89
+ * `more` when there were more: `more.above`, the lowest `at` of the uses
90
+ * the window holds above the last of them, from `from` on (null: none).
91
+ * `barrier`, told the barrier's `upto` before the window is read.
92
+ */
93
+ close(after: bigint, from: number, limit?: number, barrier?: (upto: bigint) => void): Promise<{
94
+ upto: bigint; newest: StoredPair | null; entries: LogEntry[]; floor: { found: number | null; set: number; ceiling: number };
95
+ more?: { above: number | null } | null;
96
+ }>;
97
+ /**
98
+ * The newest pair: each tree's own newest note, `upto` the larger of theirs
99
+ * (Seal.tla's PairChecked). Stored as one row a pair, that is the newest
100
+ * row; stored as a row a tree, never one tree's newest row for both, since
101
+ * deleting the other tree's newest row would then pass unseen. Notes of two
102
+ * places then fail every check that wants one pair.
103
+ */
104
+ newest(): Promise<StoredPair | null>;
105
+ /**
106
+ * One statement: both trees' tiles and both notes (PairWrittenTogether). No
107
+ * state holds one head without the other.
108
+ */
109
+ write(row: StoredPair, tiles: Record<TreeName, Tile[]>): Promise<void>;
110
+ /**
111
+ * Entry `number` and its leaf index in its tree, if it is in the log; a
112
+ * use found at `at` when the caller knows its time, which spares a scan
113
+ * of its window's time. With `tree`, an entry in the other is refused
114
+ * (`misplaced`) before its index is sought.
115
+ */
116
+ locate(number: bigint, options?: Locating): Promise<{ entry: LogEntry; index: number } | null>;
117
+ };
118
+
119
+ /**
120
+ * The signer's memory: the one record holding the last pair it signed. A
121
+ * Durable Object's storage on Workers, a synced file beside a Node vault.
122
+ */
123
+ export type Memory = { last(): Promise<string | null>; remember(record: string): Promise<void> };
124
+
125
+ /** What a proof asks of `locate`: the use's time, if known, and the tree the entry must be in. */
126
+ export type Locating = { at?: number; tree?: TreeName };
127
+
128
+ /**
129
+ * An answer of the database a proof refuses: an entry in the other tree, a
130
+ * window no checkpoints make, an entry changed since its tick. Never an I/O
131
+ * failure, which a caller may ask again past; `sealThrough` alarms on these
132
+ * alone (PR 8 review r4 of W240).
133
+ */
134
+ export class Refused extends Error {}
135
+
136
+ /**
137
+ * An entry found in the other tree than the one asked: a writer who deleted
138
+ * a change and renumbered a use into its place (KindInMac). Refused before
139
+ * its index is sought, which for a use reads its window's tiles, whose
140
+ * bounds are the database's (PR 8 review r2).
141
+ */
142
+ export function misplaced(entry: LogEntry, tree: TreeName): Refused {
143
+ return new Refused(`entry ${entry.number} is in the ${entry.tree} tree, not the ${tree}: a ${entry.tree === 'uses' ? 'use' : 'change'} holds its number`);
144
+ }
145
+
146
+ /**
147
+ * The pairs a memory's record holds outside the database, for the verifier
148
+ * (`VaultOptions.heldOutside`): none before the first, and a record that does
149
+ * not read as a pair refused, never read as none, which would drop the walk's
150
+ * anchor in silence (review r11).
151
+ */
152
+ export function heldIn(record: string | null): PairNotes[] {
153
+ if (record === null) return [];
154
+ const notes = decodePair(record);
155
+ if (notes === null) throw new Error("the signer's memory does not read as a pair: the log is not checked against it");
156
+ return [notes];
157
+ }
158
+
159
+ /** Somewhere each signed pair goes that a database writer cannot reach: a witness, an archive (R40's plugin). */
160
+ export type Witness = {
161
+ name: string;
162
+ /**
163
+ * Publishes the pair; `proofs(from)` gives each tree's consistency proof
164
+ * from the sizes the witness holds. A try that outlives its bound (2 s) is
165
+ * a failure to the pump, and `signal` aborts: an implementation that makes
166
+ * a request passes it on, so abandoned tries do not pile up.
167
+ */
168
+ publish(notes: PairNotes, proofs: (from: Record<TreeName, number>) => Promise<PairProofs>, signal: AbortSignal): Promise<WitnessAnswer>;
169
+ };
170
+
171
+ /**
172
+ * How much older than the pair the signer holds an entry it seals may be.
173
+ * A use stamps `at`, which its MAC covers, and takes its number within its
174
+ * deadline (3 s, R53), so an older entry is a replay: the copy of a use whose
175
+ * day retention dropped, say, which no lookup of its request can match. The
176
+ * bound is the signer's own pair, not a row a writer can delete.
177
+ */
178
+ export const FRESH_MS = 10 * 60_000;
179
+
180
+ /** How long a change waits for the witnesses once sealed before it answers unwitnessed (R51). */
181
+ export const WITNESS_DEADLINE_MS = 2000;
182
+
183
+ /**
184
+ * How long a tick may run (T169). A tick seals a tick's window, tens of
185
+ * milliseconds at R44's rate; one past this waits on a database that
186
+ * stalled under it (a connection that went dead, a connect that never
187
+ * answers, a lock never let go), and the checkpointer stalls: closed for
188
+ * good, its fence with it, so that its host opens another.
189
+ */
190
+ export const TICK_BOUND_MS = 10_000;
191
+
192
+ /**
193
+ * How long a change waits for the ticks that seal it, and its proof (T169).
194
+ * Past it the change, committed and in force, answers `Unsealed`: the app
195
+ * says 503, try again. The witnesses' 2 s come after.
196
+ */
197
+ export const SEAL_WAIT_MS = 5000;
198
+
199
+ /** How long after its last tick that ended well a checkpointer still seals, for readiness (T169): ten ticks. */
200
+ export const SEALING_STALE_MS = 5000;
201
+
202
+ /**
203
+ * Whether a checkpointer seals, from its host's view (`Vault.sealing`): open, no failure standing, and its last
204
+ * tick ended well within `SEALING_STALE_MS`, at `last.at` by the host's clock.
205
+ */
206
+ export function sealingOf(state: { open: boolean; failed: string | null; last: { at: number; failures: readonly string[] } | null }, now = Date.now()): Sealing {
207
+ if (state.failed !== null) return { ok: false, reason: state.failed };
208
+ if (!state.open) return { ok: false, reason: 'the checkpointer is not open' };
209
+ if (state.last === null) return { ok: false, reason: 'the checkpointer has not ticked yet' };
210
+ if (state.last.failures.length > 0) return { ok: false, reason: `its last tick was refused: ${state.last.failures.join('; ')}` };
211
+ if (now - state.last.at > SEALING_STALE_MS) return { ok: false, reason: `its last tick ended ${now - state.last.at} ms ago` };
212
+ return { ok: true };
213
+ }
214
+
215
+ /**
216
+ * The most entries a tick seals (T169): a window past it is sealed a part
217
+ * at a time, each part a pair, the next tick at once. After a stall of
218
+ * minutes at R44's rate, the backlog is hundreds of thousands: read and
219
+ * sealed whole, it would run past `TICK_BOUND_MS` and stall again, for good.
220
+ */
221
+ export const WINDOW_MAX = 10_000;
222
+
223
+ /**
224
+ * How much longer than `TICK_BOUND_MS` a tick may run for each number its
225
+ * barrier's window spans (review by Fable): its read sorts the window's
226
+ * uses by number, which have no index by number, so it costs the whole
227
+ * backlog left, 1.4 µs a use measured (1.6 s at a million). A tick is
228
+ * bounded by the work asked of it, never by a clock alone: a drain of
229
+ * millions goes on, a few times slower than measured, and a stall is
230
+ * still one.
231
+ */
232
+ export const WINDOW_ENTRY_MS = 0.01;
233
+
234
+ /** A wait past its bound (`within`). */
235
+ export class TimedOut extends Error {}
236
+
237
+ /** What a fence closed refuses: no alarm, since another checkpointer holds its memory and does the work. */
238
+ export class FenceClosed extends Error {}
239
+
240
+ /** A tick past `TICK_BOUND_MS`: the checkpointer stalled, and is closed for good. */
241
+ export class Stalled extends Error {}
242
+
243
+ /**
244
+ * A change committed whose seal did not come in time (T169): the
245
+ * checkpointer stalled, is reopening, or its database does not answer. Its
246
+ * message carries core's `UNSEALED`, which the app answers with 503 and
247
+ * Retry-After; the change is in force, and the next tick seals it.
248
+ */
249
+ export class Unsealed extends Error {
250
+ constructor(number: bigint, why: string, cause?: unknown) {
251
+ super(`change ${number} ${UNSEALED}: ${why}; try again shortly`, { cause });
252
+ }
253
+ }
254
+
255
+ /**
256
+ * `work`, or `TimedOut` once `ms` have passed, `expired` run first: what a
257
+ * host cuts there (the connection), so that what `work` still awaits fails
258
+ * rather than waits on. `work` may go on; whatever it still does, its fence
259
+ * refuses.
260
+ */
261
+ export function within<T>(work: Promise<T>, ms: number, what: string, expired?: () => void): Promise<T> {
262
+ const until = performance.now() + ms;
263
+ return withinUntil(work, () => until, () => `${what} did not end within ${ms} ms`, expired);
264
+ }
265
+
266
+ /** `within`, its deadline `until()` read again when it comes: one that moved on is waited for. */
267
+ function withinUntil<T>(work: Promise<T>, until: () => number, said: () => string, expired?: () => void): Promise<T> {
268
+ let timer: ReturnType<typeof setTimeout> | undefined;
269
+ const past = new Promise<never>((_, reject) => {
270
+ const arm = () => {
271
+ timer = setTimeout(() => {
272
+ if (performance.now() < until()) return arm();
273
+ expired?.();
274
+ reject(new TimedOut(said()));
275
+ }, Math.max(0, until() - performance.now()));
276
+ (timer as { unref?: () => void }).unref?.();
277
+ };
278
+ arm();
279
+ });
280
+ return Promise.race([work, past]).finally(() => clearTimeout(timer));
281
+ }
282
+
283
+ /**
284
+ * What a checkpointer given up on may still do (T169; Seal.tla's Fenced):
285
+ * nothing it starts once its fence is closed. Every call it makes to its
286
+ * store, and every write to its memory, is checked against the fence
287
+ * first. What it sent before may still land: a database write is refused
288
+ * where a row holds its place (`write_checkpoint`, one statement), and a
289
+ * barrier raises the floor no higher than the next one does
290
+ * (HotPathOne.tla's LateBarrier); the memory writes in flight, `close`
291
+ * waits for, so that the next checkpointer reads them.
292
+ */
293
+ export class Fence {
294
+ #closed: string | null = null;
295
+ readonly #landing = new Set<Promise<void>>();
296
+
297
+ /** Why it was closed; null while it is open. */
298
+ get closed(): string | null {
299
+ return this.#closed;
300
+ }
301
+
302
+ /** Closed for good; resolves once the memory writes already sent have landed. */
303
+ close(why: string): Promise<void> {
304
+ this.#closed ??= why;
305
+ return Promise.allSettled([...this.#landing]).then(() => {});
306
+ }
307
+
308
+ #check(): void {
309
+ if (this.#closed !== null) throw new FenceClosed(`this checkpointer was closed (${this.#closed}): another holds its memory`);
310
+ }
311
+
312
+ /**
313
+ * `answer`, refused if the fence closed while it was awaited: what a call sent before the fence closed answers too
314
+ * late to be read, so that a checkpointer given up on never acts on it, nor alarms on it (review r1).
315
+ */
316
+ #late<T>(answer: T): T {
317
+ if (!(answer instanceof Promise)) return answer;
318
+ return answer.then(
319
+ (value: unknown) => {
320
+ this.#check();
321
+ return value;
322
+ },
323
+ (error: unknown) => {
324
+ // Failed after the fence closed, its connection cut by its host as like as not: the fence's, no answer of the
325
+ // database's, so that nothing alarms on it (review r6).
326
+ if (this.#closed !== null) throw new FenceClosed(`this checkpointer was closed (${this.#closed}): another holds its memory`, { cause: error });
327
+ throw error;
328
+ },
329
+ ) as T;
330
+ }
331
+
332
+ /** The store, each of its calls refused once the fence is closed, and each answer that comes after. */
333
+ store<T extends object>(store: T): T {
334
+ return new Proxy(store, {
335
+ get: (target, key) => {
336
+ const value = Reflect.get(target, key) as unknown;
337
+ if (typeof value !== 'function') return value;
338
+ return (...args: unknown[]) => {
339
+ this.#check();
340
+ return this.#late((value as (...a: unknown[]) => unknown).apply(target, args));
341
+ };
342
+ },
343
+ });
344
+ }
345
+
346
+ /** The memory, its writes refused once the fence is closed, and those in flight kept for `close`. */
347
+ memory(memory: Memory): Memory {
348
+ return {
349
+ last: () => {
350
+ this.#check();
351
+ return this.#late(memory.last());
352
+ },
353
+ remember: (record) => {
354
+ this.#check();
355
+ const landing = memory.remember(record);
356
+ const settled = landing.then(() => {}, () => {});
357
+ this.#landing.add(settled);
358
+ void settled.then(() => this.#landing.delete(settled));
359
+ return landing;
360
+ },
361
+ };
362
+ }
363
+ }
364
+
365
+ export type Tick = {
366
+ /**
367
+ * Whether a pair landed in the database this tick: a new one, or a pending
368
+ * one finished. A tick can finish a pending pair and then refuse its own
369
+ * window: `failures` says so.
370
+ */
371
+ signed: boolean;
372
+ upto: bigint;
373
+ sizes: Record<TreeName, number>;
374
+ entries: number;
375
+ /** Why nothing was signed, when something is wrong: each is an alarm for an operator. */
376
+ failures: string[];
377
+ /** Database time: the barrier and window, then the write. */
378
+ closeMs: number;
379
+ writeMs: number;
380
+ /** Whether the window held more than this tick sealed (`WINDOW_MAX`): its host ticks again at once. */
381
+ more: boolean;
382
+ };
383
+
384
+ export type Sealed = {
385
+ pair: Pair;
386
+ notes: PairNotes;
387
+ /** null when no witness is set up, so nothing reads "witnessed" there. */
388
+ witnessed: boolean | null;
389
+ failures: string[];
390
+ /**
391
+ * Each witness's cosignature of the newest pair it took in time: a pair
392
+ * covering the change, which may be later than `pair` if a tick landed
393
+ * meanwhile. Each note says its own place. An archive cosigns nothing and
394
+ * adds none: whether the change was witnessed is `witnessed`, never the
395
+ * length of this.
396
+ */
397
+ cosigned: PairNotes[];
398
+ /** The change's entry, proved in the pair: what the admin's client checks and can keep. */
399
+ proof: InclusionProof;
400
+ };
401
+
402
+ /**
403
+ * The change path's seam (plan PR 4): a change, once committed, asks for its
404
+ * seal and answers with what this gives it (R51). The Node checkpointer's
405
+ * `sealThrough` is one; on Workers, `durableSealThrough` asks the
406
+ * checkpointer's Durable Object.
407
+ */
408
+ export type SealThrough = (number: bigint, deadline?: number) => Promise<Sealed>;
409
+
410
+ /** An alarm the checkpointer raised, as its host keeps it for whoever watches (R40's plugin, PR 8's surfaces). */
411
+ export type Alarm = { at: number; message: string };
412
+
413
+ /** The log's origin for a signing seed: one per key, `coffre.log/<the first 8 bytes of SHA-256 of the seed's changes key, hex>`. */
414
+ export async function logOrigin(seed: Uint8Array): Promise<string> {
415
+ const { changes } = await pairSigners('coffre.log', seed);
416
+ return `coffre.log/${createHash('sha256').update(changes.publicKey).digest('hex').slice(0, 16)}`;
417
+ }
418
+
419
+ export type InclusionProof = {
420
+ number: bigint;
421
+ tree: TreeName;
422
+ index: number;
423
+ size: number;
424
+ /** The leaf's data: the whole entry. */
425
+ leaf: Buffer;
426
+ proof: Hash[];
427
+ /** The pair it is proved against. */
428
+ notes: PairNotes;
429
+ };
430
+
431
+ type Pump = {
432
+ busy: boolean;
433
+ next: { notes: PairNotes; pair: Pair } | null;
434
+ acked: bigint;
435
+ cosigned: PairNotes | null;
436
+ failure: string | null;
437
+ waiters: { upto: bigint; done: () => void }[];
438
+ };
439
+
440
+ type Pending = { next: { pair: Pair; notes: PairNotes }; edges: Record<TreeName, Edge>; tiles: Record<TreeName, Tile[]>; changes: Map<bigint, Placed> };
441
+
442
+ /** A change a tick sealed: its leaf's data, the whole entry, and its index in the changes tree. */
443
+ type Placed = { leaf: Buffer; index: number };
444
+
445
+ /**
446
+ * A pair a tick signed and wrote, kept with the changes tree's tiles that tick wrote and the changes it sealed (T171):
447
+ * a change's proof in it is computed from them, rather than read again from the database.
448
+ */
449
+ type Signed = { head: { pair: Pair; notes: PairNotes }; tiles: Tile[]; changes: Map<bigint, Placed>; bytes: number };
450
+
451
+ /**
452
+ * How much of its last pairs the checkpointer keeps to prove a change in (T171): those a change still waiting asks
453
+ * about, the newest first, their leaves and tiles 4 MiB at most (review r1: a window is up to `WINDOW_MAX` entries,
454
+ * each its whole payload). A pair past it alone is not kept: its changes are proved from the database.
455
+ */
456
+ const KEPT_BYTES = 4 * 2 ** 20;
457
+
458
+ /** The tick running now, and the barrier it took, once known (T171). */
459
+ type Flight = { run: Promise<Tick>; barrier: bigint | null };
460
+
461
+ type Options = {
462
+ memory: Memory;
463
+ witnesses?: readonly Witness[];
464
+ /**
465
+ * Whether an entry's MAC holds: the vault checks it with its key before
466
+ * anything is sealed. The MAC must cover the entry's tree (Seal.tla's
467
+ * KindInMac): otherwise a change moved into the uses partition before its
468
+ * tick would be sealed into the uses tree, and pruned with the uses.
469
+ */
470
+ authentic: (entry: LogEntry) => boolean;
471
+ /** Where alarms go: refused ticks, unwitnessed changes, memory and database out of step. */
472
+ alert?: (message: string) => void;
473
+ freshMs?: number;
474
+ now?: () => number;
475
+ /**
476
+ * What closes this checkpointer for good (T169): a host bounding the open
477
+ * passes its own, to close it if the open stalls. One is made otherwise.
478
+ */
479
+ fence?: Fence;
480
+ /** `TICK_BOUND_MS`, `SEAL_WAIT_MS` and `WINDOW_MAX`, for the tests. */
481
+ tickBoundMs?: number;
482
+ sealWaitMs?: number;
483
+ windowMax?: number;
484
+ };
485
+
486
+ export class Checkpointer {
487
+ private store: CheckpointStore;
488
+ private signers: PairSigners;
489
+ private options: Required<Omit<Options, 'witnesses' | 'fence'>> & { witnesses: readonly Witness[] };
490
+ private fence: Fence;
491
+ private edges: Record<TreeName, Edge>;
492
+ private head: { pair: Pair; notes: PairNotes };
493
+ private running: Promise<unknown> = Promise.resolve();
494
+ private pumps = new Map<Witness, Pump>();
495
+ /**
496
+ * A pair signed, perhaps remembered, not known to be written: the memory
497
+ * may hold it, or the database may (a write that committed, then threw).
498
+ * It is finished before anything else is signed, never replaced by another
499
+ * pair for its place.
500
+ */
501
+ private pending: Pending | null = null;
502
+ /** The last pairs it wrote, newest last, each with what proves a change it sealed (T171). */
503
+ private signed: Signed[] = [];
504
+ /** The tick running now (T171): a change committed before its barrier is sealed by it, and waits for no other. */
505
+ private flight: Flight | null = null;
506
+ /**
507
+ * The floor of `at` signed into the newest pair: windows are read from it.
508
+ * It moves only when a pair carrying a new floor is written.
509
+ */
510
+ private floor: number;
511
+ /**
512
+ * The floor this process's last barrier set, which the next barrier must
513
+ * find exactly; null until its first barrier (a restart cannot know what
514
+ * barriers whose pairs went unsigned raised it to).
515
+ */
516
+ private barrier: number | null = null;
517
+ /** Set once a barrier finds the floor elsewhere: every tick refuses until the checkpointer is reopened. */
518
+ private floorAlarm: string | null = null;
519
+ /** The failures the last refused tick alerted: a refusal that stands is alerted once, not every tick. */
520
+ private refused: string | null = null;
521
+
522
+ /** The log's public keys, which open its pairs. */
523
+ readonly verifiers: PairVerifiers;
524
+
525
+ private constructor(store: CheckpointStore, signers: PairSigners, verifiers: PairVerifiers, options: Options & { fence: Fence }, edges: Record<TreeName, Edge>, head: { pair: Pair; notes: PairNotes }) {
526
+ this.store = store;
527
+ this.fence = options.fence;
528
+ this.signers = signers;
529
+ this.verifiers = verifiers;
530
+ // Each default by itself: an option passed as undefined takes it too.
531
+ this.options = {
532
+ memory: options.memory,
533
+ authentic: options.authentic,
534
+ witnesses: options.witnesses ?? [],
535
+ alert: options.alert ?? (() => {}),
536
+ freshMs: options.freshMs ?? FRESH_MS,
537
+ now: options.now ?? Date.now,
538
+ tickBoundMs: options.tickBoundMs ?? TICK_BOUND_MS,
539
+ sealWaitMs: options.sealWaitMs ?? SEAL_WAIT_MS,
540
+ windowMax: options.windowMax ?? WINDOW_MAX,
541
+ };
542
+ this.edges = edges;
543
+ this.head = head;
544
+ this.floor = head.pair.floor;
545
+ }
546
+
547
+ /**
548
+ * What stands against it sealing, whichever tick met it, an alarm's or a change's: why it stalled, or the refusal of
549
+ * its last tick; null when its last tick held. Readiness reads it beside its host's own view (T169, review r5).
550
+ */
551
+ get standing(): string | null {
552
+ return this.stalled ?? (this.closed ? 'this checkpointer was closed' : (this.refused ?? this.thrown));
553
+ }
554
+
555
+ /** Why its last tick threw, whichever tick it was, a change's or its host's; null once one ends well (review r6). */
556
+ private thrown: string | null = null;
557
+
558
+ /** The newest pair the signer signed and the database holds. */
559
+ get pair(): { pair: Pair; notes: PairNotes } {
560
+ return this.head;
561
+ }
562
+
563
+ /** How much older than its pair an entry the signer seals may be (`FRESH_MS`): retention drops a day two of these past its cut. */
564
+ get freshMs(): number {
565
+ return this.options.freshMs;
566
+ }
567
+
568
+ /**
569
+ * From the newest checkpoint row, checked against what the signer
570
+ * remembers, which comes first: nothing is signed or remembered before the
571
+ * two are compared. The signer remembers a pair before the database has it
572
+ * (RememberFirst), so the memory may be ahead of the database: after a crash
573
+ * between the two, or because a writer deleted the newest rows. Either way,
574
+ * if the entries the database holds give the remembered pair, its row is
575
+ * written again and nothing was lost; if they do not, the database lost
576
+ * entries the signer sealed, and it refuses. A database beside the memory,
577
+ * another pair at the same place, is refused too.
578
+ */
579
+ static async open(store: CheckpointStore, signers: PairSigners, options: Options): Promise<Checkpointer> {
580
+ // Everything this checkpointer does to the database and its memory, its open included, goes through its fence.
581
+ const fence = options.fence ?? new Fence();
582
+ store = fence.store(store);
583
+ options = { ...options, memory: fence.memory(options.memory), fence };
584
+ const verifiers = await pairVerifiers({ changes: signers.changes.verifierKey, uses: signers.uses.verifierKey });
585
+ const record = await options.memory.last();
586
+ let mine: { pair: Pair; notes: PairNotes } | null = null;
587
+ if (record !== null) {
588
+ const notes = decodePair(record);
589
+ const pair = notes === null ? null : await openPair(notes, verifiers);
590
+ if (pair === null) throw new Error('the signer remembers a pair it did not sign');
591
+ mine = { pair, notes: notes! };
592
+ }
593
+ const row = await store.newest();
594
+ if (row === null) {
595
+ if (mine !== null && (mine.pair.upto !== 0n || TREES.some((tree) => mine.pair[tree].size > 0))) {
596
+ throw new Error(`the database holds no checkpoint; the signer signed up to ${mine.pair.upto} (${said(mine.pair)})`);
597
+ }
598
+ // A new log: both trees empty at number 0, sealing nothing, so the first tick seals every entry there is. A
599
+ // genesis already remembered (a crash before its write) is written as it was, never signed again.
600
+ const empty = { size: 0, root: EMPTY_ROOT };
601
+ // Its time is 0: it seals nothing, and no clock is trusted before the first entry holds it (the tick's rule).
602
+ const genesis = mine ?? (await signPair(signers, 0n, 0, { changes: empty, uses: empty }));
603
+ await options.memory.remember(encodePair(genesis.notes));
604
+ await rewrite(store, { upto: 0n, notes: genesis.notes }, { changes: [], uses: [] });
605
+ return new Checkpointer(store, signers, verifiers, { ...options, fence }, { changes: Edge.empty(), uses: Edge.empty() }, genesis);
606
+ }
607
+ const newest = await openPair(row.notes, verifiers);
608
+ if (newest === null || newest.upto !== row.upto) throw new Error(`the newest checkpoint (${row.upto}) is not a pair signed by this log`);
609
+ let head = { pair: newest, notes: row.notes };
610
+ let edges = await loadEdges(store, newest);
611
+ if (mine !== null && !samePair(mine.pair, newest)) {
612
+ if (mine.pair.upto === newest.upto) throw new Error(`the database's checkpoint at ${newest.upto} is not the pair the signer signed`);
613
+ if (mine.pair.upto > newest.upto) {
614
+ // The memory is ahead: seal again from the newest row up to the remembered pair, from the entries, and require
615
+ // exactly that pair in both trees, at a time no earlier than the row's.
616
+ if (mine.pair.at < newest.at || mine.pair.floor < newest.floor) {
617
+ throw new Error(`the database's newest pair (up to ${newest.upto}, at ${newest.at}, floor ${newest.floor}) is ahead in time or floor of the later one the signer remembers (at ${mine.pair.at}, floor ${mine.pair.floor}): time runs back`);
618
+ }
619
+ const entries = await store.entries(newest.upto, mine.pair.upto);
620
+ const tiles = appendWindow(edges, entries);
621
+ const lost = TREES.filter((tree) => edges[tree].size !== mine.pair[tree].size || !edges[tree].root().equals(mine.pair[tree].root));
622
+ if (lost.length > 0) {
623
+ throw new Error(`the database lost entries the signer sealed: up to ${mine.pair.upto} it holds ${said(edges)}, the signer signed ${said(mine.pair)}`);
624
+ }
625
+ await rewrite(store, { upto: mine.pair.upto, notes: mine.notes }, tiles);
626
+ head = mine;
627
+ } else {
628
+ // The database is ahead, which RememberFirst rules out for this signer (a memory restored from a backup, say):
629
+ // Time never comes down along one history either (what retention's freshness step rests on).
630
+ if (newest.at < mine.pair.at || newest.floor < mine.pair.floor) {
631
+ throw new Error(`the database's newest pair (up to ${newest.upto}, at ${newest.at}, floor ${newest.floor}) is behind in time or floor the one the signer remembers (at ${mine.pair.at}, floor ${mine.pair.floor}): time runs back`);
632
+ }
633
+ // accepted only if the remembered pair is a prefix of it, in both trees: by a consistency proof between the two
634
+ // signed roots, never a head recomputed from tiles, which hold each subtree at several levels and could be
635
+ // planted at one the newest head never reads.
636
+ for (const tree of TREES) {
637
+ const { size, root } = mine.pair[tree];
638
+ const proof = size > newest[tree].size ? null : await proveConsistency(store, tree, size, newest[tree].size).catch(() => null);
639
+ const prefix = proof !== null && verifyConsistency(size, newest[tree].size, root, newest[tree].root, proof);
640
+ if (!prefix) throw new Error(`the database's ${tree} head (${newest[tree].size} up to ${newest.upto}) does not extend the one the signer remembers (${size} up to ${mine.pair.upto})`);
641
+ }
642
+ options.alert?.(`the audit signer's memory (up to ${mine.pair.upto}) is behind the database (up to ${newest.upto}): restored from a backup, or another signer on this key; accepted as a prefix`);
643
+ await options.memory.remember(encodePair(row.notes));
644
+ }
645
+ edges = await loadEdges(store, head.pair);
646
+ } else if (mine === null) {
647
+ // A signer with no memory (a new one on an existing log, or its storage reset) can only take the database's word
648
+ // for the newest pair: said where an operator sees it, and remembered before anything is signed on top of it.
649
+ options.alert?.(`the audit signer remembers no pair; it starts from the database's, up to ${newest.upto}`);
650
+ await options.memory.remember(encodePair(row.notes));
651
+ } else if (encodePair(mine.notes) !== encodePair(row.notes)) {
652
+ // The same heads signed twice, at two times (a pending pair re-signed by an earlier version, say): the memory
653
+ // takes the row's notes, so that it and the head are one record again; never a time or floor that runs back.
654
+ if (newest.at < mine.pair.at || newest.floor < mine.pair.floor) {
655
+ throw new Error(`the database's pair up to ${newest.upto} is the one the signer remembers, signed again earlier or at a lower floor (at ${newest.at}, floor ${newest.floor}; remembered at ${mine.pair.at}, floor ${mine.pair.floor}): time runs back`);
656
+ }
657
+ await options.memory.remember(encodePair(row.notes));
658
+ }
659
+ return new Checkpointer(store, signers, verifiers, { ...options, fence }, edges, head);
660
+ }
661
+
662
+ /**
663
+ * One checkpoint: seal what has committed, sign it, remember it, store it,
664
+ * publish it. Ticks run one at a time, the regular ones and those a change
665
+ * asks for.
666
+ */
667
+ /**
668
+ * Stops this checkpointer for good: every tick after, and every change
669
+ * that would tick, is refused, and its fence refuses whatever its tick in
670
+ * flight still does. Resolves once that tick has ended or stalled
671
+ * (`TICK_BOUND_MS`), and its memory writes in flight have landed. A host
672
+ * that opens another over the same memory closes this one first, so that
673
+ * two signers never act at once.
674
+ */
675
+ close(): Promise<void> {
676
+ this.closed = true;
677
+ return Promise.all([this.fence.close('closed by its host'), this.running]).then(() => {});
678
+ }
679
+
680
+ private closed = false;
681
+ /** Why it stalled (T169): a tick past its bound. */
682
+ private stalled: string | null = null;
683
+ /** When the tick in flight stalls: `TICK_BOUND_MS` after it started, and more for a long window. */
684
+ private until = 0;
685
+ /** What the tick in flight awaits, which a stall says, so that an operator sees what stalled. */
686
+ private phase: 'finish' | 'barrier' | 'sign' | 'remember' | 'write' = 'barrier';
687
+
688
+ /**
689
+ * A tick: the one queued behind the running one, if there is one yet,
690
+ * else a new one. Its barrier comes after every commit made before it
691
+ * starts, so whoever asks before it starts is sealed by it as well as by
692
+ * one of their own: many changes waiting at once share ticks, rather than
693
+ * each waiting for a tick per change before it (0.15-plan.md, "As built:
694
+ * PR 4b", measured).
695
+ */
696
+ tick(): Promise<Tick> {
697
+ if (this.queued !== null) return this.queued;
698
+ const run: Promise<Tick> = this.running.then(() => {
699
+ this.queued = null;
700
+ this.flight = { run, barrier: null };
701
+ // Bounded (T169): past it the checkpointer stalls, its fence closed, whatever the tick still awaits. Its window,
702
+ // once the barrier has said how far, adds to the bound (`WINDOW_ENTRY_MS`).
703
+ const t0 = performance.now();
704
+ this.until = t0 + this.options.tickBoundMs;
705
+ return withinUntil(this.tickNow(), () => this.until, () => `a tick did not end within ${Math.round(this.until - t0)} ms`, () => {
706
+ this.stalled ??= `a tick ran past ${Math.round(this.until - t0)} ms in its ${this.phase}: the database stalled under it`;
707
+ this.closed = true;
708
+ void this.fence.close(this.stalled);
709
+ }).then((t) => {
710
+ this.thrown = null;
711
+ return t;
712
+ }, (error: unknown) => {
713
+ const thrown = error instanceof TimedOut ? new Stalled(this.stalled!) : error;
714
+ this.thrown = `a checkpoint failed: ${(thrown as Error).message}`;
715
+ throw thrown;
716
+ });
717
+ });
718
+ const landed = () => {
719
+ if (this.flight?.run === run) this.flight = null;
720
+ };
721
+ void run.then(landed, landed);
722
+ this.queued = run;
723
+ this.running = run.catch(() => {});
724
+ return run;
725
+ }
726
+
727
+ private queued: Promise<Tick> | null = null;
728
+
729
+ private async tickNow(): Promise<Tick> {
730
+ if (this.stalled !== null) throw new Stalled(`this checkpointer stalled (${this.stalled}): its host opens another`);
731
+ if (this.closed) throw new Error('this checkpointer was closed: another took over its memory');
732
+ let finished = false;
733
+ if (this.pending !== null) {
734
+ this.phase = 'finish';
735
+ await this.finish(this.pending);
736
+ finished = true;
737
+ }
738
+ const { pair } = this.head;
739
+ const t0 = performance.now();
740
+ this.phase = 'barrier';
741
+ const { upto, newest, entries, floor, more = null } = await this.store.close(pair.upto, this.floor, this.options.windowMax, (upto) => {
742
+ if (upto > pair.upto) this.until += Number(upto - pair.upto) * WINDOW_ENTRY_MS;
743
+ if (this.flight !== null) this.flight.barrier = upto;
744
+ });
745
+ const closeMs = performance.now() - t0;
746
+ const failures: string[] = [];
747
+ // The floor is an unsigned row (HotPathOne.tla's BarrierChecksFloor): a writer who lowered it would let a use in
748
+ // below it, which a window read from the floor the signer holds misses; one who raised it would get its value
749
+ // signed, and so kept. Each barrier must find it exactly where this process's last one left it, else the tick is
750
+ // refused, and every tick after it, until the checkpointer is reopened (sticky, as the model's alarm: a later
751
+ // barrier can land the row back on the expected value, but not undo what the floor let in meanwhile). After a
752
+ // restart the first barrier may find it above the newest pair's honestly (barriers whose pairs went unsigned
753
+ // raised it), never below.
754
+ // Above where its last barrier left it is a barrier of a checkpointer given up on, landing late (T169), so long as it
755
+ // is no higher than this barrier's own clock less the deadline, which this barrier raised it past anyway
756
+ // (HotPathOne.tla's BarrierAcceptsLate): higher, a writer raised it.
757
+ // The first barrier after an open too (review by Fable): the barriers whose pairs went unsigned left the floor at
758
+ // most at their own clock less the deadline, which this barrier's ceiling is past; higher, a writer raised it.
759
+ const expected = this.barrier ?? this.floor;
760
+ const raised = floor.found !== null && floor.found > (this.barrier ?? -Infinity) && floor.found > floor.ceiling;
761
+ if (floor.found === null || floor.found < expected || raised) {
762
+ this.floorAlarm ??= `the floor of at is ${floor.found ?? 'gone'}, not the ${expected} the signer's barrier set: ${floor.found !== null && floor.found > expected ? 'raised' : 'lowered'} by a writer; reopen the checkpointer once it is put right`;
763
+ } else {
764
+ this.barrier = floor.set;
765
+ }
766
+ if (this.floorAlarm !== null) failures.push(this.floorAlarm);
767
+ // A running signer seals from the pair it holds and never takes the database's word for it; but the database's
768
+ // newest row must still be that pair, note for note. Rows deleted or replaced since show here, within a tick;
769
+ // nothing is signed over them.
770
+ if (!holds(newest, this.head)) failures.push(`the database's newest checkpoint is ${newest === null ? 'gone' : newest.upto === pair.upto ? 'another pair' : newest.upto}, the signer's is at ${pair.upto}`);
771
+ if (upto < pair.upto) failures.push(`the barrier says ${upto} is final, below the signer's ${pair.upto}`);
772
+ let previous = pair.upto;
773
+ for (const e of entries) {
774
+ if (e.number <= previous || e.number > upto) failures.push(`${e.number}: outside the window (${previous}, ${upto}]: two entries with one number, or out of order`);
775
+ else if (!TREES.includes(e.tree)) failures.push(`${e.number}: in no tree`);
776
+ else if (!this.options.authentic(e)) failures.push(`${e.number}: its MAC does not hold`);
777
+ // A use takes its number within its deadline of `at`; an entry much older than the pair the signer holds is a
778
+ // replay, e.g. the copy of a use whose day was dropped, which no lookup can match.
779
+ else if (e.at < pair.at - this.options.freshMs) failures.push(`${e.number}: at ${e.at}, older than the signer's pair (${pair.at}): a replay`);
780
+ previous = e.number > previous ? e.number : previous;
781
+ }
782
+ // The pair's `at` never comes down: retention's freshness step (plan PR 7) needs the bound it sets to hold, so that
783
+ // a replay of a dropped use can never be fresh again when a clock steps back. Nor may it run ahead: one tick on a
784
+ // signer clock that has jumped forward would otherwise sign a time that makes every honest entry read as a replay
785
+ // until real time caught up. So it is the signer's clock, held to at most the bound past the window's newest entry
786
+ // (stamped within seconds of its number): a clock ahead moves it one bound at most, a signer that stalled on a
787
+ // quiet log still seals, and a window of gaps alone, with no entry to hold the clock against, keeps the last time.
788
+ // A part of the window (T169) is held to its own newest entry, no further: the rest of a backlog may be stamped long
789
+ // before now, and is no replay.
790
+ const newestAt = entries.reduce((max, e) => Math.max(max, e.at), -Infinity);
791
+ const at = entries.length === 0 ? pair.at : Math.max(pair.at, Math.min(this.options.now(), newestAt + (more === null ? this.options.freshMs : 0)));
792
+ const result = (signed: boolean, writeMs = 0): Tick => ({
793
+ signed: signed || finished, upto: this.head.pair.upto, sizes: sizes(this.head.pair), entries: entries.length, failures, closeMs, writeMs,
794
+ more: signed && more !== null,
795
+ });
796
+ if (failures.length > 0) {
797
+ const said = failures.join('\n');
798
+ if (said !== this.refused) for (const f of failures) this.options.alert(`audit checkpoint refused: ${f}`);
799
+ this.refused = said;
800
+ return result(false);
801
+ }
802
+ this.refused = null;
803
+ if (upto === pair.upto) {
804
+ // Nothing new. A quiet log: hand the newest pair again to any witness that failed to take it.
805
+ for (const w of this.options.witnesses) if (this.pump(w).acked < pair.upto) this.kick(w);
806
+ return result(false);
807
+ }
808
+ // The edges move on a copy, kept once the pair is written. Until then the pair is pending: a failure leaves it to
809
+ // be finished by the next tick, and the signer at the pair the database holds meanwhile.
810
+ const edges = { changes: this.edges.changes.clone(), uses: this.edges.uses.clone() };
811
+ // Each change's leaf index, as appendWindow places it: after the tree's leaves, in number order.
812
+ const changes = new Map<bigint, Placed>();
813
+ for (const e of entries) if (e.tree === 'changes') changes.set(e.number, { leaf: leafData('changes', e.number, e.columns), index: edges.changes.size + changes.size });
814
+ const tiles = appendWindow(edges, entries);
815
+ const heads: Heads = { changes: { size: edges.changes.size, root: edges.changes.root() }, uses: { size: edges.uses.size, root: edges.uses.root() } };
816
+ // The pair carries the floor this barrier left, which the signer reads from once the pair is written, and the floor
817
+ // of the pair before it: so the pair's own note says whether it is the first past a time (retention's cut).
818
+ // A part of the window (T169) ends at its last entry, every number below it read, and carries the lowest `at` of the
819
+ // uses left above it, if below the barrier's floor (HotPathOne.tla's ChunkFloor): the next window is read from it,
820
+ // and the rest of a backlog may be stamped long before the barrier. Its own uses sit within 63 s of it
821
+ // (CeilingHolds): each was numbered before the barrier and before any use left above it, within [at − 60 s,
822
+ // at + 3 s] by Postgres's clock (`ahead`, the deadline).
823
+ const place = more === null ? upto : entries.at(-1)!.number;
824
+ const signedFloor = more === null ? floor.set : Math.max(this.floor, Math.min(floor.set, more.above ?? floor.set));
825
+ this.phase = 'sign';
826
+ const next = await signPair(this.signers, place, at, heads, signedFloor, pair.floor);
827
+ this.pending = { next, edges, tiles, changes };
828
+ // Remembered outside the database before the database has it (RememberFirst): a crash between the two and a
829
+ // rollback can then never get a second pair signed for the same place.
830
+ this.phase = 'remember';
831
+ await this.options.memory.remember(encodePair(next.notes));
832
+ const t1 = performance.now();
833
+ this.phase = 'write';
834
+ await this.store.write({ upto: place, notes: next.notes }, tiles);
835
+ const writeMs = performance.now() - t1;
836
+ this.commit(this.pending);
837
+ return result(true, writeMs);
838
+ }
839
+
840
+ /**
841
+ * Finishes a pending pair: remembered again (the first try may have failed,
842
+ * or not), then held by the database, which may hold it already; else it is
843
+ * written now over the pair the signer holds. A database at neither pair
844
+ * is refused until the checkpointer is reopened, whose rules then apply.
845
+ */
846
+ private async finish(p: Pending): Promise<void> {
847
+ await this.options.memory.remember(encodePair(p.next.notes));
848
+ const row = await this.store.newest();
849
+ if (!holds(row, p.next)) {
850
+ if (!holds(row, this.head)) {
851
+ const message = `the database's newest checkpoint is ${row?.upto ?? 'gone'}; the signer's is at ${this.head.pair.upto}, with ${p.next.pair.upto} to write: reopen the checkpointer`;
852
+ // Alerted once while it stands, as a refused tick is: every tick finds it again.
853
+ if (message !== this.refused) this.options.alert(`audit checkpoint refused: ${message}`);
854
+ this.refused = message;
855
+ throw new Error(message);
856
+ }
857
+ await this.store.write({ upto: p.next.pair.upto, notes: p.next.notes }, p.tiles);
858
+ }
859
+ this.commit(p);
860
+ }
861
+
862
+ private commit(p: Pending): void {
863
+ this.edges = p.edges;
864
+ this.head = p.next;
865
+ this.floor = p.next.pair.floor;
866
+ this.pending = null;
867
+ if (p.changes.size > 0) {
868
+ // The tiles the tick wrote, and its edge, which hold every node a proof of a leaf it appended reads (`edgeTiles`).
869
+ const tiles = [...p.tiles.changes, ...p.edges.changes.edgeTiles()];
870
+ let bytes = tiles.reduce((n, t) => n + t.hashes.length, 0);
871
+ for (const { leaf } of p.changes.values()) bytes += leaf.length;
872
+ if (bytes <= KEPT_BYTES) {
873
+ this.signed.push({ head: p.next, tiles, changes: p.changes, bytes });
874
+ let kept = this.signed.reduce((n, k) => n + k.bytes, 0);
875
+ while (kept > KEPT_BYTES) kept -= this.signed.shift()!.bytes;
876
+ }
877
+ }
878
+ this.publish();
879
+ }
880
+
881
+ /**
882
+ * A change answers once its entry is sealed and, where witnesses are set
883
+ * up, witnessed (R51): seal now unless a pair already covers it, then wait
884
+ * for every witness to hold a pair that covers it, whichever tick sealed it,
885
+ * for at most `deadline`. A witness silent that long lets the change answer
886
+ * unwitnessed, alerted: the change is in force either way, and its entry is
887
+ * sealed in the database and in the signer's memory. A tick that cannot seal
888
+ * (an entry failing its MAC) fails the wait rather than spinning.
889
+ *
890
+ * A pair covering the number is not enough: the entry must be in it. An
891
+ * entry deleted before its tick leaves its number a gap the pair calls
892
+ * final, and nothing could ever show the change again; so the change is
893
+ * proved in the pair, and refused, alerted, if it is not there.
894
+ */
895
+ async sealThrough(number: bigint, deadline = WITNESS_DEADLINE_MS): Promise<Sealed> {
896
+ // Its ticks and its proof within `SEAL_WAIT_MS` (T169): past it, or once the checkpointer stalled or its database does
897
+ // not answer, the change answers `Unsealed`, committed, and the next checkpointer that ticks seals it.
898
+ const until = performance.now() + this.options.sealWaitMs;
899
+ const bounded = <T>(work: Promise<T>, what: string): Promise<T> =>
900
+ within(work, until - performance.now(), what).catch((error: unknown) => {
901
+ if (error instanceof TimedOut || error instanceof Stalled || this.closed || isUnavailable(error)) throw new Unsealed(number, (error as Error).message, error);
902
+ throw error;
903
+ });
904
+ while (this.head.pair.upto < number) {
905
+ // The tick running now seals it if its barrier comes after the change committed, which was before it asked: one
906
+ // whose barrier is not known yet, or is known to pass it (T171). Otherwise the next tick, which is the change's own.
907
+ const flight = this.flight;
908
+ const own = flight === null || (flight.barrier !== null && flight.barrier < number) ? this.tick() : null;
909
+ const before = this.head.pair.upto;
910
+ const t = await bounded(own ?? flight!.run, 'its seal');
911
+ if (this.head.pair.upto >= number || own === null) continue;
912
+ if (this.head.pair.upto === before) throw new Error(`cannot seal ${number}: ${t.failures.join('; ') || 'the barrier has not passed it'}`);
913
+ }
914
+ // Proved in the pair whose tick sealed it, from the tiles that tick wrote, as the signer holds them (T171): the
915
+ // signer's own head holds the change (Seal.tla's WaitForSeal), and the proof checks against its root. A change
916
+ // sealed before the checkpointer kept it is proved from the database, against the newest pair.
917
+ // From the changes tree (KindInMac): the MAC does not cover the number, so a writer could delete the change and
918
+ // renumber a use into its place before the tick; that use is sealed, and provable, but in the uses tree.
919
+ // A proof the database answers wrongly for (an entry changed since its tick, `locate`'s refusal) is the same alarm.
920
+ // A checkpointer closed or stalled answers no change, from what it kept or from the database (T169; review r1); nor
921
+ // one whose last tick was refused, while that stands (Seal.tla's Answer: the signer neither down nor refused).
922
+ if (this.closed) throw new Unsealed(number, this.stalled ?? 'this checkpointer was closed: another holds its memory');
923
+ if (this.refused !== null) throw new Error(`cannot answer change ${number}: the checkpointer's last tick was refused: ${this.refused}`);
924
+ const kept = this.proveKept(number);
925
+ const head = kept?.head ?? this.head;
926
+ let proof: InclusionProof | null;
927
+ try {
928
+ proof = kept?.proof ?? await bounded(this.proveAt(number, head, { tree: 'changes' }), 'the proof of its seal');
929
+ } catch (error) {
930
+ if (!(error instanceof Refused)) throw error;
931
+ const message = `change ${number} cannot be proved in the signed changes tree: ${error.message}`;
932
+ this.options.alert(message);
933
+ throw new Error(message);
934
+ }
935
+ if (proof === null) {
936
+ const message = `change ${number} is not in the signed changes tree: its entry was lost before its seal`;
937
+ this.options.alert(message);
938
+ throw new Error(message);
939
+ }
940
+ const failures = await this.witnessed(number, deadline);
941
+ const cosigned = this.options.witnesses.map((w) => this.pump(w)).filter((p) => p.acked >= number && p.cosigned !== null).map((p) => p.cosigned!);
942
+ const witnessed = this.options.witnesses.length === 0 ? null : failures.length === 0;
943
+ if (witnessed === false) this.options.alert(`change ${number} answered unwitnessed: ${failures.join('; ')}`);
944
+ // Closed or stalled while the witnesses were waited for: no answer either (review r2).
945
+ if (this.closed) throw new Unsealed(number, this.stalled ?? 'this checkpointer was closed: another holds its memory');
946
+ return { pair: head.pair, notes: head.notes, witnessed, failures, cosigned, proof };
947
+ }
948
+
949
+ /**
950
+ * Change `number`'s inclusion proof in a pair this checkpointer signed and wrote, from the tiles its tick wrote, and
951
+ * checked against that pair's root; null if it kept none holding it (T171).
952
+ */
953
+ private proveKept(number: bigint): { head: { pair: Pair; notes: PairNotes }; proof: InclusionProof } | null {
954
+ for (let i = this.signed.length - 1; i >= 0; i -= 1) {
955
+ const { head, tiles, changes } = this.signed[i]!;
956
+ const placed = changes.get(number);
957
+ if (placed === undefined) continue;
958
+ const { index, leaf } = placed;
959
+ const size = head.pair.changes.size;
960
+ try {
961
+ const proof = inclusionProof(nodesFrom(tileMap(tiles)), index, size);
962
+ if (verifyInclusion(leafHash(leaf), index, size, proof, head.pair.changes.root)) return { head, proof: { number, tree: 'changes', index, size, leaf, proof, notes: head.notes } };
963
+ this.options.alert(`change ${number}: the proof its tick kept does not check against its pair; proved from the database instead`);
964
+ } catch (error) {
965
+ // The tick made the tiles and the root from one edge: this is a bug, not a writer's doing. Said, and proved from
966
+ // the database, so that it costs two round trips, never an answer.
967
+ this.options.alert(`change ${number}: its tick's tiles do not prove it (${(error as Error).message}); proved from the database instead`);
968
+ }
969
+ return null;
970
+ }
971
+ return null;
972
+ }
973
+
974
+ /**
975
+ * Entry `number`'s inclusion proof against the newest pair, checked before
976
+ * it is handed out; null if not sealed. A use's `at`, if known, finds it
977
+ * in one probe; with `tree`, an entry in the other is refused.
978
+ */
979
+ prove(number: bigint, options: Locating = {}): Promise<InclusionProof | null> {
980
+ return this.proveAt(number, this.head, options);
981
+ }
982
+
983
+ private proveAt(number: bigint, head: { pair: Pair; notes: PairNotes }, options: Locating = {}): Promise<InclusionProof | null> {
984
+ return proveIn(this.store, number, head, options);
985
+ }
986
+
987
+ /** Each witness's pump: one publication in flight at a time, always of the newest pair (a witness needs only that). */
988
+ private pump(w: Witness): Pump {
989
+ let p = this.pumps.get(w);
990
+ if (p === undefined) {
991
+ p = { busy: false, next: null, acked: -1n, cosigned: null, failure: null, waiters: [] };
992
+ this.pumps.set(w, p);
993
+ }
994
+ return p;
995
+ }
996
+
997
+ private publish(): void {
998
+ for (const w of this.options.witnesses) this.kick(w);
999
+ }
1000
+
1001
+ /** Hands a witness the newest pair: sent now if its pump is idle, else next. A checkpointer closed hands none. */
1002
+ private kick(w: Witness): void {
1003
+ if (this.closed) return;
1004
+ const p = this.pump(w);
1005
+ p.next = this.head;
1006
+ if (!p.busy) void this.drain(w, p);
1007
+ }
1008
+
1009
+ private async drain(w: Witness, p: Pump): Promise<void> {
1010
+ p.busy = true;
1011
+ try {
1012
+ await this.publishAll(w, p);
1013
+ } finally {
1014
+ p.busy = false;
1015
+ }
1016
+ // Someone still waits on this witness after a failed try: try again shortly, with the newest pair.
1017
+ if (p.waiters.length > 0 && p.failure !== null) {
1018
+ const retry = setTimeout(() => {
1019
+ if (!p.busy && p.waiters.length > 0) this.kick(w);
1020
+ }, 100);
1021
+ (retry as { unref?: () => void }).unref?.();
1022
+ }
1023
+ }
1024
+
1025
+ private async publishAll(w: Witness, p: Pump): Promise<void> {
1026
+ while (p.next !== null) {
1027
+ const { notes, pair } = p.next;
1028
+ p.next = null;
1029
+ // A witness that holds more leaves than this pair has seen another history: no proof, and it refuses.
1030
+ const prove = (tree: TreeName, from: number) => (from > pair[tree].size ? [] : proveConsistency(this.store, tree, from, pair[tree].size));
1031
+ const proofs = async (from: Record<TreeName, number>) => ({ changes: await prove('changes', from.changes), uses: await prove('uses', from.uses) });
1032
+ const a = await attempt((signal) => w.publish(notes, proofs, signal), WITNESS_DEADLINE_MS);
1033
+ if (a.ok && pair.upto > p.acked) {
1034
+ p.acked = pair.upto;
1035
+ p.cosigned = a.cosigned ?? null;
1036
+ }
1037
+ p.failure = a.ok ? null : a.reason;
1038
+ p.waiters = p.waiters.filter((x) => {
1039
+ if (x.upto > p.acked) return true;
1040
+ x.done();
1041
+ return false;
1042
+ });
1043
+ }
1044
+ }
1045
+
1046
+ /** The witnesses that do not hold a pair sealing `upto` within the deadline, each with its last failure. */
1047
+ private async witnessed(upto: bigint, deadline: number): Promise<string[]> {
1048
+ const answers = await Promise.all(this.options.witnesses.map((w) => {
1049
+ const p = this.pump(w);
1050
+ if (p.acked >= upto) return null;
1051
+ const answer = new Promise<string | null>((resolve) => {
1052
+ const waiter = { upto, done: () => { clearTimeout(timer); resolve(null); } };
1053
+ // At the deadline the waiter goes, so nothing keeps retrying on its behalf after the change has answered.
1054
+ const timer = setTimeout(() => {
1055
+ p.waiters = p.waiters.filter((x) => x !== waiter);
1056
+ resolve(`witness ${w.name}: no answer within ${deadline} ms${p.failure === null ? '' : ` (last: ${p.failure})`}`);
1057
+ }, deadline);
1058
+ p.waiters.push(waiter);
1059
+ });
1060
+ // A witness behind and idle (its last try failed) is asked again now, with the newest pair.
1061
+ if (!p.busy) this.kick(w);
1062
+ return answer;
1063
+ }));
1064
+ return answers.filter((a): a is string => a !== null);
1065
+ }
1066
+ }
1067
+
1068
+ /**
1069
+ * Entry `number`'s inclusion proof against the pair `head`, checked before it is handed out; null if not sealed. The
1070
+ * checkpointer's (`prove`, a change's seal), and the vault's, which proves the cutover's change from its database
1071
+ * alone (T169): a pair covering a number may cover the gap a writer moved an entry into, so the entry is proved.
1072
+ */
1073
+ export async function proveIn(store: Pick<CheckpointStore, 'locate' | 'tiles'>, number: bigint, { pair, notes }: { pair: Pair; notes: PairNotes }, options: Locating = {}): Promise<InclusionProof | null> {
1074
+ if (number > pair.upto) return null;
1075
+ const found = await store.locate(number, options);
1076
+ if (found === null) return null;
1077
+ const { entry, index } = found;
1078
+ if (options.tree !== undefined && entry.tree !== options.tree) throw misplaced(entry, options.tree);
1079
+ if (entry.number !== number || !TREES.includes(entry.tree) || !Number.isSafeInteger(index) || index < 0) {
1080
+ throw new Refused(`the database answered for another entry than ${number}`);
1081
+ }
1082
+ // An entry numbered at or below the pair's place is in its tree by construction: an index past the tree is the
1083
+ // database's answer, not the tree's.
1084
+ const size = pair[entry.tree].size;
1085
+ if (index >= size) throw new Refused(`the database places entry ${number} at ${index}, past the ${entry.tree} tree the signer signed (${size})`);
1086
+ const nodes = nodesFrom(tileMap(await store.tiles(entry.tree, tilesFor((n) => inclusionProof(n, index, size)))));
1087
+ const proof = inclusionProof(nodes, index, size);
1088
+ if (!verifyInclusion(entryLeaf(entry.tree, entry.number, entry.columns), index, size, proof, pair[entry.tree].root)) {
1089
+ throw new Refused(`entry ${number} is not at ${index} of the ${entry.tree} tree the signer signed: the database changed it`);
1090
+ }
1091
+ return { number, tree: entry.tree, index, size, leaf: leafData(entry.tree, entry.number, entry.columns), proof, notes };
1092
+ }
1093
+
1094
+ /** An in-process witness: what the witness's Durable Object does, without the network. */
1095
+ export function localWitness(name: string, log: PairVerifiers, self: NoteSigner): Witness & { held(): WitnessState } {
1096
+ let state: WitnessState = null;
1097
+ // One publication at a time, as the Durable Object's input gate gives it: a try the pump gave up on, landing late,
1098
+ // must not put back an older pair over a newer one.
1099
+ let queue: Promise<unknown> = Promise.resolve();
1100
+ return {
1101
+ name,
1102
+ held: () => state,
1103
+ publish(notes, proofs) {
1104
+ const run = queue.then(async () => {
1105
+ const r = await witnessPair(state, notes, await proofs(state === null ? { changes: 0, uses: 0 } : sizes(state.pair)), log, self);
1106
+ state = r.state;
1107
+ return r.answer;
1108
+ });
1109
+ queue = run.catch(() => {});
1110
+ return run;
1111
+ },
1112
+ };
1113
+ }
1114
+
1115
+ /**
1116
+ * One publication, bounded: a witness that never answers, or throws before it
1117
+ * returns a promise, is a failed try, so its pump moves on to the next pair
1118
+ * instead of waiting on it for the life of the process.
1119
+ */
1120
+ async function attempt(publish: (signal: AbortSignal) => Promise<WitnessAnswer>, ms: number): Promise<WitnessAnswer> {
1121
+ const abort = new AbortController();
1122
+ let timer: ReturnType<typeof setTimeout> | undefined;
1123
+ const expired = new Promise<WitnessAnswer>((resolve) => {
1124
+ timer = setTimeout(() => {
1125
+ abort.abort(new Error(`no answer within ${ms} ms`));
1126
+ resolve({ ok: false, reason: `no answer within ${ms} ms` });
1127
+ }, ms);
1128
+ });
1129
+ try {
1130
+ return await Promise.race([Promise.resolve().then(() => publish(abort.signal)), expired]);
1131
+ } catch (error) {
1132
+ return { ok: false, reason: `unreachable: ${(error as Error).message}` };
1133
+ } finally {
1134
+ clearTimeout(timer);
1135
+ }
1136
+ }
1137
+
1138
+ /**
1139
+ * A pair the signer signed before written again, at an open: one a
1140
+ * checkpointer given up on had sent may have landed meanwhile (T169), which
1141
+ * refuses this write, a row at its place. That row being the pair is what
1142
+ * the write was for.
1143
+ */
1144
+ async function rewrite(store: CheckpointStore, row: StoredPair, tiles: Record<TreeName, Tile[]>): Promise<void> {
1145
+ try {
1146
+ await store.write(row, tiles);
1147
+ } catch (error) {
1148
+ const newest = await store.newest();
1149
+ if (newest === null || newest.upto !== row.upto || newest.notes.changes !== row.notes.changes || newest.notes.uses !== row.notes.uses) throw error;
1150
+ }
1151
+ }
1152
+
1153
+ /** Whether a row is exactly this pair: the same place and the same two notes, byte for byte. */
1154
+ const holds = (row: StoredPair | null, head: { pair: Pair; notes: PairNotes }) =>
1155
+ row !== null && row.upto === head.pair.upto && row.notes.changes === head.notes.changes && row.notes.uses === head.notes.uses;
1156
+
1157
+ const sizes = (heads: Record<TreeName, { size: number }>) => ({ changes: heads.changes.size, uses: heads.uses.size });
1158
+ /** Both trees' sizes as a message says them: `changes 4, uses 120`. */
1159
+ const said = (heads: Record<TreeName, { size: number }>) => `changes ${heads.changes.size}, uses ${heads.uses.size}`;
1160
+
1161
+ /** Appends a window's entries, in number order, each to its tree: the tiles to write. */
1162
+ function appendWindow(edges: Record<TreeName, Edge>, entries: readonly LogEntry[]): Record<TreeName, Tile[]> {
1163
+ const leaves = (tree: TreeName) => entries.filter((e) => e.tree === tree).map((e) => entryLeaf(tree, e.number, e.columns));
1164
+ return { changes: edges.changes.append(leaves('changes')), uses: edges.uses.append(leaves('uses')) };
1165
+ }