@peerbit/trusted-network 6.0.104 → 6.0.106
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/v2-policy-anchor.d.ts +75 -0
- package/dist/src/v2-policy-anchor.d.ts.map +1 -0
- package/dist/src/v2-policy-anchor.js +990 -0
- package/dist/src/v2-policy-anchor.js.map +1 -0
- package/dist/src/v2-policy-engine.d.ts +54 -11
- package/dist/src/v2-policy-engine.d.ts.map +1 -1
- package/dist/src/v2-policy-engine.js +431 -68
- package/dist/src/v2-policy-engine.js.map +1 -1
- package/dist/src/v2.d.ts +6 -0
- package/dist/src/v2.d.ts.map +1 -1
- package/dist/src/v2.js +6 -0
- package/dist/src/v2.js.map +1 -1
- package/package.json +4 -4
- package/src/v2-policy-anchor.ts +1152 -0
- package/src/v2-policy-engine.ts +621 -77
- package/src/v2.ts +7 -0
package/src/v2-policy-engine.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
PolicySnapshotBodyV2,
|
|
8
8
|
PolicySubjectBindingV2,
|
|
9
9
|
TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS,
|
|
10
|
+
TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES,
|
|
10
11
|
assertNetworkDescriptorV2,
|
|
11
12
|
decodePolicySnapshotBodyV2,
|
|
12
13
|
digestPolicySnapshotBodyV2,
|
|
@@ -22,8 +23,14 @@ import {
|
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
const DEFAULT_MAX_PENDING_POLICIES_V2 = 64;
|
|
25
|
-
const DEFAULT_MAX_PENDING_POLICY_BYTES_V2 = 256 * 1024;
|
|
26
26
|
const PENDING_POLICY_ACCOUNTING_OVERHEAD_V2 = 64;
|
|
27
|
+
const MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2 =
|
|
28
|
+
TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 2 +
|
|
29
|
+
PENDING_POLICY_ACCOUNTING_OVERHEAD_V2;
|
|
30
|
+
const DEFAULT_MAX_PENDING_POLICY_BYTES_V2 =
|
|
31
|
+
MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2;
|
|
32
|
+
const DEFAULT_POLICY_RESOLUTION_TIMEOUT_MS_V2 = 10 * 1000;
|
|
33
|
+
const MAX_TIMER_DELAY_MS_V2 = 0x7fffffff;
|
|
27
34
|
const MAX_UNAVAILABLE_REASON_LENGTH_V2 = 512;
|
|
28
35
|
|
|
29
36
|
const copyBytes = (bytes: Uint8Array): Uint8Array => Uint8Array.from(bytes);
|
|
@@ -76,7 +83,8 @@ const copySnapshot = (
|
|
|
76
83
|
|
|
77
84
|
export type PolicySnapshotResolverV2 = (
|
|
78
85
|
digest: Uint8Array,
|
|
79
|
-
|
|
86
|
+
options: { signal: AbortSignal },
|
|
87
|
+
) => Uint8Array | undefined | Promise<Uint8Array | undefined>;
|
|
80
88
|
|
|
81
89
|
export type PolicyParentFetchHintV2 = {
|
|
82
90
|
kind: "policy-parent";
|
|
@@ -114,12 +122,50 @@ export type PolicyAdmissionResultV2 = {
|
|
|
114
122
|
status: PolicyAdmissionStatusV2;
|
|
115
123
|
reason?: string;
|
|
116
124
|
head?: PolicyHeadProjectionV2;
|
|
125
|
+
/**
|
|
126
|
+
* Authenticated direct children surfaced while entering or already in
|
|
127
|
+
* FORKED. The outer durable layer can persist these proofs without decoding
|
|
128
|
+
* or verifying them a second time. They are bounded by one admission plus the
|
|
129
|
+
* pending working set and are not accumulated by this reducer.
|
|
130
|
+
*/
|
|
131
|
+
forkObservations?: PolicyForkChildProofV2[];
|
|
117
132
|
fetchHints: PolicyParentFetchHintV2[];
|
|
118
133
|
pendingCount: number;
|
|
119
134
|
pendingBytes: number;
|
|
120
135
|
evictedPolicyDigests?: Uint8Array[];
|
|
121
136
|
};
|
|
122
137
|
|
|
138
|
+
export type PolicyReducerDurableStateV2 =
|
|
139
|
+
| { formatVersion: 1; state: "EMPTY" }
|
|
140
|
+
| {
|
|
141
|
+
formatVersion: 1;
|
|
142
|
+
state: "ACTIVE";
|
|
143
|
+
acceptedHeadEntryBytes: Uint8Array;
|
|
144
|
+
}
|
|
145
|
+
| {
|
|
146
|
+
formatVersion: 1;
|
|
147
|
+
state: "UNAVAILABLE";
|
|
148
|
+
acceptedHeadEntryBytes: Uint8Array;
|
|
149
|
+
comparisonCandidateEntryBytes: Uint8Array;
|
|
150
|
+
acceptedAncestorDigest: Uint8Array;
|
|
151
|
+
reason: string;
|
|
152
|
+
}
|
|
153
|
+
| {
|
|
154
|
+
formatVersion: 1;
|
|
155
|
+
state: "FORKED";
|
|
156
|
+
commonParentEntryBytes: Uint8Array;
|
|
157
|
+
childEntryBytes: [Uint8Array, Uint8Array];
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export type TrustedNetworkV2PolicyReducerProperties = {
|
|
161
|
+
descriptor: NetworkDescriptorV2;
|
|
162
|
+
resolvePolicyEntry: PolicySnapshotResolverV2;
|
|
163
|
+
resolveTimeoutMs?: number;
|
|
164
|
+
signal?: AbortSignal;
|
|
165
|
+
maxPending?: number;
|
|
166
|
+
maxPendingPolicyBytes?: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
123
169
|
type PendingPolicySnapshotV2 = {
|
|
124
170
|
snapshot: ValidatedPolicySnapshotV2;
|
|
125
171
|
missingParentDigest: Uint8Array;
|
|
@@ -127,13 +173,14 @@ type PendingPolicySnapshotV2 = {
|
|
|
127
173
|
|
|
128
174
|
type UnavailablePolicyComparisonV2 = {
|
|
129
175
|
acceptedAncestorDigest: Uint8Array;
|
|
130
|
-
|
|
176
|
+
comparisonCandidate: ValidatedPolicySnapshotV2;
|
|
131
177
|
reason: string;
|
|
132
178
|
};
|
|
133
179
|
|
|
134
180
|
type ParentResolutionV2 =
|
|
135
181
|
| { status: "found"; parent: ValidatedPolicySnapshotV2 }
|
|
136
182
|
| { status: "missing"; digest: Uint8Array }
|
|
183
|
+
| { status: "unavailable"; digest: Uint8Array; reason: string }
|
|
137
184
|
| { status: "reject"; digest: Uint8Array; reason: string };
|
|
138
185
|
|
|
139
186
|
type SnapshotResolutionCacheV2 = Map<
|
|
@@ -144,7 +191,7 @@ type SnapshotResolutionCacheV2 = Map<
|
|
|
144
191
|
type EvaluationV2 =
|
|
145
192
|
| { status: "accept" }
|
|
146
193
|
| { status: "duplicate" }
|
|
147
|
-
| { status: "missing"; digest: Uint8Array }
|
|
194
|
+
| { status: "missing"; digest: Uint8Array; reason?: string }
|
|
148
195
|
| { status: "reject"; reason: string }
|
|
149
196
|
| { status: "unavailable"; digest: Uint8Array; reason: string }
|
|
150
197
|
| {
|
|
@@ -156,7 +203,8 @@ type EvaluationV2 =
|
|
|
156
203
|
|
|
157
204
|
type PendingDrainOutcomeV2 =
|
|
158
205
|
| { status: "accepted" }
|
|
159
|
-
| { status: "forked" }
|
|
206
|
+
| { status: "forked"; forkObservations: PolicyForkChildProofV2[] }
|
|
207
|
+
| { status: "halted" }
|
|
160
208
|
| {
|
|
161
209
|
status: "unavailable";
|
|
162
210
|
retained: boolean;
|
|
@@ -169,34 +217,61 @@ const validationMessage = (error: unknown): string =>
|
|
|
169
217
|
const boundedUnavailableReason = (reason: string): string =>
|
|
170
218
|
reason.slice(0, MAX_UNAVAILABLE_REASON_LENGTH_V2);
|
|
171
219
|
|
|
172
|
-
|
|
173
|
-
|
|
220
|
+
class PolicyDependencyUnavailableErrorV2 extends Error {
|
|
221
|
+
constructor(message: string) {
|
|
222
|
+
super(message);
|
|
223
|
+
this.name = "PolicyDependencyUnavailableErrorV2";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const capturePolicySnapshotEntryBytesV2 = (
|
|
228
|
+
entryBytes: Uint8Array,
|
|
229
|
+
): Uint8Array => {
|
|
230
|
+
if (!(entryBytes instanceof Uint8Array)) {
|
|
231
|
+
throw new Error("Policy snapshot must use canonical EntryV0 bytes");
|
|
232
|
+
}
|
|
233
|
+
if (entryBytes.byteLength === 0) {
|
|
234
|
+
throw new Error("Policy snapshot must use EntryV0");
|
|
235
|
+
}
|
|
236
|
+
if (entryBytes.byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Policy snapshot entry must contain 1-${TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES} bytes`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Apply the protocol byte ceiling before this first copy, decode, or crypto
|
|
243
|
+
// operation. The retained copy also prevents caller mutation during awaits.
|
|
244
|
+
return copyBytes(entryBytes);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const authenticateCapturedPolicySnapshotEntryV2 = async (
|
|
248
|
+
canonicalEntryBytes: Uint8Array,
|
|
174
249
|
descriptor: NetworkDescriptorV2,
|
|
175
250
|
): Promise<ValidatedPolicySnapshotV2> => {
|
|
176
|
-
|
|
177
|
-
if (!(
|
|
251
|
+
const authenticatedEntry = deserialize(canonicalEntryBytes, Entry);
|
|
252
|
+
if (!(authenticatedEntry instanceof EntryV0)) {
|
|
178
253
|
throw new Error("Policy snapshot must use EntryV0");
|
|
179
254
|
}
|
|
180
|
-
if (!(
|
|
255
|
+
if (!equals(canonicalEntryBytes, serialize(authenticatedEntry))) {
|
|
256
|
+
throw new Error("Policy snapshot entry encoding is not canonical");
|
|
257
|
+
}
|
|
258
|
+
if (!(authenticatedEntry._meta instanceof DecryptedThing)) {
|
|
181
259
|
throw new Error("Policy snapshot metadata must be public");
|
|
182
260
|
}
|
|
183
|
-
if (!(
|
|
261
|
+
if (!(authenticatedEntry._payload instanceof DecryptedThing)) {
|
|
184
262
|
throw new Error("Policy snapshot payload must be public");
|
|
185
263
|
}
|
|
186
264
|
if (
|
|
187
|
-
|
|
188
|
-
|
|
265
|
+
authenticatedEntry._signatures === undefined ||
|
|
266
|
+
authenticatedEntry._signatures.signatures.length !== 1
|
|
189
267
|
) {
|
|
190
268
|
throw new Error("Policy snapshot must contain exactly one signature");
|
|
191
269
|
}
|
|
192
|
-
if (
|
|
270
|
+
if (
|
|
271
|
+
!(authenticatedEntry._signatures.signatures[0] instanceof DecryptedThing)
|
|
272
|
+
) {
|
|
193
273
|
throw new Error("Policy snapshot signature must be public");
|
|
194
274
|
}
|
|
195
|
-
const entryBytes = serialize(entry);
|
|
196
|
-
const authenticatedEntry = deserialize(entryBytes, Entry);
|
|
197
|
-
if (!(authenticatedEntry instanceof EntryV0)) {
|
|
198
|
-
throw new Error("Policy snapshot must decode as EntryV0");
|
|
199
|
-
}
|
|
200
275
|
authenticatedEntry.init({ encoding: NO_ENCODING });
|
|
201
276
|
|
|
202
277
|
const signatures = await authenticatedEntry.getSignatures();
|
|
@@ -228,14 +303,25 @@ export const authenticatePolicySnapshotEntryV2 = async (
|
|
|
228
303
|
body: copyBody(body),
|
|
229
304
|
digest: copyBytes(digest),
|
|
230
305
|
digestKey: bytesKey(digest),
|
|
231
|
-
entryBytes,
|
|
306
|
+
entryBytes: canonicalEntryBytes,
|
|
232
307
|
accountedBytes:
|
|
233
|
-
|
|
308
|
+
canonicalEntryBytes.byteLength +
|
|
234
309
|
serialize(body).byteLength +
|
|
235
310
|
PENDING_POLICY_ACCOUNTING_OVERHEAD_V2,
|
|
236
311
|
};
|
|
237
312
|
};
|
|
238
313
|
|
|
314
|
+
export const authenticatePolicySnapshotEntryV2 = async (
|
|
315
|
+
entryBytes: Uint8Array,
|
|
316
|
+
descriptor: NetworkDescriptorV2,
|
|
317
|
+
): Promise<ValidatedPolicySnapshotV2> => {
|
|
318
|
+
assertNetworkDescriptorV2(descriptor);
|
|
319
|
+
return authenticateCapturedPolicySnapshotEntryV2(
|
|
320
|
+
capturePolicySnapshotEntryBytesV2(entryBytes),
|
|
321
|
+
descriptor,
|
|
322
|
+
);
|
|
323
|
+
};
|
|
324
|
+
|
|
239
325
|
const projectionFromSnapshot = (
|
|
240
326
|
snapshot: ValidatedPolicySnapshotV2,
|
|
241
327
|
): PolicyHeadProjectionV2 => ({
|
|
@@ -288,11 +374,93 @@ const copyForkEvidence = (
|
|
|
288
374
|
],
|
|
289
375
|
});
|
|
290
376
|
|
|
377
|
+
const captureDurableStateV2 = (
|
|
378
|
+
durableState: PolicyReducerDurableStateV2,
|
|
379
|
+
): PolicyReducerDurableStateV2 => {
|
|
380
|
+
if (
|
|
381
|
+
durableState === null ||
|
|
382
|
+
typeof durableState !== "object" ||
|
|
383
|
+
durableState.formatVersion !== 1
|
|
384
|
+
) {
|
|
385
|
+
throw new Error("Unsupported TrustedNetwork v2 reducer state format");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
switch (durableState.state) {
|
|
389
|
+
case "EMPTY":
|
|
390
|
+
return { formatVersion: 1, state: "EMPTY" };
|
|
391
|
+
case "ACTIVE":
|
|
392
|
+
return {
|
|
393
|
+
formatVersion: 1,
|
|
394
|
+
state: "ACTIVE",
|
|
395
|
+
acceptedHeadEntryBytes: capturePolicySnapshotEntryBytesV2(
|
|
396
|
+
durableState.acceptedHeadEntryBytes,
|
|
397
|
+
),
|
|
398
|
+
};
|
|
399
|
+
case "UNAVAILABLE": {
|
|
400
|
+
if (
|
|
401
|
+
!(durableState.acceptedAncestorDigest instanceof Uint8Array) ||
|
|
402
|
+
durableState.acceptedAncestorDigest.byteLength !== 32
|
|
403
|
+
) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
"Unavailable accepted ancestor digest must contain exactly 32 bytes",
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (
|
|
409
|
+
typeof durableState.reason !== "string" ||
|
|
410
|
+
durableState.reason.length === 0 ||
|
|
411
|
+
durableState.reason.length > MAX_UNAVAILABLE_REASON_LENGTH_V2
|
|
412
|
+
) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
`Unavailable reason must contain 1-${MAX_UNAVAILABLE_REASON_LENGTH_V2} characters`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
formatVersion: 1,
|
|
419
|
+
state: "UNAVAILABLE",
|
|
420
|
+
acceptedHeadEntryBytes: capturePolicySnapshotEntryBytesV2(
|
|
421
|
+
durableState.acceptedHeadEntryBytes,
|
|
422
|
+
),
|
|
423
|
+
comparisonCandidateEntryBytes: capturePolicySnapshotEntryBytesV2(
|
|
424
|
+
durableState.comparisonCandidateEntryBytes,
|
|
425
|
+
),
|
|
426
|
+
acceptedAncestorDigest: copyBytes(durableState.acceptedAncestorDigest),
|
|
427
|
+
reason: durableState.reason,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
case "FORKED":
|
|
431
|
+
if (
|
|
432
|
+
!Array.isArray(durableState.childEntryBytes) ||
|
|
433
|
+
durableState.childEntryBytes.length !== 2
|
|
434
|
+
) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
"Forked reducer state must contain exactly two children",
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
formatVersion: 1,
|
|
441
|
+
state: "FORKED",
|
|
442
|
+
commonParentEntryBytes: capturePolicySnapshotEntryBytesV2(
|
|
443
|
+
durableState.commonParentEntryBytes,
|
|
444
|
+
),
|
|
445
|
+
childEntryBytes: [
|
|
446
|
+
capturePolicySnapshotEntryBytesV2(durableState.childEntryBytes[0]),
|
|
447
|
+
capturePolicySnapshotEntryBytesV2(durableState.childEntryBytes[1]),
|
|
448
|
+
],
|
|
449
|
+
};
|
|
450
|
+
default:
|
|
451
|
+
throw new Error("Unsupported TrustedNetwork v2 reducer state");
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
291
455
|
export class TrustedNetworkV2PolicyReducer {
|
|
292
456
|
private readonly descriptor: NetworkDescriptorV2;
|
|
293
457
|
private readonly resolvePolicyEntry: PolicySnapshotResolverV2;
|
|
458
|
+
private readonly resolveTimeoutMs: number;
|
|
294
459
|
private readonly maxPending: number;
|
|
295
460
|
private readonly maxPendingPolicyBytes: number;
|
|
461
|
+
private readonly lifecycleController = new AbortController();
|
|
462
|
+
private externalSignal?: AbortSignal;
|
|
463
|
+
private externalAbortListener?: () => void;
|
|
296
464
|
private acceptedHead?: ValidatedPolicySnapshotV2;
|
|
297
465
|
private projectedRoles = new Map<string, number>();
|
|
298
466
|
private readonly pending = new Map<string, PendingPolicySnapshotV2>();
|
|
@@ -302,12 +470,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
302
470
|
private fork?: PolicyForkEvidenceV2;
|
|
303
471
|
private admissionTail: Promise<void> = Promise.resolve();
|
|
304
472
|
|
|
305
|
-
constructor(properties: {
|
|
306
|
-
descriptor: NetworkDescriptorV2;
|
|
307
|
-
resolvePolicyEntry: PolicySnapshotResolverV2;
|
|
308
|
-
maxPending?: number;
|
|
309
|
-
maxPendingPolicyBytes?: number;
|
|
310
|
-
}) {
|
|
473
|
+
constructor(properties: TrustedNetworkV2PolicyReducerProperties) {
|
|
311
474
|
assertNetworkDescriptorV2(properties.descriptor);
|
|
312
475
|
const maxPending = properties.maxPending ?? DEFAULT_MAX_PENDING_POLICIES_V2;
|
|
313
476
|
if (!Number.isSafeInteger(maxPending) || maxPending < 1) {
|
|
@@ -317,20 +480,147 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
317
480
|
properties.maxPendingPolicyBytes ?? DEFAULT_MAX_PENDING_POLICY_BYTES_V2;
|
|
318
481
|
if (
|
|
319
482
|
!Number.isSafeInteger(maxPendingPolicyBytes) ||
|
|
320
|
-
maxPendingPolicyBytes < 1
|
|
483
|
+
maxPendingPolicyBytes < 1 ||
|
|
484
|
+
maxPendingPolicyBytes > MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2
|
|
321
485
|
) {
|
|
322
|
-
throw new Error(
|
|
486
|
+
throw new Error(
|
|
487
|
+
`maxPendingPolicyBytes must be a positive safe integer no greater than ${MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2}`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
const resolveTimeoutMs =
|
|
491
|
+
properties.resolveTimeoutMs ?? DEFAULT_POLICY_RESOLUTION_TIMEOUT_MS_V2;
|
|
492
|
+
if (
|
|
493
|
+
!Number.isSafeInteger(resolveTimeoutMs) ||
|
|
494
|
+
resolveTimeoutMs < 1 ||
|
|
495
|
+
resolveTimeoutMs > MAX_TIMER_DELAY_MS_V2
|
|
496
|
+
) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`resolveTimeoutMs must be a positive safe integer no greater than ${MAX_TIMER_DELAY_MS_V2}`,
|
|
499
|
+
);
|
|
323
500
|
}
|
|
324
501
|
this.descriptor = deserialize(
|
|
325
502
|
serialize(properties.descriptor),
|
|
326
503
|
NetworkDescriptorV2,
|
|
327
504
|
);
|
|
328
505
|
this.resolvePolicyEntry = properties.resolvePolicyEntry;
|
|
506
|
+
this.resolveTimeoutMs = resolveTimeoutMs;
|
|
329
507
|
this.maxPending = maxPending;
|
|
330
508
|
this.maxPendingPolicyBytes = maxPendingPolicyBytes;
|
|
509
|
+
|
|
510
|
+
if (properties.signal?.aborted) {
|
|
511
|
+
this.lifecycleController.abort();
|
|
512
|
+
} else if (properties.signal !== undefined) {
|
|
513
|
+
this.externalSignal = properties.signal;
|
|
514
|
+
this.externalAbortListener = (): void => {
|
|
515
|
+
this.externalSignal = undefined;
|
|
516
|
+
this.externalAbortListener = undefined;
|
|
517
|
+
this.lifecycleController.abort();
|
|
518
|
+
};
|
|
519
|
+
this.externalSignal.addEventListener(
|
|
520
|
+
"abort",
|
|
521
|
+
this.externalAbortListener,
|
|
522
|
+
{ once: true },
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
static async restore(
|
|
528
|
+
properties: TrustedNetworkV2PolicyReducerProperties & {
|
|
529
|
+
durableState: PolicyReducerDurableStateV2;
|
|
530
|
+
},
|
|
531
|
+
): Promise<TrustedNetworkV2PolicyReducer> {
|
|
532
|
+
// Capture the complete checkpoint before the first await. Persistence
|
|
533
|
+
// adapters commonly reuse read buffers, and mutation during authentication
|
|
534
|
+
// must not change what is restored.
|
|
535
|
+
const durableState = captureDurableStateV2(properties.durableState);
|
|
536
|
+
const reducer = new TrustedNetworkV2PolicyReducer(properties);
|
|
537
|
+
const authenticate = (
|
|
538
|
+
entryBytes: Uint8Array,
|
|
539
|
+
): Promise<ValidatedPolicySnapshotV2> =>
|
|
540
|
+
authenticateCapturedPolicySnapshotEntryV2(entryBytes, reducer.descriptor);
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
switch (durableState.state) {
|
|
544
|
+
case "EMPTY":
|
|
545
|
+
return reducer;
|
|
546
|
+
case "ACTIVE": {
|
|
547
|
+
const acceptedHead = await authenticate(
|
|
548
|
+
durableState.acceptedHeadEntryBytes,
|
|
549
|
+
);
|
|
550
|
+
// A durable ACTIVE head is a trusted prior-validation checkpoint. Its
|
|
551
|
+
// authority signature and network binding are re-authenticated above,
|
|
552
|
+
// but restore deliberately does not require historical resolver data.
|
|
553
|
+
reducer.project(acceptedHead);
|
|
554
|
+
return reducer;
|
|
555
|
+
}
|
|
556
|
+
case "UNAVAILABLE": {
|
|
557
|
+
const [acceptedHead, comparisonCandidate] = await Promise.all([
|
|
558
|
+
authenticate(durableState.acceptedHeadEntryBytes),
|
|
559
|
+
authenticate(durableState.comparisonCandidateEntryBytes),
|
|
560
|
+
]);
|
|
561
|
+
if (equals(acceptedHead.digest, comparisonCandidate.digest)) {
|
|
562
|
+
throw new Error(
|
|
563
|
+
"Unavailable comparison candidate must differ from the accepted head",
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
reducer.project(acceptedHead);
|
|
567
|
+
reducer.addPending(
|
|
568
|
+
comparisonCandidate,
|
|
569
|
+
durableState.acceptedAncestorDigest,
|
|
570
|
+
);
|
|
571
|
+
reducer.unavailable = {
|
|
572
|
+
acceptedAncestorDigest: copyBytes(
|
|
573
|
+
durableState.acceptedAncestorDigest,
|
|
574
|
+
),
|
|
575
|
+
comparisonCandidate: copySnapshot(comparisonCandidate),
|
|
576
|
+
reason: durableState.reason,
|
|
577
|
+
};
|
|
578
|
+
return reducer;
|
|
579
|
+
}
|
|
580
|
+
case "FORKED": {
|
|
581
|
+
const [commonParent, firstChild, secondChild] = await Promise.all([
|
|
582
|
+
authenticate(durableState.commonParentEntryBytes),
|
|
583
|
+
authenticate(durableState.childEntryBytes[0]),
|
|
584
|
+
authenticate(durableState.childEntryBytes[1]),
|
|
585
|
+
]);
|
|
586
|
+
for (const child of [firstChild, secondChild]) {
|
|
587
|
+
if (
|
|
588
|
+
child.body.sequence !== commonParent.body.sequence + 1n ||
|
|
589
|
+
!equals(child.body.previousPolicyDigest, commonParent.digest)
|
|
590
|
+
) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
"Fork child must be a direct successor of the common parent",
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (equals(firstChild.digest, secondChild.digest)) {
|
|
597
|
+
throw new Error("Fork children must have distinct policy digests");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const children = [
|
|
601
|
+
forkProofFromSnapshot(firstChild),
|
|
602
|
+
forkProofFromSnapshot(secondChild),
|
|
603
|
+
].sort(compareForkChildProofs) as [
|
|
604
|
+
PolicyForkChildProofV2,
|
|
605
|
+
PolicyForkChildProofV2,
|
|
606
|
+
];
|
|
607
|
+
reducer.project(commonParent);
|
|
608
|
+
reducer.fork = {
|
|
609
|
+
commonParent: projectionFromSnapshot(commonParent),
|
|
610
|
+
children,
|
|
611
|
+
};
|
|
612
|
+
return reducer;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
} catch (error) {
|
|
616
|
+
// Do not retain a caller-owned AbortSignal listener when restore rejects.
|
|
617
|
+
reducer.abort();
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
331
620
|
}
|
|
332
621
|
|
|
333
|
-
get state(): "EMPTY" | "ACTIVE" | "UNAVAILABLE" | "FORKED" {
|
|
622
|
+
get state(): "EMPTY" | "ACTIVE" | "UNAVAILABLE" | "FORKED" | "HALTED" {
|
|
623
|
+
if (this.lifecycleController.signal.aborted) return "HALTED";
|
|
334
624
|
if (this.fork !== undefined) return "FORKED";
|
|
335
625
|
if (this.unavailable !== undefined) return "UNAVAILABLE";
|
|
336
626
|
return this.acceptedHead === undefined ? "EMPTY" : "ACTIVE";
|
|
@@ -364,6 +654,50 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
364
654
|
.map(({ snapshot }) => copyBytes(snapshot.digest));
|
|
365
655
|
}
|
|
366
656
|
|
|
657
|
+
exportDurableState(): PolicyReducerDurableStateV2 {
|
|
658
|
+
// Lifecycle cancellation is process-local. Export the underlying protocol
|
|
659
|
+
// safety state so a replacement process cannot erase UNAVAILABLE/FORKED.
|
|
660
|
+
if (this.fork !== undefined) {
|
|
661
|
+
if (this.acceptedHead === undefined) {
|
|
662
|
+
throw new Error("Forked reducer is missing its common-parent entry");
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
formatVersion: 1,
|
|
666
|
+
state: "FORKED",
|
|
667
|
+
commonParentEntryBytes: copyBytes(this.acceptedHead.entryBytes),
|
|
668
|
+
childEntryBytes: [
|
|
669
|
+
copyBytes(this.fork.children[0].entryBytes),
|
|
670
|
+
copyBytes(this.fork.children[1].entryBytes),
|
|
671
|
+
],
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
if (this.unavailable !== undefined) {
|
|
675
|
+
if (this.acceptedHead === undefined) {
|
|
676
|
+
throw new Error("Unavailable reducer is missing its accepted head");
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
formatVersion: 1,
|
|
680
|
+
state: "UNAVAILABLE",
|
|
681
|
+
acceptedHeadEntryBytes: copyBytes(this.acceptedHead.entryBytes),
|
|
682
|
+
comparisonCandidateEntryBytes: copyBytes(
|
|
683
|
+
this.unavailable.comparisonCandidate.entryBytes,
|
|
684
|
+
),
|
|
685
|
+
acceptedAncestorDigest: copyBytes(
|
|
686
|
+
this.unavailable.acceptedAncestorDigest,
|
|
687
|
+
),
|
|
688
|
+
reason: this.unavailable.reason,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
if (this.acceptedHead !== undefined) {
|
|
692
|
+
return {
|
|
693
|
+
formatVersion: 1,
|
|
694
|
+
state: "ACTIVE",
|
|
695
|
+
acceptedHeadEntryBytes: copyBytes(this.acceptedHead.entryBytes),
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
return { formatVersion: 1, state: "EMPTY" };
|
|
699
|
+
}
|
|
700
|
+
|
|
367
701
|
rolesFor(subject: PublicSignKey): number {
|
|
368
702
|
return this.projectedRoles.get(publicKeyId(subject)) ?? 0;
|
|
369
703
|
}
|
|
@@ -380,6 +714,21 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
380
714
|
return (this.rolesFor(subject) & roles) === roles;
|
|
381
715
|
}
|
|
382
716
|
|
|
717
|
+
abort(): void {
|
|
718
|
+
if (
|
|
719
|
+
this.externalSignal !== undefined &&
|
|
720
|
+
this.externalAbortListener !== undefined
|
|
721
|
+
) {
|
|
722
|
+
this.externalSignal.removeEventListener(
|
|
723
|
+
"abort",
|
|
724
|
+
this.externalAbortListener,
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
this.externalSignal = undefined;
|
|
728
|
+
this.externalAbortListener = undefined;
|
|
729
|
+
this.lifecycleController.abort();
|
|
730
|
+
}
|
|
731
|
+
|
|
383
732
|
private fetchHints(): PolicyParentFetchHintV2[] {
|
|
384
733
|
const unique = new Map<string, Uint8Array>();
|
|
385
734
|
for (const { missingParentDigest } of this.pending.values()) {
|
|
@@ -403,11 +752,16 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
403
752
|
status: PolicyAdmissionStatusV2,
|
|
404
753
|
reason?: string,
|
|
405
754
|
evictedPolicyDigests?: Uint8Array[],
|
|
755
|
+
forkObservations?: PolicyForkChildProofV2[],
|
|
406
756
|
): PolicyAdmissionResultV2 {
|
|
407
757
|
return {
|
|
408
758
|
status,
|
|
409
759
|
reason,
|
|
410
760
|
head: this.head,
|
|
761
|
+
forkObservations:
|
|
762
|
+
forkObservations === undefined
|
|
763
|
+
? undefined
|
|
764
|
+
: forkObservations.map(copyForkChildProof),
|
|
411
765
|
fetchHints: this.fetchHints(),
|
|
412
766
|
pendingCount: this.pending.size,
|
|
413
767
|
pendingBytes: this.pendingBytes,
|
|
@@ -418,8 +772,19 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
418
772
|
};
|
|
419
773
|
}
|
|
420
774
|
|
|
421
|
-
private forkedResult(
|
|
422
|
-
|
|
775
|
+
private forkedResult(
|
|
776
|
+
forkObservations?: PolicyForkChildProofV2[],
|
|
777
|
+
): PolicyAdmissionResultV2 {
|
|
778
|
+
return this.result(
|
|
779
|
+
"forked",
|
|
780
|
+
"Policy authority signed competing children",
|
|
781
|
+
undefined,
|
|
782
|
+
forkObservations,
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private haltedResult(): PolicyAdmissionResultV2 {
|
|
787
|
+
return this.result("halted", "Policy reducer lifecycle is aborted");
|
|
423
788
|
}
|
|
424
789
|
|
|
425
790
|
private unavailableResult(retention?: {
|
|
@@ -428,7 +793,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
428
793
|
}): PolicyAdmissionResultV2 {
|
|
429
794
|
const blockedCandidateRetained =
|
|
430
795
|
this.unavailable !== undefined &&
|
|
431
|
-
this.pending.has(this.unavailable.
|
|
796
|
+
this.pending.has(this.unavailable.comparisonCandidate.digestKey);
|
|
432
797
|
const recoverable =
|
|
433
798
|
(retention?.retained ?? true) && blockedCandidateRetained;
|
|
434
799
|
const reason = recoverable
|
|
@@ -446,7 +811,10 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
446
811
|
private completedDrainResult(
|
|
447
812
|
outcome: PendingDrainOutcomeV2 | undefined,
|
|
448
813
|
): PolicyAdmissionResultV2 | undefined {
|
|
449
|
-
if (outcome?.status === "forked")
|
|
814
|
+
if (outcome?.status === "forked") {
|
|
815
|
+
return this.forkedResult(outcome.forkObservations);
|
|
816
|
+
}
|
|
817
|
+
if (outcome?.status === "halted") return this.haltedResult();
|
|
450
818
|
return outcome?.status === "unavailable"
|
|
451
819
|
? this.unavailableResult(outcome)
|
|
452
820
|
: undefined;
|
|
@@ -471,6 +839,88 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
471
839
|
}
|
|
472
840
|
}
|
|
473
841
|
|
|
842
|
+
private async resolveExternalSnapshot(
|
|
843
|
+
digest: Uint8Array,
|
|
844
|
+
): Promise<ValidatedPolicySnapshotV2 | undefined> {
|
|
845
|
+
if (this.lifecycleController.signal.aborted) {
|
|
846
|
+
throw new PolicyDependencyUnavailableErrorV2(
|
|
847
|
+
"Policy resolver lifecycle is aborted",
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const attemptController = new AbortController();
|
|
852
|
+
let timedOut = false;
|
|
853
|
+
const abortFromLifecycle = (): void => attemptController.abort();
|
|
854
|
+
this.lifecycleController.signal.addEventListener(
|
|
855
|
+
"abort",
|
|
856
|
+
abortFromLifecycle,
|
|
857
|
+
{ once: true },
|
|
858
|
+
);
|
|
859
|
+
const timeout = setTimeout(() => {
|
|
860
|
+
timedOut = true;
|
|
861
|
+
attemptController.abort();
|
|
862
|
+
}, this.resolveTimeoutMs);
|
|
863
|
+
const abortError = (): PolicyDependencyUnavailableErrorV2 =>
|
|
864
|
+
new PolicyDependencyUnavailableErrorV2(
|
|
865
|
+
timedOut
|
|
866
|
+
? `Policy resolver timed out after ${this.resolveTimeoutMs} ms`
|
|
867
|
+
: "Policy resolver attempt was aborted",
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
let rejectOnAbort: (() => void) | undefined;
|
|
871
|
+
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
872
|
+
rejectOnAbort = (): void => {
|
|
873
|
+
reject(abortError());
|
|
874
|
+
};
|
|
875
|
+
attemptController.signal.addEventListener("abort", rejectOnAbort, {
|
|
876
|
+
once: true,
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
const resolution = Promise.resolve().then(async () => {
|
|
881
|
+
if (attemptController.signal.aborted) throw abortError();
|
|
882
|
+
const entryBytes = await this.resolvePolicyEntry(copyBytes(digest), {
|
|
883
|
+
signal: attemptController.signal,
|
|
884
|
+
});
|
|
885
|
+
// A resolver may ignore cancellation. Do not spend decode or signature
|
|
886
|
+
// verification work on bytes that arrive after this attempt expired.
|
|
887
|
+
if (attemptController.signal.aborted) throw abortError();
|
|
888
|
+
if (entryBytes === undefined) return undefined;
|
|
889
|
+
const snapshot = await authenticatePolicySnapshotEntryV2(
|
|
890
|
+
entryBytes,
|
|
891
|
+
this.descriptor,
|
|
892
|
+
);
|
|
893
|
+
if (!equals(snapshot.digest, digest)) {
|
|
894
|
+
throw new Error("Policy resolver returned the wrong body digest");
|
|
895
|
+
}
|
|
896
|
+
return snapshot;
|
|
897
|
+
});
|
|
898
|
+
// Promise.race installs handlers, but this explicit observer documents and
|
|
899
|
+
// preserves consumption if the resolver settles after its deadline.
|
|
900
|
+
void resolution.then(
|
|
901
|
+
(): void => undefined,
|
|
902
|
+
(): void => undefined,
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
return await Promise.race([resolution, abortPromise]);
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (error instanceof PolicyDependencyUnavailableErrorV2) throw error;
|
|
909
|
+
throw new PolicyDependencyUnavailableErrorV2(
|
|
910
|
+
`Policy resolver dependency is unavailable: ${validationMessage(error)}`,
|
|
911
|
+
);
|
|
912
|
+
} finally {
|
|
913
|
+
clearTimeout(timeout);
|
|
914
|
+
this.lifecycleController.signal.removeEventListener(
|
|
915
|
+
"abort",
|
|
916
|
+
abortFromLifecycle,
|
|
917
|
+
);
|
|
918
|
+
if (rejectOnAbort !== undefined) {
|
|
919
|
+
attemptController.signal.removeEventListener("abort", rejectOnAbort);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
474
924
|
private async resolveSnapshot(
|
|
475
925
|
digest: Uint8Array,
|
|
476
926
|
cache?: SnapshotResolutionCacheV2,
|
|
@@ -483,18 +933,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
483
933
|
if (pending !== undefined) return copySnapshot(pending.snapshot);
|
|
484
934
|
let resolution = cache?.get(digestKey);
|
|
485
935
|
if (resolution === undefined) {
|
|
486
|
-
resolution = (
|
|
487
|
-
const entry = await this.resolvePolicyEntry(copyBytes(digest));
|
|
488
|
-
if (entry === undefined) return undefined;
|
|
489
|
-
const snapshot = await authenticatePolicySnapshotEntryV2(
|
|
490
|
-
entry,
|
|
491
|
-
this.descriptor,
|
|
492
|
-
);
|
|
493
|
-
if (!equals(snapshot.digest, digest)) {
|
|
494
|
-
throw new Error("Policy resolver returned the wrong body digest");
|
|
495
|
-
}
|
|
496
|
-
return snapshot;
|
|
497
|
-
})();
|
|
936
|
+
resolution = this.resolveExternalSnapshot(digest);
|
|
498
937
|
cache?.set(digestKey, resolution);
|
|
499
938
|
}
|
|
500
939
|
const snapshot = await resolution;
|
|
@@ -520,9 +959,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
520
959
|
);
|
|
521
960
|
} catch (error) {
|
|
522
961
|
return {
|
|
523
|
-
status: "
|
|
962
|
+
status: "unavailable",
|
|
524
963
|
digest: copyBytes(child.body.previousPolicyDigest),
|
|
525
|
-
reason: `Policy parent
|
|
964
|
+
reason: `Policy parent dependency is unavailable: ${validationMessage(error)}`,
|
|
526
965
|
};
|
|
527
966
|
}
|
|
528
967
|
if (parent === undefined) {
|
|
@@ -541,6 +980,18 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
541
980
|
return { status: "found", parent };
|
|
542
981
|
}
|
|
543
982
|
|
|
983
|
+
private candidateAncestryResult(
|
|
984
|
+
resolution: Exclude<ParentResolutionV2, { status: "found" }>,
|
|
985
|
+
): EvaluationV2 {
|
|
986
|
+
return resolution.status === "unavailable"
|
|
987
|
+
? {
|
|
988
|
+
status: "missing",
|
|
989
|
+
digest: copyBytes(resolution.digest),
|
|
990
|
+
reason: resolution.reason,
|
|
991
|
+
}
|
|
992
|
+
: resolution;
|
|
993
|
+
}
|
|
994
|
+
|
|
544
995
|
private acceptedAncestryUnavailable(
|
|
545
996
|
resolution: Exclude<ParentResolutionV2, { status: "found" }>,
|
|
546
997
|
): Extract<EvaluationV2, { status: "unavailable" }> {
|
|
@@ -550,7 +1001,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
550
1001
|
reason:
|
|
551
1002
|
resolution.status === "missing"
|
|
552
1003
|
? "Accepted policy ancestry is unavailable from the resolver"
|
|
553
|
-
:
|
|
1004
|
+
: resolution.status === "unavailable"
|
|
1005
|
+
? `Accepted policy ancestry is unavailable: ${resolution.reason}`
|
|
1006
|
+
: `Accepted policy ancestry validation failed: ${resolution.reason}`,
|
|
554
1007
|
};
|
|
555
1008
|
}
|
|
556
1009
|
|
|
@@ -562,7 +1015,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
562
1015
|
let cursor = candidate;
|
|
563
1016
|
while (cursor.body.sequence !== 0n) {
|
|
564
1017
|
const parent = await this.parentOf(cursor, resolutionCache);
|
|
565
|
-
if (parent.status !== "found")
|
|
1018
|
+
if (parent.status !== "found") {
|
|
1019
|
+
return this.candidateAncestryResult(parent);
|
|
1020
|
+
}
|
|
566
1021
|
cursor = parent.parent;
|
|
567
1022
|
}
|
|
568
1023
|
return { status: "accept" };
|
|
@@ -576,7 +1031,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
576
1031
|
while (candidateCursor.body.sequence > acceptedCursor.body.sequence) {
|
|
577
1032
|
candidateChild = candidateCursor;
|
|
578
1033
|
const parent = await this.parentOf(candidateCursor, resolutionCache);
|
|
579
|
-
if (parent.status !== "found")
|
|
1034
|
+
if (parent.status !== "found") {
|
|
1035
|
+
return this.candidateAncestryResult(parent);
|
|
1036
|
+
}
|
|
580
1037
|
candidateCursor = parent.parent;
|
|
581
1038
|
}
|
|
582
1039
|
while (acceptedCursor.body.sequence > candidateCursor.body.sequence) {
|
|
@@ -607,7 +1064,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
607
1064
|
if (acceptedParent.status !== "found") {
|
|
608
1065
|
return this.acceptedAncestryUnavailable(acceptedParent);
|
|
609
1066
|
}
|
|
610
|
-
if (candidateParent.status !== "found")
|
|
1067
|
+
if (candidateParent.status !== "found") {
|
|
1068
|
+
return this.candidateAncestryResult(candidateParent);
|
|
1069
|
+
}
|
|
611
1070
|
candidateCursor = candidateParent.parent;
|
|
612
1071
|
acceptedCursor = acceptedParent.parent;
|
|
613
1072
|
}
|
|
@@ -629,7 +1088,39 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
629
1088
|
};
|
|
630
1089
|
}
|
|
631
1090
|
|
|
632
|
-
private setFork(
|
|
1091
|
+
private setFork(
|
|
1092
|
+
evaluation: Extract<EvaluationV2, { status: "fork" }>,
|
|
1093
|
+
): PolicyForkChildProofV2[] {
|
|
1094
|
+
// Pending snapshots were authenticated when admitted. Combine every one
|
|
1095
|
+
// that is already provably a direct child with the pair that first exposed
|
|
1096
|
+
// the fork. Canonical selection below may displace either initial child, so
|
|
1097
|
+
// the durable layer needs the complete bounded observation set and removes
|
|
1098
|
+
// whichever final pair is carried by exportDurableState().
|
|
1099
|
+
const pendingForkObservationSnapshots = [...this.pending.values()]
|
|
1100
|
+
.map(({ snapshot }) => snapshot)
|
|
1101
|
+
.filter(
|
|
1102
|
+
(snapshot) =>
|
|
1103
|
+
snapshot.body.sequence ===
|
|
1104
|
+
evaluation.commonParent.body.sequence + 1n &&
|
|
1105
|
+
equals(
|
|
1106
|
+
snapshot.body.previousPolicyDigest,
|
|
1107
|
+
evaluation.commonParent.digest,
|
|
1108
|
+
),
|
|
1109
|
+
);
|
|
1110
|
+
const forkObservationSnapshots = [
|
|
1111
|
+
evaluation.candidateChild,
|
|
1112
|
+
evaluation.acceptedChild,
|
|
1113
|
+
...pendingForkObservationSnapshots,
|
|
1114
|
+
];
|
|
1115
|
+
const forkObservations = forkObservationSnapshots
|
|
1116
|
+
.map(forkProofFromSnapshot)
|
|
1117
|
+
.sort(compareForkChildProofs)
|
|
1118
|
+
.filter(
|
|
1119
|
+
(proof, index, observations) =>
|
|
1120
|
+
index === 0 ||
|
|
1121
|
+
!equals(proof.entryBytes, observations[index - 1]!.entryBytes),
|
|
1122
|
+
);
|
|
1123
|
+
|
|
633
1124
|
this.project(evaluation.commonParent);
|
|
634
1125
|
const children = [
|
|
635
1126
|
forkProofFromSnapshot(evaluation.candidateChild),
|
|
@@ -642,8 +1133,12 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
642
1133
|
commonParent: projectionFromSnapshot(evaluation.commonParent),
|
|
643
1134
|
children,
|
|
644
1135
|
};
|
|
1136
|
+
for (const snapshot of forkObservationSnapshots) {
|
|
1137
|
+
this.retainCanonicalForkChild(snapshot);
|
|
1138
|
+
}
|
|
645
1139
|
this.unavailable = undefined;
|
|
646
1140
|
this.pending.clear();
|
|
1141
|
+
return forkObservations;
|
|
647
1142
|
}
|
|
648
1143
|
|
|
649
1144
|
private retainCanonicalForkChild(snapshot: ValidatedPolicySnapshotV2): void {
|
|
@@ -669,20 +1164,26 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
669
1164
|
this.fork.children = [canonical[0]!, canonical[1]!];
|
|
670
1165
|
}
|
|
671
1166
|
|
|
672
|
-
private observeAfterFork(
|
|
673
|
-
|
|
1167
|
+
private observeAfterFork(
|
|
1168
|
+
snapshot: ValidatedPolicySnapshotV2,
|
|
1169
|
+
): PolicyForkChildProofV2 | undefined {
|
|
1170
|
+
if (this.fork === undefined || this.acceptedHead === undefined) {
|
|
1171
|
+
return undefined;
|
|
1172
|
+
}
|
|
674
1173
|
const commonParent = this.acceptedHead;
|
|
675
1174
|
if (
|
|
676
1175
|
snapshot.body.sequence !== commonParent.body.sequence + 1n ||
|
|
677
1176
|
!equals(snapshot.body.previousPolicyDigest, commonParent.digest)
|
|
678
1177
|
) {
|
|
679
|
-
return;
|
|
1178
|
+
return undefined;
|
|
680
1179
|
}
|
|
681
1180
|
|
|
682
|
-
// This bounded kernel
|
|
683
|
-
//
|
|
684
|
-
//
|
|
1181
|
+
// This bounded kernel retains only the canonical two direct child proofs.
|
|
1182
|
+
// Return this already-authenticated observation so the durable outer layer
|
|
1183
|
+
// can retain every proof without verifying it twice.
|
|
1184
|
+
const observation = forkProofFromSnapshot(snapshot);
|
|
685
1185
|
this.retainCanonicalForkChild(snapshot);
|
|
1186
|
+
return observation;
|
|
686
1187
|
}
|
|
687
1188
|
|
|
688
1189
|
private addPending(
|
|
@@ -735,13 +1236,16 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
735
1236
|
const retention = this.addPending(snapshot, evaluation.digest);
|
|
736
1237
|
this.unavailable = {
|
|
737
1238
|
acceptedAncestorDigest: copyBytes(evaluation.digest),
|
|
738
|
-
|
|
1239
|
+
comparisonCandidate: copySnapshot(snapshot),
|
|
739
1240
|
reason: boundedUnavailableReason(evaluation.reason),
|
|
740
1241
|
};
|
|
741
1242
|
return retention;
|
|
742
1243
|
}
|
|
743
1244
|
|
|
744
1245
|
private async drainPending(): Promise<PendingDrainOutcomeV2 | undefined> {
|
|
1246
|
+
if (this.lifecycleController.signal.aborted) {
|
|
1247
|
+
return { status: "halted" };
|
|
1248
|
+
}
|
|
745
1249
|
let accepted = false;
|
|
746
1250
|
let progress = true;
|
|
747
1251
|
while (
|
|
@@ -757,6 +1261,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
757
1261
|
for (const pending of ordered) {
|
|
758
1262
|
if (!this.pending.has(pending.snapshot.digestKey)) continue;
|
|
759
1263
|
const evaluation = await this.evaluate(pending.snapshot);
|
|
1264
|
+
if (this.lifecycleController.signal.aborted) {
|
|
1265
|
+
return { status: "halted" };
|
|
1266
|
+
}
|
|
760
1267
|
if (evaluation.status === "missing") {
|
|
761
1268
|
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
762
1269
|
continue;
|
|
@@ -771,8 +1278,10 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
771
1278
|
this.project(pending.snapshot);
|
|
772
1279
|
accepted = true;
|
|
773
1280
|
} else if (evaluation.status === "fork") {
|
|
774
|
-
|
|
775
|
-
|
|
1281
|
+
return {
|
|
1282
|
+
status: "forked",
|
|
1283
|
+
forkObservations: this.setFork(evaluation),
|
|
1284
|
+
};
|
|
776
1285
|
}
|
|
777
1286
|
}
|
|
778
1287
|
}
|
|
@@ -782,7 +1291,15 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
782
1291
|
private enqueueAdmission(
|
|
783
1292
|
operation: () => Promise<PolicyAdmissionResultV2>,
|
|
784
1293
|
): Promise<PolicyAdmissionResultV2> {
|
|
785
|
-
const result = this.admissionTail.then(
|
|
1294
|
+
const result = this.admissionTail.then(async () => {
|
|
1295
|
+
if (this.lifecycleController.signal.aborted) {
|
|
1296
|
+
return this.haltedResult();
|
|
1297
|
+
}
|
|
1298
|
+
const admission = await operation();
|
|
1299
|
+
return this.lifecycleController.signal.aborted
|
|
1300
|
+
? this.haltedResult()
|
|
1301
|
+
: admission;
|
|
1302
|
+
});
|
|
786
1303
|
this.admissionTail = result.then(
|
|
787
1304
|
(): void => {},
|
|
788
1305
|
(_reason: unknown): void => {},
|
|
@@ -790,30 +1307,47 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
790
1307
|
return result;
|
|
791
1308
|
}
|
|
792
1309
|
|
|
793
|
-
ingest(
|
|
794
|
-
|
|
1310
|
+
ingest(entryBytes: Uint8Array): Promise<PolicyAdmissionResultV2> {
|
|
1311
|
+
if (this.lifecycleController.signal.aborted) {
|
|
1312
|
+
return Promise.resolve(this.haltedResult());
|
|
1313
|
+
}
|
|
1314
|
+
let capturedEntryBytes: Uint8Array;
|
|
1315
|
+
try {
|
|
1316
|
+
// Capture at the API boundary, before this admission waits behind earlier
|
|
1317
|
+
// work. Otherwise a caller could mutate a queued entry before validation.
|
|
1318
|
+
capturedEntryBytes = capturePolicySnapshotEntryBytesV2(entryBytes);
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
const reason = validationMessage(error);
|
|
1321
|
+
return this.enqueueAdmission(async () => this.result("rejected", reason));
|
|
1322
|
+
}
|
|
1323
|
+
return this.enqueueAdmission(() => this.ingestOne(capturedEntryBytes));
|
|
795
1324
|
}
|
|
796
1325
|
|
|
797
1326
|
retryUnavailable(): Promise<PolicyAdmissionResultV2> {
|
|
798
1327
|
return this.enqueueAdmission(() => this.retryUnavailableOne());
|
|
799
1328
|
}
|
|
800
1329
|
|
|
801
|
-
private async ingestOne(
|
|
1330
|
+
private async ingestOne(
|
|
1331
|
+
entryBytes: Uint8Array,
|
|
1332
|
+
): Promise<PolicyAdmissionResultV2> {
|
|
802
1333
|
let snapshot: ValidatedPolicySnapshotV2;
|
|
803
1334
|
try {
|
|
804
|
-
snapshot = await
|
|
805
|
-
|
|
1335
|
+
snapshot = await authenticateCapturedPolicySnapshotEntryV2(
|
|
1336
|
+
entryBytes,
|
|
806
1337
|
this.descriptor,
|
|
807
1338
|
);
|
|
808
1339
|
} catch (error) {
|
|
809
1340
|
return this.result("rejected", validationMessage(error));
|
|
810
1341
|
}
|
|
1342
|
+
if (this.lifecycleController.signal.aborted) return this.haltedResult();
|
|
811
1343
|
|
|
812
1344
|
if (this.fork !== undefined) {
|
|
813
|
-
this.observeAfterFork(snapshot);
|
|
1345
|
+
const forkObservation = this.observeAfterFork(snapshot);
|
|
814
1346
|
return this.result(
|
|
815
1347
|
"halted",
|
|
816
1348
|
"Policy reducer is halted by authority equivocation",
|
|
1349
|
+
undefined,
|
|
1350
|
+
forkObservation === undefined ? undefined : [forkObservation],
|
|
817
1351
|
);
|
|
818
1352
|
}
|
|
819
1353
|
this.retainCanonicalHeadEntry(snapshot);
|
|
@@ -822,6 +1356,15 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
822
1356
|
if (this.acceptedHead?.digestKey === snapshot.digestKey) {
|
|
823
1357
|
return this.result("unavailable", this.unavailable.reason);
|
|
824
1358
|
}
|
|
1359
|
+
if (
|
|
1360
|
+
this.unavailable.comparisonCandidate.digestKey === snapshot.digestKey &&
|
|
1361
|
+
compare(
|
|
1362
|
+
snapshot.entryBytes,
|
|
1363
|
+
this.unavailable.comparisonCandidate.entryBytes,
|
|
1364
|
+
) < 0
|
|
1365
|
+
) {
|
|
1366
|
+
this.unavailable.comparisonCandidate = copySnapshot(snapshot);
|
|
1367
|
+
}
|
|
825
1368
|
const existingPending = this.pending.get(snapshot.digestKey);
|
|
826
1369
|
if (existingPending !== undefined) {
|
|
827
1370
|
this.addPending(snapshot, existingPending.missingParentDigest);
|
|
@@ -841,6 +1384,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
841
1384
|
}
|
|
842
1385
|
|
|
843
1386
|
const evaluation = await this.evaluate(snapshot);
|
|
1387
|
+
if (this.lifecycleController.signal.aborted) return this.haltedResult();
|
|
844
1388
|
if (evaluation.status === "reject") {
|
|
845
1389
|
return this.result("rejected", evaluation.reason);
|
|
846
1390
|
}
|
|
@@ -848,8 +1392,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
848
1392
|
return this.result("duplicate");
|
|
849
1393
|
}
|
|
850
1394
|
if (evaluation.status === "fork") {
|
|
851
|
-
this.setFork(evaluation);
|
|
852
|
-
return this.forkedResult();
|
|
1395
|
+
return this.forkedResult(this.setFork(evaluation));
|
|
853
1396
|
}
|
|
854
1397
|
if (evaluation.status === "unavailable") {
|
|
855
1398
|
const retention = this.enterUnavailable(snapshot, evaluation);
|
|
@@ -860,7 +1403,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
860
1403
|
return this.result(
|
|
861
1404
|
pending.retained ? "pending" : "capacity",
|
|
862
1405
|
pending.retained
|
|
863
|
-
? "Policy parent is missing"
|
|
1406
|
+
? (evaluation.reason ?? "Policy parent is missing")
|
|
864
1407
|
: "Policy pending capacity did not retain this candidate",
|
|
865
1408
|
pending.evictedPolicyDigests,
|
|
866
1409
|
);
|
|
@@ -874,6 +1417,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
874
1417
|
}
|
|
875
1418
|
|
|
876
1419
|
private async retryUnavailableOne(): Promise<PolicyAdmissionResultV2> {
|
|
1420
|
+
if (this.lifecycleController.signal.aborted) return this.haltedResult();
|
|
877
1421
|
if (this.fork !== undefined) {
|
|
878
1422
|
return this.result(
|
|
879
1423
|
"halted",
|
|
@@ -884,7 +1428,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
884
1428
|
if (unavailable === undefined) {
|
|
885
1429
|
return this.result("duplicate", "Policy reducer is not unavailable");
|
|
886
1430
|
}
|
|
887
|
-
const pending = this.pending.get(unavailable.
|
|
1431
|
+
const pending = this.pending.get(unavailable.comparisonCandidate.digestKey);
|
|
888
1432
|
if (pending === undefined) {
|
|
889
1433
|
return this.result(
|
|
890
1434
|
"capacity",
|
|
@@ -893,11 +1437,12 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
893
1437
|
}
|
|
894
1438
|
|
|
895
1439
|
const evaluation = await this.evaluate(pending.snapshot);
|
|
1440
|
+
if (this.lifecycleController.signal.aborted) return this.haltedResult();
|
|
896
1441
|
if (evaluation.status === "unavailable") {
|
|
897
1442
|
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
898
1443
|
this.unavailable = {
|
|
899
1444
|
acceptedAncestorDigest: copyBytes(evaluation.digest),
|
|
900
|
-
|
|
1445
|
+
comparisonCandidate: copySnapshot(pending.snapshot),
|
|
901
1446
|
reason: boundedUnavailableReason(evaluation.reason),
|
|
902
1447
|
};
|
|
903
1448
|
return this.result("unavailable", this.unavailable.reason);
|
|
@@ -909,12 +1454,11 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
909
1454
|
if (evaluation.status === "missing") {
|
|
910
1455
|
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
911
1456
|
status = "pending";
|
|
912
|
-
reason = "Policy parent is missing";
|
|
1457
|
+
reason = evaluation.reason ?? "Policy parent is missing";
|
|
913
1458
|
} else {
|
|
914
1459
|
this.pending.delete(pending.snapshot.digestKey);
|
|
915
1460
|
if (evaluation.status === "fork") {
|
|
916
|
-
this.setFork(evaluation);
|
|
917
|
-
return this.forkedResult();
|
|
1461
|
+
return this.forkedResult(this.setFork(evaluation));
|
|
918
1462
|
}
|
|
919
1463
|
if (evaluation.status === "accept") {
|
|
920
1464
|
this.project(pending.snapshot);
|