@peerbit/trusted-network 6.0.104 → 6.0.105
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-engine.d.ts +16 -5
- package/dist/src/v2-policy-engine.d.ts.map +1 -1
- package/dist/src/v2-policy-engine.js +212 -45
- 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-engine.ts +277 -48
- 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";
|
|
@@ -134,6 +142,7 @@ type UnavailablePolicyComparisonV2 = {
|
|
|
134
142
|
type ParentResolutionV2 =
|
|
135
143
|
| { status: "found"; parent: ValidatedPolicySnapshotV2 }
|
|
136
144
|
| { status: "missing"; digest: Uint8Array }
|
|
145
|
+
| { status: "unavailable"; digest: Uint8Array; reason: string }
|
|
137
146
|
| { status: "reject"; digest: Uint8Array; reason: string };
|
|
138
147
|
|
|
139
148
|
type SnapshotResolutionCacheV2 = Map<
|
|
@@ -144,7 +153,7 @@ type SnapshotResolutionCacheV2 = Map<
|
|
|
144
153
|
type EvaluationV2 =
|
|
145
154
|
| { status: "accept" }
|
|
146
155
|
| { status: "duplicate" }
|
|
147
|
-
| { status: "missing"; digest: Uint8Array }
|
|
156
|
+
| { status: "missing"; digest: Uint8Array; reason?: string }
|
|
148
157
|
| { status: "reject"; reason: string }
|
|
149
158
|
| { status: "unavailable"; digest: Uint8Array; reason: string }
|
|
150
159
|
| {
|
|
@@ -157,6 +166,7 @@ type EvaluationV2 =
|
|
|
157
166
|
type PendingDrainOutcomeV2 =
|
|
158
167
|
| { status: "accepted" }
|
|
159
168
|
| { status: "forked" }
|
|
169
|
+
| { status: "halted" }
|
|
160
170
|
| {
|
|
161
171
|
status: "unavailable";
|
|
162
172
|
retained: boolean;
|
|
@@ -169,34 +179,61 @@ const validationMessage = (error: unknown): string =>
|
|
|
169
179
|
const boundedUnavailableReason = (reason: string): string =>
|
|
170
180
|
reason.slice(0, MAX_UNAVAILABLE_REASON_LENGTH_V2);
|
|
171
181
|
|
|
172
|
-
|
|
173
|
-
|
|
182
|
+
class PolicyDependencyUnavailableErrorV2 extends Error {
|
|
183
|
+
constructor(message: string) {
|
|
184
|
+
super(message);
|
|
185
|
+
this.name = "PolicyDependencyUnavailableErrorV2";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const capturePolicySnapshotEntryBytesV2 = (
|
|
190
|
+
entryBytes: Uint8Array,
|
|
191
|
+
): Uint8Array => {
|
|
192
|
+
if (!(entryBytes instanceof Uint8Array)) {
|
|
193
|
+
throw new Error("Policy snapshot must use canonical EntryV0 bytes");
|
|
194
|
+
}
|
|
195
|
+
if (entryBytes.byteLength === 0) {
|
|
196
|
+
throw new Error("Policy snapshot must use EntryV0");
|
|
197
|
+
}
|
|
198
|
+
if (entryBytes.byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Policy snapshot entry must contain 1-${TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES} bytes`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Apply the protocol byte ceiling before this first copy, decode, or crypto
|
|
205
|
+
// operation. The retained copy also prevents caller mutation during awaits.
|
|
206
|
+
return copyBytes(entryBytes);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const authenticateCapturedPolicySnapshotEntryV2 = async (
|
|
210
|
+
canonicalEntryBytes: Uint8Array,
|
|
174
211
|
descriptor: NetworkDescriptorV2,
|
|
175
212
|
): Promise<ValidatedPolicySnapshotV2> => {
|
|
176
|
-
|
|
177
|
-
if (!(
|
|
213
|
+
const authenticatedEntry = deserialize(canonicalEntryBytes, Entry);
|
|
214
|
+
if (!(authenticatedEntry instanceof EntryV0)) {
|
|
178
215
|
throw new Error("Policy snapshot must use EntryV0");
|
|
179
216
|
}
|
|
180
|
-
if (!(
|
|
217
|
+
if (!equals(canonicalEntryBytes, serialize(authenticatedEntry))) {
|
|
218
|
+
throw new Error("Policy snapshot entry encoding is not canonical");
|
|
219
|
+
}
|
|
220
|
+
if (!(authenticatedEntry._meta instanceof DecryptedThing)) {
|
|
181
221
|
throw new Error("Policy snapshot metadata must be public");
|
|
182
222
|
}
|
|
183
|
-
if (!(
|
|
223
|
+
if (!(authenticatedEntry._payload instanceof DecryptedThing)) {
|
|
184
224
|
throw new Error("Policy snapshot payload must be public");
|
|
185
225
|
}
|
|
186
226
|
if (
|
|
187
|
-
|
|
188
|
-
|
|
227
|
+
authenticatedEntry._signatures === undefined ||
|
|
228
|
+
authenticatedEntry._signatures.signatures.length !== 1
|
|
189
229
|
) {
|
|
190
230
|
throw new Error("Policy snapshot must contain exactly one signature");
|
|
191
231
|
}
|
|
192
|
-
if (
|
|
232
|
+
if (
|
|
233
|
+
!(authenticatedEntry._signatures.signatures[0] instanceof DecryptedThing)
|
|
234
|
+
) {
|
|
193
235
|
throw new Error("Policy snapshot signature must be public");
|
|
194
236
|
}
|
|
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
237
|
authenticatedEntry.init({ encoding: NO_ENCODING });
|
|
201
238
|
|
|
202
239
|
const signatures = await authenticatedEntry.getSignatures();
|
|
@@ -228,14 +265,25 @@ export const authenticatePolicySnapshotEntryV2 = async (
|
|
|
228
265
|
body: copyBody(body),
|
|
229
266
|
digest: copyBytes(digest),
|
|
230
267
|
digestKey: bytesKey(digest),
|
|
231
|
-
entryBytes,
|
|
268
|
+
entryBytes: canonicalEntryBytes,
|
|
232
269
|
accountedBytes:
|
|
233
|
-
|
|
270
|
+
canonicalEntryBytes.byteLength +
|
|
234
271
|
serialize(body).byteLength +
|
|
235
272
|
PENDING_POLICY_ACCOUNTING_OVERHEAD_V2,
|
|
236
273
|
};
|
|
237
274
|
};
|
|
238
275
|
|
|
276
|
+
export const authenticatePolicySnapshotEntryV2 = async (
|
|
277
|
+
entryBytes: Uint8Array,
|
|
278
|
+
descriptor: NetworkDescriptorV2,
|
|
279
|
+
): Promise<ValidatedPolicySnapshotV2> => {
|
|
280
|
+
assertNetworkDescriptorV2(descriptor);
|
|
281
|
+
return authenticateCapturedPolicySnapshotEntryV2(
|
|
282
|
+
capturePolicySnapshotEntryBytesV2(entryBytes),
|
|
283
|
+
descriptor,
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
|
|
239
287
|
const projectionFromSnapshot = (
|
|
240
288
|
snapshot: ValidatedPolicySnapshotV2,
|
|
241
289
|
): PolicyHeadProjectionV2 => ({
|
|
@@ -291,8 +339,12 @@ const copyForkEvidence = (
|
|
|
291
339
|
export class TrustedNetworkV2PolicyReducer {
|
|
292
340
|
private readonly descriptor: NetworkDescriptorV2;
|
|
293
341
|
private readonly resolvePolicyEntry: PolicySnapshotResolverV2;
|
|
342
|
+
private readonly resolveTimeoutMs: number;
|
|
294
343
|
private readonly maxPending: number;
|
|
295
344
|
private readonly maxPendingPolicyBytes: number;
|
|
345
|
+
private readonly lifecycleController = new AbortController();
|
|
346
|
+
private externalSignal?: AbortSignal;
|
|
347
|
+
private externalAbortListener?: () => void;
|
|
296
348
|
private acceptedHead?: ValidatedPolicySnapshotV2;
|
|
297
349
|
private projectedRoles = new Map<string, number>();
|
|
298
350
|
private readonly pending = new Map<string, PendingPolicySnapshotV2>();
|
|
@@ -305,6 +357,8 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
305
357
|
constructor(properties: {
|
|
306
358
|
descriptor: NetworkDescriptorV2;
|
|
307
359
|
resolvePolicyEntry: PolicySnapshotResolverV2;
|
|
360
|
+
resolveTimeoutMs?: number;
|
|
361
|
+
signal?: AbortSignal;
|
|
308
362
|
maxPending?: number;
|
|
309
363
|
maxPendingPolicyBytes?: number;
|
|
310
364
|
}) {
|
|
@@ -317,20 +371,52 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
317
371
|
properties.maxPendingPolicyBytes ?? DEFAULT_MAX_PENDING_POLICY_BYTES_V2;
|
|
318
372
|
if (
|
|
319
373
|
!Number.isSafeInteger(maxPendingPolicyBytes) ||
|
|
320
|
-
maxPendingPolicyBytes < 1
|
|
374
|
+
maxPendingPolicyBytes < 1 ||
|
|
375
|
+
maxPendingPolicyBytes > MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2
|
|
321
376
|
) {
|
|
322
|
-
throw new Error(
|
|
377
|
+
throw new Error(
|
|
378
|
+
`maxPendingPolicyBytes must be a positive safe integer no greater than ${MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2}`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const resolveTimeoutMs =
|
|
382
|
+
properties.resolveTimeoutMs ?? DEFAULT_POLICY_RESOLUTION_TIMEOUT_MS_V2;
|
|
383
|
+
if (
|
|
384
|
+
!Number.isSafeInteger(resolveTimeoutMs) ||
|
|
385
|
+
resolveTimeoutMs < 1 ||
|
|
386
|
+
resolveTimeoutMs > MAX_TIMER_DELAY_MS_V2
|
|
387
|
+
) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`resolveTimeoutMs must be a positive safe integer no greater than ${MAX_TIMER_DELAY_MS_V2}`,
|
|
390
|
+
);
|
|
323
391
|
}
|
|
324
392
|
this.descriptor = deserialize(
|
|
325
393
|
serialize(properties.descriptor),
|
|
326
394
|
NetworkDescriptorV2,
|
|
327
395
|
);
|
|
328
396
|
this.resolvePolicyEntry = properties.resolvePolicyEntry;
|
|
397
|
+
this.resolveTimeoutMs = resolveTimeoutMs;
|
|
329
398
|
this.maxPending = maxPending;
|
|
330
399
|
this.maxPendingPolicyBytes = maxPendingPolicyBytes;
|
|
400
|
+
|
|
401
|
+
if (properties.signal?.aborted) {
|
|
402
|
+
this.lifecycleController.abort();
|
|
403
|
+
} else if (properties.signal !== undefined) {
|
|
404
|
+
this.externalSignal = properties.signal;
|
|
405
|
+
this.externalAbortListener = (): void => {
|
|
406
|
+
this.externalSignal = undefined;
|
|
407
|
+
this.externalAbortListener = undefined;
|
|
408
|
+
this.lifecycleController.abort();
|
|
409
|
+
};
|
|
410
|
+
this.externalSignal.addEventListener(
|
|
411
|
+
"abort",
|
|
412
|
+
this.externalAbortListener,
|
|
413
|
+
{ once: true },
|
|
414
|
+
);
|
|
415
|
+
}
|
|
331
416
|
}
|
|
332
417
|
|
|
333
|
-
get state(): "EMPTY" | "ACTIVE" | "UNAVAILABLE" | "FORKED" {
|
|
418
|
+
get state(): "EMPTY" | "ACTIVE" | "UNAVAILABLE" | "FORKED" | "HALTED" {
|
|
419
|
+
if (this.lifecycleController.signal.aborted) return "HALTED";
|
|
334
420
|
if (this.fork !== undefined) return "FORKED";
|
|
335
421
|
if (this.unavailable !== undefined) return "UNAVAILABLE";
|
|
336
422
|
return this.acceptedHead === undefined ? "EMPTY" : "ACTIVE";
|
|
@@ -380,6 +466,21 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
380
466
|
return (this.rolesFor(subject) & roles) === roles;
|
|
381
467
|
}
|
|
382
468
|
|
|
469
|
+
abort(): void {
|
|
470
|
+
if (
|
|
471
|
+
this.externalSignal !== undefined &&
|
|
472
|
+
this.externalAbortListener !== undefined
|
|
473
|
+
) {
|
|
474
|
+
this.externalSignal.removeEventListener(
|
|
475
|
+
"abort",
|
|
476
|
+
this.externalAbortListener,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
this.externalSignal = undefined;
|
|
480
|
+
this.externalAbortListener = undefined;
|
|
481
|
+
this.lifecycleController.abort();
|
|
482
|
+
}
|
|
483
|
+
|
|
383
484
|
private fetchHints(): PolicyParentFetchHintV2[] {
|
|
384
485
|
const unique = new Map<string, Uint8Array>();
|
|
385
486
|
for (const { missingParentDigest } of this.pending.values()) {
|
|
@@ -422,6 +523,10 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
422
523
|
return this.result("forked", "Policy authority signed competing children");
|
|
423
524
|
}
|
|
424
525
|
|
|
526
|
+
private haltedResult(): PolicyAdmissionResultV2 {
|
|
527
|
+
return this.result("halted", "Policy reducer lifecycle is aborted");
|
|
528
|
+
}
|
|
529
|
+
|
|
425
530
|
private unavailableResult(retention?: {
|
|
426
531
|
retained: boolean;
|
|
427
532
|
evictedPolicyDigests: Uint8Array[];
|
|
@@ -447,6 +552,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
447
552
|
outcome: PendingDrainOutcomeV2 | undefined,
|
|
448
553
|
): PolicyAdmissionResultV2 | undefined {
|
|
449
554
|
if (outcome?.status === "forked") return this.forkedResult();
|
|
555
|
+
if (outcome?.status === "halted") return this.haltedResult();
|
|
450
556
|
return outcome?.status === "unavailable"
|
|
451
557
|
? this.unavailableResult(outcome)
|
|
452
558
|
: undefined;
|
|
@@ -471,6 +577,88 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
471
577
|
}
|
|
472
578
|
}
|
|
473
579
|
|
|
580
|
+
private async resolveExternalSnapshot(
|
|
581
|
+
digest: Uint8Array,
|
|
582
|
+
): Promise<ValidatedPolicySnapshotV2 | undefined> {
|
|
583
|
+
if (this.lifecycleController.signal.aborted) {
|
|
584
|
+
throw new PolicyDependencyUnavailableErrorV2(
|
|
585
|
+
"Policy resolver lifecycle is aborted",
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const attemptController = new AbortController();
|
|
590
|
+
let timedOut = false;
|
|
591
|
+
const abortFromLifecycle = (): void => attemptController.abort();
|
|
592
|
+
this.lifecycleController.signal.addEventListener(
|
|
593
|
+
"abort",
|
|
594
|
+
abortFromLifecycle,
|
|
595
|
+
{ once: true },
|
|
596
|
+
);
|
|
597
|
+
const timeout = setTimeout(() => {
|
|
598
|
+
timedOut = true;
|
|
599
|
+
attemptController.abort();
|
|
600
|
+
}, this.resolveTimeoutMs);
|
|
601
|
+
const abortError = (): PolicyDependencyUnavailableErrorV2 =>
|
|
602
|
+
new PolicyDependencyUnavailableErrorV2(
|
|
603
|
+
timedOut
|
|
604
|
+
? `Policy resolver timed out after ${this.resolveTimeoutMs} ms`
|
|
605
|
+
: "Policy resolver attempt was aborted",
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
let rejectOnAbort: (() => void) | undefined;
|
|
609
|
+
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
610
|
+
rejectOnAbort = (): void => {
|
|
611
|
+
reject(abortError());
|
|
612
|
+
};
|
|
613
|
+
attemptController.signal.addEventListener("abort", rejectOnAbort, {
|
|
614
|
+
once: true,
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const resolution = Promise.resolve().then(async () => {
|
|
619
|
+
if (attemptController.signal.aborted) throw abortError();
|
|
620
|
+
const entryBytes = await this.resolvePolicyEntry(copyBytes(digest), {
|
|
621
|
+
signal: attemptController.signal,
|
|
622
|
+
});
|
|
623
|
+
// A resolver may ignore cancellation. Do not spend decode or signature
|
|
624
|
+
// verification work on bytes that arrive after this attempt expired.
|
|
625
|
+
if (attemptController.signal.aborted) throw abortError();
|
|
626
|
+
if (entryBytes === undefined) return undefined;
|
|
627
|
+
const snapshot = await authenticatePolicySnapshotEntryV2(
|
|
628
|
+
entryBytes,
|
|
629
|
+
this.descriptor,
|
|
630
|
+
);
|
|
631
|
+
if (!equals(snapshot.digest, digest)) {
|
|
632
|
+
throw new Error("Policy resolver returned the wrong body digest");
|
|
633
|
+
}
|
|
634
|
+
return snapshot;
|
|
635
|
+
});
|
|
636
|
+
// Promise.race installs handlers, but this explicit observer documents and
|
|
637
|
+
// preserves consumption if the resolver settles after its deadline.
|
|
638
|
+
void resolution.then(
|
|
639
|
+
(): void => undefined,
|
|
640
|
+
(): void => undefined,
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
return await Promise.race([resolution, abortPromise]);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (error instanceof PolicyDependencyUnavailableErrorV2) throw error;
|
|
647
|
+
throw new PolicyDependencyUnavailableErrorV2(
|
|
648
|
+
`Policy resolver dependency is unavailable: ${validationMessage(error)}`,
|
|
649
|
+
);
|
|
650
|
+
} finally {
|
|
651
|
+
clearTimeout(timeout);
|
|
652
|
+
this.lifecycleController.signal.removeEventListener(
|
|
653
|
+
"abort",
|
|
654
|
+
abortFromLifecycle,
|
|
655
|
+
);
|
|
656
|
+
if (rejectOnAbort !== undefined) {
|
|
657
|
+
attemptController.signal.removeEventListener("abort", rejectOnAbort);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
474
662
|
private async resolveSnapshot(
|
|
475
663
|
digest: Uint8Array,
|
|
476
664
|
cache?: SnapshotResolutionCacheV2,
|
|
@@ -483,18 +671,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
483
671
|
if (pending !== undefined) return copySnapshot(pending.snapshot);
|
|
484
672
|
let resolution = cache?.get(digestKey);
|
|
485
673
|
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
|
-
})();
|
|
674
|
+
resolution = this.resolveExternalSnapshot(digest);
|
|
498
675
|
cache?.set(digestKey, resolution);
|
|
499
676
|
}
|
|
500
677
|
const snapshot = await resolution;
|
|
@@ -520,9 +697,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
520
697
|
);
|
|
521
698
|
} catch (error) {
|
|
522
699
|
return {
|
|
523
|
-
status: "
|
|
700
|
+
status: "unavailable",
|
|
524
701
|
digest: copyBytes(child.body.previousPolicyDigest),
|
|
525
|
-
reason: `Policy parent
|
|
702
|
+
reason: `Policy parent dependency is unavailable: ${validationMessage(error)}`,
|
|
526
703
|
};
|
|
527
704
|
}
|
|
528
705
|
if (parent === undefined) {
|
|
@@ -541,6 +718,18 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
541
718
|
return { status: "found", parent };
|
|
542
719
|
}
|
|
543
720
|
|
|
721
|
+
private candidateAncestryResult(
|
|
722
|
+
resolution: Exclude<ParentResolutionV2, { status: "found" }>,
|
|
723
|
+
): EvaluationV2 {
|
|
724
|
+
return resolution.status === "unavailable"
|
|
725
|
+
? {
|
|
726
|
+
status: "missing",
|
|
727
|
+
digest: copyBytes(resolution.digest),
|
|
728
|
+
reason: resolution.reason,
|
|
729
|
+
}
|
|
730
|
+
: resolution;
|
|
731
|
+
}
|
|
732
|
+
|
|
544
733
|
private acceptedAncestryUnavailable(
|
|
545
734
|
resolution: Exclude<ParentResolutionV2, { status: "found" }>,
|
|
546
735
|
): Extract<EvaluationV2, { status: "unavailable" }> {
|
|
@@ -550,7 +739,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
550
739
|
reason:
|
|
551
740
|
resolution.status === "missing"
|
|
552
741
|
? "Accepted policy ancestry is unavailable from the resolver"
|
|
553
|
-
:
|
|
742
|
+
: resolution.status === "unavailable"
|
|
743
|
+
? `Accepted policy ancestry is unavailable: ${resolution.reason}`
|
|
744
|
+
: `Accepted policy ancestry validation failed: ${resolution.reason}`,
|
|
554
745
|
};
|
|
555
746
|
}
|
|
556
747
|
|
|
@@ -562,7 +753,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
562
753
|
let cursor = candidate;
|
|
563
754
|
while (cursor.body.sequence !== 0n) {
|
|
564
755
|
const parent = await this.parentOf(cursor, resolutionCache);
|
|
565
|
-
if (parent.status !== "found")
|
|
756
|
+
if (parent.status !== "found") {
|
|
757
|
+
return this.candidateAncestryResult(parent);
|
|
758
|
+
}
|
|
566
759
|
cursor = parent.parent;
|
|
567
760
|
}
|
|
568
761
|
return { status: "accept" };
|
|
@@ -576,7 +769,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
576
769
|
while (candidateCursor.body.sequence > acceptedCursor.body.sequence) {
|
|
577
770
|
candidateChild = candidateCursor;
|
|
578
771
|
const parent = await this.parentOf(candidateCursor, resolutionCache);
|
|
579
|
-
if (parent.status !== "found")
|
|
772
|
+
if (parent.status !== "found") {
|
|
773
|
+
return this.candidateAncestryResult(parent);
|
|
774
|
+
}
|
|
580
775
|
candidateCursor = parent.parent;
|
|
581
776
|
}
|
|
582
777
|
while (acceptedCursor.body.sequence > candidateCursor.body.sequence) {
|
|
@@ -607,7 +802,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
607
802
|
if (acceptedParent.status !== "found") {
|
|
608
803
|
return this.acceptedAncestryUnavailable(acceptedParent);
|
|
609
804
|
}
|
|
610
|
-
if (candidateParent.status !== "found")
|
|
805
|
+
if (candidateParent.status !== "found") {
|
|
806
|
+
return this.candidateAncestryResult(candidateParent);
|
|
807
|
+
}
|
|
611
808
|
candidateCursor = candidateParent.parent;
|
|
612
809
|
acceptedCursor = acceptedParent.parent;
|
|
613
810
|
}
|
|
@@ -742,6 +939,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
742
939
|
}
|
|
743
940
|
|
|
744
941
|
private async drainPending(): Promise<PendingDrainOutcomeV2 | undefined> {
|
|
942
|
+
if (this.lifecycleController.signal.aborted) {
|
|
943
|
+
return { status: "halted" };
|
|
944
|
+
}
|
|
745
945
|
let accepted = false;
|
|
746
946
|
let progress = true;
|
|
747
947
|
while (
|
|
@@ -757,6 +957,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
757
957
|
for (const pending of ordered) {
|
|
758
958
|
if (!this.pending.has(pending.snapshot.digestKey)) continue;
|
|
759
959
|
const evaluation = await this.evaluate(pending.snapshot);
|
|
960
|
+
if (this.lifecycleController.signal.aborted) {
|
|
961
|
+
return { status: "halted" };
|
|
962
|
+
}
|
|
760
963
|
if (evaluation.status === "missing") {
|
|
761
964
|
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
762
965
|
continue;
|
|
@@ -782,7 +985,15 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
782
985
|
private enqueueAdmission(
|
|
783
986
|
operation: () => Promise<PolicyAdmissionResultV2>,
|
|
784
987
|
): Promise<PolicyAdmissionResultV2> {
|
|
785
|
-
const result = this.admissionTail.then(
|
|
988
|
+
const result = this.admissionTail.then(async () => {
|
|
989
|
+
if (this.lifecycleController.signal.aborted) {
|
|
990
|
+
return this.haltedResult();
|
|
991
|
+
}
|
|
992
|
+
const admission = await operation();
|
|
993
|
+
return this.lifecycleController.signal.aborted
|
|
994
|
+
? this.haltedResult()
|
|
995
|
+
: admission;
|
|
996
|
+
});
|
|
786
997
|
this.admissionTail = result.then(
|
|
787
998
|
(): void => {},
|
|
788
999
|
(_reason: unknown): void => {},
|
|
@@ -790,24 +1001,39 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
790
1001
|
return result;
|
|
791
1002
|
}
|
|
792
1003
|
|
|
793
|
-
ingest(
|
|
794
|
-
|
|
1004
|
+
ingest(entryBytes: Uint8Array): Promise<PolicyAdmissionResultV2> {
|
|
1005
|
+
if (this.lifecycleController.signal.aborted) {
|
|
1006
|
+
return Promise.resolve(this.haltedResult());
|
|
1007
|
+
}
|
|
1008
|
+
let capturedEntryBytes: Uint8Array;
|
|
1009
|
+
try {
|
|
1010
|
+
// Capture at the API boundary, before this admission waits behind earlier
|
|
1011
|
+
// work. Otherwise a caller could mutate a queued entry before validation.
|
|
1012
|
+
capturedEntryBytes = capturePolicySnapshotEntryBytesV2(entryBytes);
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
const reason = validationMessage(error);
|
|
1015
|
+
return this.enqueueAdmission(async () => this.result("rejected", reason));
|
|
1016
|
+
}
|
|
1017
|
+
return this.enqueueAdmission(() => this.ingestOne(capturedEntryBytes));
|
|
795
1018
|
}
|
|
796
1019
|
|
|
797
1020
|
retryUnavailable(): Promise<PolicyAdmissionResultV2> {
|
|
798
1021
|
return this.enqueueAdmission(() => this.retryUnavailableOne());
|
|
799
1022
|
}
|
|
800
1023
|
|
|
801
|
-
private async ingestOne(
|
|
1024
|
+
private async ingestOne(
|
|
1025
|
+
entryBytes: Uint8Array,
|
|
1026
|
+
): Promise<PolicyAdmissionResultV2> {
|
|
802
1027
|
let snapshot: ValidatedPolicySnapshotV2;
|
|
803
1028
|
try {
|
|
804
|
-
snapshot = await
|
|
805
|
-
|
|
1029
|
+
snapshot = await authenticateCapturedPolicySnapshotEntryV2(
|
|
1030
|
+
entryBytes,
|
|
806
1031
|
this.descriptor,
|
|
807
1032
|
);
|
|
808
1033
|
} catch (error) {
|
|
809
1034
|
return this.result("rejected", validationMessage(error));
|
|
810
1035
|
}
|
|
1036
|
+
if (this.lifecycleController.signal.aborted) return this.haltedResult();
|
|
811
1037
|
|
|
812
1038
|
if (this.fork !== undefined) {
|
|
813
1039
|
this.observeAfterFork(snapshot);
|
|
@@ -841,6 +1067,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
841
1067
|
}
|
|
842
1068
|
|
|
843
1069
|
const evaluation = await this.evaluate(snapshot);
|
|
1070
|
+
if (this.lifecycleController.signal.aborted) return this.haltedResult();
|
|
844
1071
|
if (evaluation.status === "reject") {
|
|
845
1072
|
return this.result("rejected", evaluation.reason);
|
|
846
1073
|
}
|
|
@@ -860,7 +1087,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
860
1087
|
return this.result(
|
|
861
1088
|
pending.retained ? "pending" : "capacity",
|
|
862
1089
|
pending.retained
|
|
863
|
-
? "Policy parent is missing"
|
|
1090
|
+
? (evaluation.reason ?? "Policy parent is missing")
|
|
864
1091
|
: "Policy pending capacity did not retain this candidate",
|
|
865
1092
|
pending.evictedPolicyDigests,
|
|
866
1093
|
);
|
|
@@ -874,6 +1101,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
874
1101
|
}
|
|
875
1102
|
|
|
876
1103
|
private async retryUnavailableOne(): Promise<PolicyAdmissionResultV2> {
|
|
1104
|
+
if (this.lifecycleController.signal.aborted) return this.haltedResult();
|
|
877
1105
|
if (this.fork !== undefined) {
|
|
878
1106
|
return this.result(
|
|
879
1107
|
"halted",
|
|
@@ -893,6 +1121,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
893
1121
|
}
|
|
894
1122
|
|
|
895
1123
|
const evaluation = await this.evaluate(pending.snapshot);
|
|
1124
|
+
if (this.lifecycleController.signal.aborted) return this.haltedResult();
|
|
896
1125
|
if (evaluation.status === "unavailable") {
|
|
897
1126
|
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
898
1127
|
this.unavailable = {
|
|
@@ -909,7 +1138,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
909
1138
|
if (evaluation.status === "missing") {
|
|
910
1139
|
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
911
1140
|
status = "pending";
|
|
912
|
-
reason = "Policy parent is missing";
|
|
1141
|
+
reason = evaluation.reason ?? "Policy parent is missing";
|
|
913
1142
|
} else {
|
|
914
1143
|
this.pending.delete(pending.snapshot.digestKey);
|
|
915
1144
|
if (evaluation.status === "fork") {
|
package/src/v2.ts
CHANGED
|
@@ -33,6 +33,13 @@ export const TRUSTED_NETWORK_V2_POLICY_HASH_SHA256 = 1;
|
|
|
33
33
|
*/
|
|
34
34
|
export const TRUSTED_NETWORK_V2_ENTRY_V0_AUTHORITY_ONLY_SIGNATURE_PROFILE = 1;
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Consensus ceiling for the canonical serialized EntryV0 carrying a v2 policy
|
|
38
|
+
* snapshot. Every replica applies this exact bound before decoding the entry;
|
|
39
|
+
* it is part of signature profile 1 rather than a replica-local resource knob.
|
|
40
|
+
*/
|
|
41
|
+
export const TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES = 128 * 1024;
|
|
42
|
+
|
|
36
43
|
export const TRUSTED_NETWORK_V2_NETWORK_ID_DOMAIN =
|
|
37
44
|
"peerbit/trusted-network/v2/network-id/v1";
|
|
38
45
|
export const TRUSTED_NETWORK_V2_POLICY_DIGEST_DOMAIN =
|