@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
|
@@ -2,7 +2,7 @@ import { deserialize, serialize } from "@dao-xyz/borsh";
|
|
|
2
2
|
import { DecryptedThing, PublicSignKey, verify } from "@peerbit/crypto";
|
|
3
3
|
import { Entry, EntryV0, NO_ENCODING } from "@peerbit/log";
|
|
4
4
|
import { compare, equals } from "uint8arrays";
|
|
5
|
-
import { NetworkDescriptorV2, PolicySnapshotBodyV2, PolicySubjectBindingV2, TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS, assertNetworkDescriptorV2, decodePolicySnapshotBodyV2, digestPolicySnapshotBodyV2, } from "./v2.js";
|
|
5
|
+
import { NetworkDescriptorV2, PolicySnapshotBodyV2, PolicySubjectBindingV2, TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS, TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES, assertNetworkDescriptorV2, decodePolicySnapshotBodyV2, digestPolicySnapshotBodyV2, } from "./v2.js";
|
|
6
6
|
/**
|
|
7
7
|
* Internal policy reducer for the non-activatable TrustedNetwork v2 scaffold.
|
|
8
8
|
*
|
|
@@ -12,8 +12,12 @@ import { NetworkDescriptorV2, PolicySnapshotBodyV2, PolicySubjectBindingV2, TRUS
|
|
|
12
12
|
* resolver instead of being accumulated in memory.
|
|
13
13
|
*/
|
|
14
14
|
const DEFAULT_MAX_PENDING_POLICIES_V2 = 64;
|
|
15
|
-
const DEFAULT_MAX_PENDING_POLICY_BYTES_V2 = 256 * 1024;
|
|
16
15
|
const PENDING_POLICY_ACCOUNTING_OVERHEAD_V2 = 64;
|
|
16
|
+
const MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2 = TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 2 +
|
|
17
|
+
PENDING_POLICY_ACCOUNTING_OVERHEAD_V2;
|
|
18
|
+
const DEFAULT_MAX_PENDING_POLICY_BYTES_V2 = MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2;
|
|
19
|
+
const DEFAULT_POLICY_RESOLUTION_TIMEOUT_MS_V2 = 10 * 1000;
|
|
20
|
+
const MAX_TIMER_DELAY_MS_V2 = 0x7fffffff;
|
|
17
21
|
const MAX_UNAVAILABLE_REASON_LENGTH_V2 = 512;
|
|
18
22
|
const copyBytes = (bytes) => Uint8Array.from(bytes);
|
|
19
23
|
const bytesKey = (bytes) => {
|
|
@@ -44,29 +48,47 @@ const copySnapshot = (snapshot) => ({
|
|
|
44
48
|
});
|
|
45
49
|
const validationMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
46
50
|
const boundedUnavailableReason = (reason) => reason.slice(0, MAX_UNAVAILABLE_REASON_LENGTH_V2);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
class PolicyDependencyUnavailableErrorV2 extends Error {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "PolicyDependencyUnavailableErrorV2";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const capturePolicySnapshotEntryBytesV2 = (entryBytes) => {
|
|
58
|
+
if (!(entryBytes instanceof Uint8Array)) {
|
|
59
|
+
throw new Error("Policy snapshot must use canonical EntryV0 bytes");
|
|
60
|
+
}
|
|
61
|
+
if (entryBytes.byteLength === 0) {
|
|
62
|
+
throw new Error("Policy snapshot must use EntryV0");
|
|
63
|
+
}
|
|
64
|
+
if (entryBytes.byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES) {
|
|
65
|
+
throw new Error(`Policy snapshot entry must contain 1-${TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES} bytes`);
|
|
66
|
+
}
|
|
67
|
+
// Apply the protocol byte ceiling before this first copy, decode, or crypto
|
|
68
|
+
// operation. The retained copy also prevents caller mutation during awaits.
|
|
69
|
+
return copyBytes(entryBytes);
|
|
70
|
+
};
|
|
71
|
+
const authenticateCapturedPolicySnapshotEntryV2 = async (canonicalEntryBytes, descriptor) => {
|
|
72
|
+
const authenticatedEntry = deserialize(canonicalEntryBytes, Entry);
|
|
73
|
+
if (!(authenticatedEntry instanceof EntryV0)) {
|
|
50
74
|
throw new Error("Policy snapshot must use EntryV0");
|
|
51
75
|
}
|
|
52
|
-
if (!(
|
|
76
|
+
if (!equals(canonicalEntryBytes, serialize(authenticatedEntry))) {
|
|
77
|
+
throw new Error("Policy snapshot entry encoding is not canonical");
|
|
78
|
+
}
|
|
79
|
+
if (!(authenticatedEntry._meta instanceof DecryptedThing)) {
|
|
53
80
|
throw new Error("Policy snapshot metadata must be public");
|
|
54
81
|
}
|
|
55
|
-
if (!(
|
|
82
|
+
if (!(authenticatedEntry._payload instanceof DecryptedThing)) {
|
|
56
83
|
throw new Error("Policy snapshot payload must be public");
|
|
57
84
|
}
|
|
58
|
-
if (
|
|
59
|
-
|
|
85
|
+
if (authenticatedEntry._signatures === undefined ||
|
|
86
|
+
authenticatedEntry._signatures.signatures.length !== 1) {
|
|
60
87
|
throw new Error("Policy snapshot must contain exactly one signature");
|
|
61
88
|
}
|
|
62
|
-
if (!(
|
|
89
|
+
if (!(authenticatedEntry._signatures.signatures[0] instanceof DecryptedThing)) {
|
|
63
90
|
throw new Error("Policy snapshot signature must be public");
|
|
64
91
|
}
|
|
65
|
-
const entryBytes = serialize(entry);
|
|
66
|
-
const authenticatedEntry = deserialize(entryBytes, Entry);
|
|
67
|
-
if (!(authenticatedEntry instanceof EntryV0)) {
|
|
68
|
-
throw new Error("Policy snapshot must decode as EntryV0");
|
|
69
|
-
}
|
|
70
92
|
authenticatedEntry.init({ encoding: NO_ENCODING });
|
|
71
93
|
const signatures = await authenticatedEntry.getSignatures();
|
|
72
94
|
if (signatures.length !== 1) {
|
|
@@ -89,12 +111,16 @@ export const authenticatePolicySnapshotEntryV2 = async (entry, descriptor) => {
|
|
|
89
111
|
body: copyBody(body),
|
|
90
112
|
digest: copyBytes(digest),
|
|
91
113
|
digestKey: bytesKey(digest),
|
|
92
|
-
entryBytes,
|
|
93
|
-
accountedBytes:
|
|
114
|
+
entryBytes: canonicalEntryBytes,
|
|
115
|
+
accountedBytes: canonicalEntryBytes.byteLength +
|
|
94
116
|
serialize(body).byteLength +
|
|
95
117
|
PENDING_POLICY_ACCOUNTING_OVERHEAD_V2,
|
|
96
118
|
};
|
|
97
119
|
};
|
|
120
|
+
export const authenticatePolicySnapshotEntryV2 = async (entryBytes, descriptor) => {
|
|
121
|
+
assertNetworkDescriptorV2(descriptor);
|
|
122
|
+
return authenticateCapturedPolicySnapshotEntryV2(capturePolicySnapshotEntryBytesV2(entryBytes), descriptor);
|
|
123
|
+
};
|
|
98
124
|
const projectionFromSnapshot = (snapshot) => ({
|
|
99
125
|
sequence: snapshot.body.sequence,
|
|
100
126
|
digest: copyBytes(snapshot.digest),
|
|
@@ -128,11 +154,67 @@ const copyForkEvidence = (evidence) => ({
|
|
|
128
154
|
copyForkChildProof(evidence.children[1]),
|
|
129
155
|
],
|
|
130
156
|
});
|
|
157
|
+
const captureDurableStateV2 = (durableState) => {
|
|
158
|
+
if (durableState === null ||
|
|
159
|
+
typeof durableState !== "object" ||
|
|
160
|
+
durableState.formatVersion !== 1) {
|
|
161
|
+
throw new Error("Unsupported TrustedNetwork v2 reducer state format");
|
|
162
|
+
}
|
|
163
|
+
switch (durableState.state) {
|
|
164
|
+
case "EMPTY":
|
|
165
|
+
return { formatVersion: 1, state: "EMPTY" };
|
|
166
|
+
case "ACTIVE":
|
|
167
|
+
return {
|
|
168
|
+
formatVersion: 1,
|
|
169
|
+
state: "ACTIVE",
|
|
170
|
+
acceptedHeadEntryBytes: capturePolicySnapshotEntryBytesV2(durableState.acceptedHeadEntryBytes),
|
|
171
|
+
};
|
|
172
|
+
case "UNAVAILABLE": {
|
|
173
|
+
if (!(durableState.acceptedAncestorDigest instanceof Uint8Array) ||
|
|
174
|
+
durableState.acceptedAncestorDigest.byteLength !== 32) {
|
|
175
|
+
throw new Error("Unavailable accepted ancestor digest must contain exactly 32 bytes");
|
|
176
|
+
}
|
|
177
|
+
if (typeof durableState.reason !== "string" ||
|
|
178
|
+
durableState.reason.length === 0 ||
|
|
179
|
+
durableState.reason.length > MAX_UNAVAILABLE_REASON_LENGTH_V2) {
|
|
180
|
+
throw new Error(`Unavailable reason must contain 1-${MAX_UNAVAILABLE_REASON_LENGTH_V2} characters`);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
formatVersion: 1,
|
|
184
|
+
state: "UNAVAILABLE",
|
|
185
|
+
acceptedHeadEntryBytes: capturePolicySnapshotEntryBytesV2(durableState.acceptedHeadEntryBytes),
|
|
186
|
+
comparisonCandidateEntryBytes: capturePolicySnapshotEntryBytesV2(durableState.comparisonCandidateEntryBytes),
|
|
187
|
+
acceptedAncestorDigest: copyBytes(durableState.acceptedAncestorDigest),
|
|
188
|
+
reason: durableState.reason,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
case "FORKED":
|
|
192
|
+
if (!Array.isArray(durableState.childEntryBytes) ||
|
|
193
|
+
durableState.childEntryBytes.length !== 2) {
|
|
194
|
+
throw new Error("Forked reducer state must contain exactly two children");
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
formatVersion: 1,
|
|
198
|
+
state: "FORKED",
|
|
199
|
+
commonParentEntryBytes: capturePolicySnapshotEntryBytesV2(durableState.commonParentEntryBytes),
|
|
200
|
+
childEntryBytes: [
|
|
201
|
+
capturePolicySnapshotEntryBytesV2(durableState.childEntryBytes[0]),
|
|
202
|
+
capturePolicySnapshotEntryBytesV2(durableState.childEntryBytes[1]),
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
default:
|
|
206
|
+
throw new Error("Unsupported TrustedNetwork v2 reducer state");
|
|
207
|
+
}
|
|
208
|
+
};
|
|
131
209
|
export class TrustedNetworkV2PolicyReducer {
|
|
132
210
|
descriptor;
|
|
133
211
|
resolvePolicyEntry;
|
|
212
|
+
resolveTimeoutMs;
|
|
134
213
|
maxPending;
|
|
135
214
|
maxPendingPolicyBytes;
|
|
215
|
+
lifecycleController = new AbortController();
|
|
216
|
+
externalSignal;
|
|
217
|
+
externalAbortListener;
|
|
136
218
|
acceptedHead;
|
|
137
219
|
projectedRoles = new Map();
|
|
138
220
|
pending = new Map();
|
|
@@ -149,15 +231,107 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
149
231
|
}
|
|
150
232
|
const maxPendingPolicyBytes = properties.maxPendingPolicyBytes ?? DEFAULT_MAX_PENDING_POLICY_BYTES_V2;
|
|
151
233
|
if (!Number.isSafeInteger(maxPendingPolicyBytes) ||
|
|
152
|
-
maxPendingPolicyBytes < 1
|
|
153
|
-
|
|
234
|
+
maxPendingPolicyBytes < 1 ||
|
|
235
|
+
maxPendingPolicyBytes > MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2) {
|
|
236
|
+
throw new Error(`maxPendingPolicyBytes must be a positive safe integer no greater than ${MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2}`);
|
|
237
|
+
}
|
|
238
|
+
const resolveTimeoutMs = properties.resolveTimeoutMs ?? DEFAULT_POLICY_RESOLUTION_TIMEOUT_MS_V2;
|
|
239
|
+
if (!Number.isSafeInteger(resolveTimeoutMs) ||
|
|
240
|
+
resolveTimeoutMs < 1 ||
|
|
241
|
+
resolveTimeoutMs > MAX_TIMER_DELAY_MS_V2) {
|
|
242
|
+
throw new Error(`resolveTimeoutMs must be a positive safe integer no greater than ${MAX_TIMER_DELAY_MS_V2}`);
|
|
154
243
|
}
|
|
155
244
|
this.descriptor = deserialize(serialize(properties.descriptor), NetworkDescriptorV2);
|
|
156
245
|
this.resolvePolicyEntry = properties.resolvePolicyEntry;
|
|
246
|
+
this.resolveTimeoutMs = resolveTimeoutMs;
|
|
157
247
|
this.maxPending = maxPending;
|
|
158
248
|
this.maxPendingPolicyBytes = maxPendingPolicyBytes;
|
|
249
|
+
if (properties.signal?.aborted) {
|
|
250
|
+
this.lifecycleController.abort();
|
|
251
|
+
}
|
|
252
|
+
else if (properties.signal !== undefined) {
|
|
253
|
+
this.externalSignal = properties.signal;
|
|
254
|
+
this.externalAbortListener = () => {
|
|
255
|
+
this.externalSignal = undefined;
|
|
256
|
+
this.externalAbortListener = undefined;
|
|
257
|
+
this.lifecycleController.abort();
|
|
258
|
+
};
|
|
259
|
+
this.externalSignal.addEventListener("abort", this.externalAbortListener, { once: true });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
static async restore(properties) {
|
|
263
|
+
// Capture the complete checkpoint before the first await. Persistence
|
|
264
|
+
// adapters commonly reuse read buffers, and mutation during authentication
|
|
265
|
+
// must not change what is restored.
|
|
266
|
+
const durableState = captureDurableStateV2(properties.durableState);
|
|
267
|
+
const reducer = new TrustedNetworkV2PolicyReducer(properties);
|
|
268
|
+
const authenticate = (entryBytes) => authenticateCapturedPolicySnapshotEntryV2(entryBytes, reducer.descriptor);
|
|
269
|
+
try {
|
|
270
|
+
switch (durableState.state) {
|
|
271
|
+
case "EMPTY":
|
|
272
|
+
return reducer;
|
|
273
|
+
case "ACTIVE": {
|
|
274
|
+
const acceptedHead = await authenticate(durableState.acceptedHeadEntryBytes);
|
|
275
|
+
// A durable ACTIVE head is a trusted prior-validation checkpoint. Its
|
|
276
|
+
// authority signature and network binding are re-authenticated above,
|
|
277
|
+
// but restore deliberately does not require historical resolver data.
|
|
278
|
+
reducer.project(acceptedHead);
|
|
279
|
+
return reducer;
|
|
280
|
+
}
|
|
281
|
+
case "UNAVAILABLE": {
|
|
282
|
+
const [acceptedHead, comparisonCandidate] = await Promise.all([
|
|
283
|
+
authenticate(durableState.acceptedHeadEntryBytes),
|
|
284
|
+
authenticate(durableState.comparisonCandidateEntryBytes),
|
|
285
|
+
]);
|
|
286
|
+
if (equals(acceptedHead.digest, comparisonCandidate.digest)) {
|
|
287
|
+
throw new Error("Unavailable comparison candidate must differ from the accepted head");
|
|
288
|
+
}
|
|
289
|
+
reducer.project(acceptedHead);
|
|
290
|
+
reducer.addPending(comparisonCandidate, durableState.acceptedAncestorDigest);
|
|
291
|
+
reducer.unavailable = {
|
|
292
|
+
acceptedAncestorDigest: copyBytes(durableState.acceptedAncestorDigest),
|
|
293
|
+
comparisonCandidate: copySnapshot(comparisonCandidate),
|
|
294
|
+
reason: durableState.reason,
|
|
295
|
+
};
|
|
296
|
+
return reducer;
|
|
297
|
+
}
|
|
298
|
+
case "FORKED": {
|
|
299
|
+
const [commonParent, firstChild, secondChild] = await Promise.all([
|
|
300
|
+
authenticate(durableState.commonParentEntryBytes),
|
|
301
|
+
authenticate(durableState.childEntryBytes[0]),
|
|
302
|
+
authenticate(durableState.childEntryBytes[1]),
|
|
303
|
+
]);
|
|
304
|
+
for (const child of [firstChild, secondChild]) {
|
|
305
|
+
if (child.body.sequence !== commonParent.body.sequence + 1n ||
|
|
306
|
+
!equals(child.body.previousPolicyDigest, commonParent.digest)) {
|
|
307
|
+
throw new Error("Fork child must be a direct successor of the common parent");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (equals(firstChild.digest, secondChild.digest)) {
|
|
311
|
+
throw new Error("Fork children must have distinct policy digests");
|
|
312
|
+
}
|
|
313
|
+
const children = [
|
|
314
|
+
forkProofFromSnapshot(firstChild),
|
|
315
|
+
forkProofFromSnapshot(secondChild),
|
|
316
|
+
].sort(compareForkChildProofs);
|
|
317
|
+
reducer.project(commonParent);
|
|
318
|
+
reducer.fork = {
|
|
319
|
+
commonParent: projectionFromSnapshot(commonParent),
|
|
320
|
+
children,
|
|
321
|
+
};
|
|
322
|
+
return reducer;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
// Do not retain a caller-owned AbortSignal listener when restore rejects.
|
|
328
|
+
reducer.abort();
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
159
331
|
}
|
|
160
332
|
get state() {
|
|
333
|
+
if (this.lifecycleController.signal.aborted)
|
|
334
|
+
return "HALTED";
|
|
161
335
|
if (this.fork !== undefined)
|
|
162
336
|
return "FORKED";
|
|
163
337
|
if (this.unavailable !== undefined)
|
|
@@ -187,6 +361,45 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
187
361
|
.sort((a, b) => compareKeys(a.snapshot.digestKey, b.snapshot.digestKey))
|
|
188
362
|
.map(({ snapshot }) => copyBytes(snapshot.digest));
|
|
189
363
|
}
|
|
364
|
+
exportDurableState() {
|
|
365
|
+
// Lifecycle cancellation is process-local. Export the underlying protocol
|
|
366
|
+
// safety state so a replacement process cannot erase UNAVAILABLE/FORKED.
|
|
367
|
+
if (this.fork !== undefined) {
|
|
368
|
+
if (this.acceptedHead === undefined) {
|
|
369
|
+
throw new Error("Forked reducer is missing its common-parent entry");
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
formatVersion: 1,
|
|
373
|
+
state: "FORKED",
|
|
374
|
+
commonParentEntryBytes: copyBytes(this.acceptedHead.entryBytes),
|
|
375
|
+
childEntryBytes: [
|
|
376
|
+
copyBytes(this.fork.children[0].entryBytes),
|
|
377
|
+
copyBytes(this.fork.children[1].entryBytes),
|
|
378
|
+
],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (this.unavailable !== undefined) {
|
|
382
|
+
if (this.acceptedHead === undefined) {
|
|
383
|
+
throw new Error("Unavailable reducer is missing its accepted head");
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
formatVersion: 1,
|
|
387
|
+
state: "UNAVAILABLE",
|
|
388
|
+
acceptedHeadEntryBytes: copyBytes(this.acceptedHead.entryBytes),
|
|
389
|
+
comparisonCandidateEntryBytes: copyBytes(this.unavailable.comparisonCandidate.entryBytes),
|
|
390
|
+
acceptedAncestorDigest: copyBytes(this.unavailable.acceptedAncestorDigest),
|
|
391
|
+
reason: this.unavailable.reason,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (this.acceptedHead !== undefined) {
|
|
395
|
+
return {
|
|
396
|
+
formatVersion: 1,
|
|
397
|
+
state: "ACTIVE",
|
|
398
|
+
acceptedHeadEntryBytes: copyBytes(this.acceptedHead.entryBytes),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return { formatVersion: 1, state: "EMPTY" };
|
|
402
|
+
}
|
|
190
403
|
rolesFor(subject) {
|
|
191
404
|
return this.projectedRoles.get(publicKeyId(subject)) ?? 0;
|
|
192
405
|
}
|
|
@@ -199,6 +412,15 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
199
412
|
}
|
|
200
413
|
return (this.rolesFor(subject) & roles) === roles;
|
|
201
414
|
}
|
|
415
|
+
abort() {
|
|
416
|
+
if (this.externalSignal !== undefined &&
|
|
417
|
+
this.externalAbortListener !== undefined) {
|
|
418
|
+
this.externalSignal.removeEventListener("abort", this.externalAbortListener);
|
|
419
|
+
}
|
|
420
|
+
this.externalSignal = undefined;
|
|
421
|
+
this.externalAbortListener = undefined;
|
|
422
|
+
this.lifecycleController.abort();
|
|
423
|
+
}
|
|
202
424
|
fetchHints() {
|
|
203
425
|
const unique = new Map();
|
|
204
426
|
for (const { missingParentDigest } of this.pending.values()) {
|
|
@@ -214,11 +436,14 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
214
436
|
digest: copyBytes(digest),
|
|
215
437
|
}));
|
|
216
438
|
}
|
|
217
|
-
result(status, reason, evictedPolicyDigests) {
|
|
439
|
+
result(status, reason, evictedPolicyDigests, forkObservations) {
|
|
218
440
|
return {
|
|
219
441
|
status,
|
|
220
442
|
reason,
|
|
221
443
|
head: this.head,
|
|
444
|
+
forkObservations: forkObservations === undefined
|
|
445
|
+
? undefined
|
|
446
|
+
: forkObservations.map(copyForkChildProof),
|
|
222
447
|
fetchHints: this.fetchHints(),
|
|
223
448
|
pendingCount: this.pending.size,
|
|
224
449
|
pendingBytes: this.pendingBytes,
|
|
@@ -227,12 +452,15 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
227
452
|
: evictedPolicyDigests.map(copyBytes),
|
|
228
453
|
};
|
|
229
454
|
}
|
|
230
|
-
forkedResult() {
|
|
231
|
-
return this.result("forked", "Policy authority signed competing children");
|
|
455
|
+
forkedResult(forkObservations) {
|
|
456
|
+
return this.result("forked", "Policy authority signed competing children", undefined, forkObservations);
|
|
457
|
+
}
|
|
458
|
+
haltedResult() {
|
|
459
|
+
return this.result("halted", "Policy reducer lifecycle is aborted");
|
|
232
460
|
}
|
|
233
461
|
unavailableResult(retention) {
|
|
234
462
|
const blockedCandidateRetained = this.unavailable !== undefined &&
|
|
235
|
-
this.pending.has(this.unavailable.
|
|
463
|
+
this.pending.has(this.unavailable.comparisonCandidate.digestKey);
|
|
236
464
|
const recoverable = (retention?.retained ?? true) && blockedCandidateRetained;
|
|
237
465
|
const reason = recoverable
|
|
238
466
|
? this.unavailable.reason
|
|
@@ -242,8 +470,11 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
242
470
|
return this.result(recoverable ? "unavailable" : "capacity", reason, retention?.evictedPolicyDigests);
|
|
243
471
|
}
|
|
244
472
|
completedDrainResult(outcome) {
|
|
245
|
-
if (outcome?.status === "forked")
|
|
246
|
-
return this.forkedResult();
|
|
473
|
+
if (outcome?.status === "forked") {
|
|
474
|
+
return this.forkedResult(outcome.forkObservations);
|
|
475
|
+
}
|
|
476
|
+
if (outcome?.status === "halted")
|
|
477
|
+
return this.haltedResult();
|
|
247
478
|
return outcome?.status === "unavailable"
|
|
248
479
|
? this.unavailableResult(outcome)
|
|
249
480
|
: undefined;
|
|
@@ -261,6 +492,67 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
261
492
|
this.acceptedHead = copySnapshot(snapshot);
|
|
262
493
|
}
|
|
263
494
|
}
|
|
495
|
+
async resolveExternalSnapshot(digest) {
|
|
496
|
+
if (this.lifecycleController.signal.aborted) {
|
|
497
|
+
throw new PolicyDependencyUnavailableErrorV2("Policy resolver lifecycle is aborted");
|
|
498
|
+
}
|
|
499
|
+
const attemptController = new AbortController();
|
|
500
|
+
let timedOut = false;
|
|
501
|
+
const abortFromLifecycle = () => attemptController.abort();
|
|
502
|
+
this.lifecycleController.signal.addEventListener("abort", abortFromLifecycle, { once: true });
|
|
503
|
+
const timeout = setTimeout(() => {
|
|
504
|
+
timedOut = true;
|
|
505
|
+
attemptController.abort();
|
|
506
|
+
}, this.resolveTimeoutMs);
|
|
507
|
+
const abortError = () => new PolicyDependencyUnavailableErrorV2(timedOut
|
|
508
|
+
? `Policy resolver timed out after ${this.resolveTimeoutMs} ms`
|
|
509
|
+
: "Policy resolver attempt was aborted");
|
|
510
|
+
let rejectOnAbort;
|
|
511
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
512
|
+
rejectOnAbort = () => {
|
|
513
|
+
reject(abortError());
|
|
514
|
+
};
|
|
515
|
+
attemptController.signal.addEventListener("abort", rejectOnAbort, {
|
|
516
|
+
once: true,
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
const resolution = Promise.resolve().then(async () => {
|
|
520
|
+
if (attemptController.signal.aborted)
|
|
521
|
+
throw abortError();
|
|
522
|
+
const entryBytes = await this.resolvePolicyEntry(copyBytes(digest), {
|
|
523
|
+
signal: attemptController.signal,
|
|
524
|
+
});
|
|
525
|
+
// A resolver may ignore cancellation. Do not spend decode or signature
|
|
526
|
+
// verification work on bytes that arrive after this attempt expired.
|
|
527
|
+
if (attemptController.signal.aborted)
|
|
528
|
+
throw abortError();
|
|
529
|
+
if (entryBytes === undefined)
|
|
530
|
+
return undefined;
|
|
531
|
+
const snapshot = await authenticatePolicySnapshotEntryV2(entryBytes, this.descriptor);
|
|
532
|
+
if (!equals(snapshot.digest, digest)) {
|
|
533
|
+
throw new Error("Policy resolver returned the wrong body digest");
|
|
534
|
+
}
|
|
535
|
+
return snapshot;
|
|
536
|
+
});
|
|
537
|
+
// Promise.race installs handlers, but this explicit observer documents and
|
|
538
|
+
// preserves consumption if the resolver settles after its deadline.
|
|
539
|
+
void resolution.then(() => undefined, () => undefined);
|
|
540
|
+
try {
|
|
541
|
+
return await Promise.race([resolution, abortPromise]);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
if (error instanceof PolicyDependencyUnavailableErrorV2)
|
|
545
|
+
throw error;
|
|
546
|
+
throw new PolicyDependencyUnavailableErrorV2(`Policy resolver dependency is unavailable: ${validationMessage(error)}`);
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
clearTimeout(timeout);
|
|
550
|
+
this.lifecycleController.signal.removeEventListener("abort", abortFromLifecycle);
|
|
551
|
+
if (rejectOnAbort !== undefined) {
|
|
552
|
+
attemptController.signal.removeEventListener("abort", rejectOnAbort);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
264
556
|
async resolveSnapshot(digest, cache) {
|
|
265
557
|
const digestKey = bytesKey(digest);
|
|
266
558
|
if (this.acceptedHead?.digestKey === digestKey) {
|
|
@@ -271,16 +563,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
271
563
|
return copySnapshot(pending.snapshot);
|
|
272
564
|
let resolution = cache?.get(digestKey);
|
|
273
565
|
if (resolution === undefined) {
|
|
274
|
-
resolution = (
|
|
275
|
-
const entry = await this.resolvePolicyEntry(copyBytes(digest));
|
|
276
|
-
if (entry === undefined)
|
|
277
|
-
return undefined;
|
|
278
|
-
const snapshot = await authenticatePolicySnapshotEntryV2(entry, this.descriptor);
|
|
279
|
-
if (!equals(snapshot.digest, digest)) {
|
|
280
|
-
throw new Error("Policy resolver returned the wrong body digest");
|
|
281
|
-
}
|
|
282
|
-
return snapshot;
|
|
283
|
-
})();
|
|
566
|
+
resolution = this.resolveExternalSnapshot(digest);
|
|
284
567
|
cache?.set(digestKey, resolution);
|
|
285
568
|
}
|
|
286
569
|
const snapshot = await resolution;
|
|
@@ -300,9 +583,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
300
583
|
}
|
|
301
584
|
catch (error) {
|
|
302
585
|
return {
|
|
303
|
-
status: "
|
|
586
|
+
status: "unavailable",
|
|
304
587
|
digest: copyBytes(child.body.previousPolicyDigest),
|
|
305
|
-
reason: `Policy parent
|
|
588
|
+
reason: `Policy parent dependency is unavailable: ${validationMessage(error)}`,
|
|
306
589
|
};
|
|
307
590
|
}
|
|
308
591
|
if (parent === undefined) {
|
|
@@ -320,13 +603,24 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
320
603
|
}
|
|
321
604
|
return { status: "found", parent };
|
|
322
605
|
}
|
|
606
|
+
candidateAncestryResult(resolution) {
|
|
607
|
+
return resolution.status === "unavailable"
|
|
608
|
+
? {
|
|
609
|
+
status: "missing",
|
|
610
|
+
digest: copyBytes(resolution.digest),
|
|
611
|
+
reason: resolution.reason,
|
|
612
|
+
}
|
|
613
|
+
: resolution;
|
|
614
|
+
}
|
|
323
615
|
acceptedAncestryUnavailable(resolution) {
|
|
324
616
|
return {
|
|
325
617
|
status: "unavailable",
|
|
326
618
|
digest: copyBytes(resolution.digest),
|
|
327
619
|
reason: resolution.status === "missing"
|
|
328
620
|
? "Accepted policy ancestry is unavailable from the resolver"
|
|
329
|
-
:
|
|
621
|
+
: resolution.status === "unavailable"
|
|
622
|
+
? `Accepted policy ancestry is unavailable: ${resolution.reason}`
|
|
623
|
+
: `Accepted policy ancestry validation failed: ${resolution.reason}`,
|
|
330
624
|
};
|
|
331
625
|
}
|
|
332
626
|
async evaluate(candidate) {
|
|
@@ -335,8 +629,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
335
629
|
let cursor = candidate;
|
|
336
630
|
while (cursor.body.sequence !== 0n) {
|
|
337
631
|
const parent = await this.parentOf(cursor, resolutionCache);
|
|
338
|
-
if (parent.status !== "found")
|
|
339
|
-
return parent;
|
|
632
|
+
if (parent.status !== "found") {
|
|
633
|
+
return this.candidateAncestryResult(parent);
|
|
634
|
+
}
|
|
340
635
|
cursor = parent.parent;
|
|
341
636
|
}
|
|
342
637
|
return { status: "accept" };
|
|
@@ -348,8 +643,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
348
643
|
while (candidateCursor.body.sequence > acceptedCursor.body.sequence) {
|
|
349
644
|
candidateChild = candidateCursor;
|
|
350
645
|
const parent = await this.parentOf(candidateCursor, resolutionCache);
|
|
351
|
-
if (parent.status !== "found")
|
|
352
|
-
return parent;
|
|
646
|
+
if (parent.status !== "found") {
|
|
647
|
+
return this.candidateAncestryResult(parent);
|
|
648
|
+
}
|
|
353
649
|
candidateCursor = parent.parent;
|
|
354
650
|
}
|
|
355
651
|
while (acceptedCursor.body.sequence > candidateCursor.body.sequence) {
|
|
@@ -377,8 +673,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
377
673
|
if (acceptedParent.status !== "found") {
|
|
378
674
|
return this.acceptedAncestryUnavailable(acceptedParent);
|
|
379
675
|
}
|
|
380
|
-
if (candidateParent.status !== "found")
|
|
381
|
-
return candidateParent;
|
|
676
|
+
if (candidateParent.status !== "found") {
|
|
677
|
+
return this.candidateAncestryResult(candidateParent);
|
|
678
|
+
}
|
|
382
679
|
candidateCursor = candidateParent.parent;
|
|
383
680
|
acceptedCursor = acceptedParent.parent;
|
|
384
681
|
}
|
|
@@ -399,6 +696,26 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
399
696
|
};
|
|
400
697
|
}
|
|
401
698
|
setFork(evaluation) {
|
|
699
|
+
// Pending snapshots were authenticated when admitted. Combine every one
|
|
700
|
+
// that is already provably a direct child with the pair that first exposed
|
|
701
|
+
// the fork. Canonical selection below may displace either initial child, so
|
|
702
|
+
// the durable layer needs the complete bounded observation set and removes
|
|
703
|
+
// whichever final pair is carried by exportDurableState().
|
|
704
|
+
const pendingForkObservationSnapshots = [...this.pending.values()]
|
|
705
|
+
.map(({ snapshot }) => snapshot)
|
|
706
|
+
.filter((snapshot) => snapshot.body.sequence ===
|
|
707
|
+
evaluation.commonParent.body.sequence + 1n &&
|
|
708
|
+
equals(snapshot.body.previousPolicyDigest, evaluation.commonParent.digest));
|
|
709
|
+
const forkObservationSnapshots = [
|
|
710
|
+
evaluation.candidateChild,
|
|
711
|
+
evaluation.acceptedChild,
|
|
712
|
+
...pendingForkObservationSnapshots,
|
|
713
|
+
];
|
|
714
|
+
const forkObservations = forkObservationSnapshots
|
|
715
|
+
.map(forkProofFromSnapshot)
|
|
716
|
+
.sort(compareForkChildProofs)
|
|
717
|
+
.filter((proof, index, observations) => index === 0 ||
|
|
718
|
+
!equals(proof.entryBytes, observations[index - 1].entryBytes));
|
|
402
719
|
this.project(evaluation.commonParent);
|
|
403
720
|
const children = [
|
|
404
721
|
forkProofFromSnapshot(evaluation.candidateChild),
|
|
@@ -408,8 +725,12 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
408
725
|
commonParent: projectionFromSnapshot(evaluation.commonParent),
|
|
409
726
|
children,
|
|
410
727
|
};
|
|
728
|
+
for (const snapshot of forkObservationSnapshots) {
|
|
729
|
+
this.retainCanonicalForkChild(snapshot);
|
|
730
|
+
}
|
|
411
731
|
this.unavailable = undefined;
|
|
412
732
|
this.pending.clear();
|
|
733
|
+
return forkObservations;
|
|
413
734
|
}
|
|
414
735
|
retainCanonicalForkChild(snapshot) {
|
|
415
736
|
if (this.fork === undefined)
|
|
@@ -434,17 +755,20 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
434
755
|
this.fork.children = [canonical[0], canonical[1]];
|
|
435
756
|
}
|
|
436
757
|
observeAfterFork(snapshot) {
|
|
437
|
-
if (this.fork === undefined || this.acceptedHead === undefined)
|
|
438
|
-
return;
|
|
758
|
+
if (this.fork === undefined || this.acceptedHead === undefined) {
|
|
759
|
+
return undefined;
|
|
760
|
+
}
|
|
439
761
|
const commonParent = this.acceptedHead;
|
|
440
762
|
if (snapshot.body.sequence !== commonParent.body.sequence + 1n ||
|
|
441
763
|
!equals(snapshot.body.previousPolicyDigest, commonParent.digest)) {
|
|
442
|
-
return;
|
|
764
|
+
return undefined;
|
|
443
765
|
}
|
|
444
|
-
// This bounded kernel
|
|
445
|
-
//
|
|
446
|
-
//
|
|
766
|
+
// This bounded kernel retains only the canonical two direct child proofs.
|
|
767
|
+
// Return this already-authenticated observation so the durable outer layer
|
|
768
|
+
// can retain every proof without verifying it twice.
|
|
769
|
+
const observation = forkProofFromSnapshot(snapshot);
|
|
447
770
|
this.retainCanonicalForkChild(snapshot);
|
|
771
|
+
return observation;
|
|
448
772
|
}
|
|
449
773
|
addPending(snapshot, missingParentDigest) {
|
|
450
774
|
const existing = this.pending.get(snapshot.digestKey);
|
|
@@ -484,12 +808,15 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
484
808
|
const retention = this.addPending(snapshot, evaluation.digest);
|
|
485
809
|
this.unavailable = {
|
|
486
810
|
acceptedAncestorDigest: copyBytes(evaluation.digest),
|
|
487
|
-
|
|
811
|
+
comparisonCandidate: copySnapshot(snapshot),
|
|
488
812
|
reason: boundedUnavailableReason(evaluation.reason),
|
|
489
813
|
};
|
|
490
814
|
return retention;
|
|
491
815
|
}
|
|
492
816
|
async drainPending() {
|
|
817
|
+
if (this.lifecycleController.signal.aborted) {
|
|
818
|
+
return { status: "halted" };
|
|
819
|
+
}
|
|
493
820
|
let accepted = false;
|
|
494
821
|
let progress = true;
|
|
495
822
|
while (progress &&
|
|
@@ -502,6 +829,9 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
502
829
|
if (!this.pending.has(pending.snapshot.digestKey))
|
|
503
830
|
continue;
|
|
504
831
|
const evaluation = await this.evaluate(pending.snapshot);
|
|
832
|
+
if (this.lifecycleController.signal.aborted) {
|
|
833
|
+
return { status: "halted" };
|
|
834
|
+
}
|
|
505
835
|
if (evaluation.status === "missing") {
|
|
506
836
|
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
507
837
|
continue;
|
|
@@ -517,41 +847,70 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
517
847
|
accepted = true;
|
|
518
848
|
}
|
|
519
849
|
else if (evaluation.status === "fork") {
|
|
520
|
-
|
|
521
|
-
|
|
850
|
+
return {
|
|
851
|
+
status: "forked",
|
|
852
|
+
forkObservations: this.setFork(evaluation),
|
|
853
|
+
};
|
|
522
854
|
}
|
|
523
855
|
}
|
|
524
856
|
}
|
|
525
857
|
return accepted ? { status: "accepted" } : undefined;
|
|
526
858
|
}
|
|
527
859
|
enqueueAdmission(operation) {
|
|
528
|
-
const result = this.admissionTail.then(
|
|
860
|
+
const result = this.admissionTail.then(async () => {
|
|
861
|
+
if (this.lifecycleController.signal.aborted) {
|
|
862
|
+
return this.haltedResult();
|
|
863
|
+
}
|
|
864
|
+
const admission = await operation();
|
|
865
|
+
return this.lifecycleController.signal.aborted
|
|
866
|
+
? this.haltedResult()
|
|
867
|
+
: admission;
|
|
868
|
+
});
|
|
529
869
|
this.admissionTail = result.then(() => { }, (_reason) => { });
|
|
530
870
|
return result;
|
|
531
871
|
}
|
|
532
|
-
ingest(
|
|
533
|
-
|
|
872
|
+
ingest(entryBytes) {
|
|
873
|
+
if (this.lifecycleController.signal.aborted) {
|
|
874
|
+
return Promise.resolve(this.haltedResult());
|
|
875
|
+
}
|
|
876
|
+
let capturedEntryBytes;
|
|
877
|
+
try {
|
|
878
|
+
// Capture at the API boundary, before this admission waits behind earlier
|
|
879
|
+
// work. Otherwise a caller could mutate a queued entry before validation.
|
|
880
|
+
capturedEntryBytes = capturePolicySnapshotEntryBytesV2(entryBytes);
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
const reason = validationMessage(error);
|
|
884
|
+
return this.enqueueAdmission(async () => this.result("rejected", reason));
|
|
885
|
+
}
|
|
886
|
+
return this.enqueueAdmission(() => this.ingestOne(capturedEntryBytes));
|
|
534
887
|
}
|
|
535
888
|
retryUnavailable() {
|
|
536
889
|
return this.enqueueAdmission(() => this.retryUnavailableOne());
|
|
537
890
|
}
|
|
538
|
-
async ingestOne(
|
|
891
|
+
async ingestOne(entryBytes) {
|
|
539
892
|
let snapshot;
|
|
540
893
|
try {
|
|
541
|
-
snapshot = await
|
|
894
|
+
snapshot = await authenticateCapturedPolicySnapshotEntryV2(entryBytes, this.descriptor);
|
|
542
895
|
}
|
|
543
896
|
catch (error) {
|
|
544
897
|
return this.result("rejected", validationMessage(error));
|
|
545
898
|
}
|
|
899
|
+
if (this.lifecycleController.signal.aborted)
|
|
900
|
+
return this.haltedResult();
|
|
546
901
|
if (this.fork !== undefined) {
|
|
547
|
-
this.observeAfterFork(snapshot);
|
|
548
|
-
return this.result("halted", "Policy reducer is halted by authority equivocation");
|
|
902
|
+
const forkObservation = this.observeAfterFork(snapshot);
|
|
903
|
+
return this.result("halted", "Policy reducer is halted by authority equivocation", undefined, forkObservation === undefined ? undefined : [forkObservation]);
|
|
549
904
|
}
|
|
550
905
|
this.retainCanonicalHeadEntry(snapshot);
|
|
551
906
|
if (this.unavailable !== undefined) {
|
|
552
907
|
if (this.acceptedHead?.digestKey === snapshot.digestKey) {
|
|
553
908
|
return this.result("unavailable", this.unavailable.reason);
|
|
554
909
|
}
|
|
910
|
+
if (this.unavailable.comparisonCandidate.digestKey === snapshot.digestKey &&
|
|
911
|
+
compare(snapshot.entryBytes, this.unavailable.comparisonCandidate.entryBytes) < 0) {
|
|
912
|
+
this.unavailable.comparisonCandidate = copySnapshot(snapshot);
|
|
913
|
+
}
|
|
555
914
|
const existingPending = this.pending.get(snapshot.digestKey);
|
|
556
915
|
if (existingPending !== undefined) {
|
|
557
916
|
this.addPending(snapshot, existingPending.missingParentDigest);
|
|
@@ -566,6 +925,8 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
566
925
|
return this.result("pending", "Policy snapshot is already pending");
|
|
567
926
|
}
|
|
568
927
|
const evaluation = await this.evaluate(snapshot);
|
|
928
|
+
if (this.lifecycleController.signal.aborted)
|
|
929
|
+
return this.haltedResult();
|
|
569
930
|
if (evaluation.status === "reject") {
|
|
570
931
|
return this.result("rejected", evaluation.reason);
|
|
571
932
|
}
|
|
@@ -573,8 +934,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
573
934
|
return this.result("duplicate");
|
|
574
935
|
}
|
|
575
936
|
if (evaluation.status === "fork") {
|
|
576
|
-
this.setFork(evaluation);
|
|
577
|
-
return this.forkedResult();
|
|
937
|
+
return this.forkedResult(this.setFork(evaluation));
|
|
578
938
|
}
|
|
579
939
|
if (evaluation.status === "unavailable") {
|
|
580
940
|
const retention = this.enterUnavailable(snapshot, evaluation);
|
|
@@ -583,7 +943,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
583
943
|
if (evaluation.status === "missing") {
|
|
584
944
|
const pending = this.addPending(snapshot, evaluation.digest);
|
|
585
945
|
return this.result(pending.retained ? "pending" : "capacity", pending.retained
|
|
586
|
-
? "Policy parent is missing"
|
|
946
|
+
? (evaluation.reason ?? "Policy parent is missing")
|
|
587
947
|
: "Policy pending capacity did not retain this candidate", pending.evictedPolicyDigests);
|
|
588
948
|
}
|
|
589
949
|
this.project(snapshot);
|
|
@@ -591,6 +951,8 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
591
951
|
this.result("accepted"));
|
|
592
952
|
}
|
|
593
953
|
async retryUnavailableOne() {
|
|
954
|
+
if (this.lifecycleController.signal.aborted)
|
|
955
|
+
return this.haltedResult();
|
|
594
956
|
if (this.fork !== undefined) {
|
|
595
957
|
return this.result("halted", "Policy reducer is halted by authority equivocation");
|
|
596
958
|
}
|
|
@@ -598,16 +960,18 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
598
960
|
if (unavailable === undefined) {
|
|
599
961
|
return this.result("duplicate", "Policy reducer is not unavailable");
|
|
600
962
|
}
|
|
601
|
-
const pending = this.pending.get(unavailable.
|
|
963
|
+
const pending = this.pending.get(unavailable.comparisonCandidate.digestKey);
|
|
602
964
|
if (pending === undefined) {
|
|
603
965
|
return this.result("capacity", "Unavailable comparison candidate is not retained; re-ingest it before retrying");
|
|
604
966
|
}
|
|
605
967
|
const evaluation = await this.evaluate(pending.snapshot);
|
|
968
|
+
if (this.lifecycleController.signal.aborted)
|
|
969
|
+
return this.haltedResult();
|
|
606
970
|
if (evaluation.status === "unavailable") {
|
|
607
971
|
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
608
972
|
this.unavailable = {
|
|
609
973
|
acceptedAncestorDigest: copyBytes(evaluation.digest),
|
|
610
|
-
|
|
974
|
+
comparisonCandidate: copySnapshot(pending.snapshot),
|
|
611
975
|
reason: boundedUnavailableReason(evaluation.reason),
|
|
612
976
|
};
|
|
613
977
|
return this.result("unavailable", this.unavailable.reason);
|
|
@@ -618,13 +982,12 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
618
982
|
if (evaluation.status === "missing") {
|
|
619
983
|
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
620
984
|
status = "pending";
|
|
621
|
-
reason = "Policy parent is missing";
|
|
985
|
+
reason = evaluation.reason ?? "Policy parent is missing";
|
|
622
986
|
}
|
|
623
987
|
else {
|
|
624
988
|
this.pending.delete(pending.snapshot.digestKey);
|
|
625
989
|
if (evaluation.status === "fork") {
|
|
626
|
-
this.setFork(evaluation);
|
|
627
|
-
return this.forkedResult();
|
|
990
|
+
return this.forkedResult(this.setFork(evaluation));
|
|
628
991
|
}
|
|
629
992
|
if (evaluation.status === "accept") {
|
|
630
993
|
this.project(pending.snapshot);
|