@peerbit/trusted-network 6.0.102 → 6.0.104
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 +90 -0
- package/dist/src/v2-policy-engine.d.ts.map +1 -0
- package/dist/src/v2-policy-engine.js +645 -0
- package/dist/src/v2-policy-engine.js.map +1 -0
- package/dist/src/v2.d.ts +87 -0
- package/dist/src/v2.d.ts.map +1 -0
- package/dist/src/v2.js +409 -0
- package/dist/src/v2.js.map +1 -0
- package/package.json +4 -4
- package/src/v2-policy-engine.ts +935 -0
- package/src/v2.ts +348 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
import { deserialize, serialize } from "@dao-xyz/borsh";
|
|
2
|
+
import { DecryptedThing, PublicSignKey, verify } from "@peerbit/crypto";
|
|
3
|
+
import { Entry, EntryV0, NO_ENCODING } from "@peerbit/log";
|
|
4
|
+
import { compare, equals } from "uint8arrays";
|
|
5
|
+
import {
|
|
6
|
+
NetworkDescriptorV2,
|
|
7
|
+
PolicySnapshotBodyV2,
|
|
8
|
+
PolicySubjectBindingV2,
|
|
9
|
+
TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS,
|
|
10
|
+
assertNetworkDescriptorV2,
|
|
11
|
+
decodePolicySnapshotBodyV2,
|
|
12
|
+
digestPolicySnapshotBodyV2,
|
|
13
|
+
} from "./v2.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Internal policy reducer for the non-activatable TrustedNetwork v2 scaffold.
|
|
17
|
+
*
|
|
18
|
+
* The reducer intentionally is not exported from the package entry point. It
|
|
19
|
+
* retains one accepted snapshot, a bounded pending working set, and (after
|
|
20
|
+
* equivocation) two child proofs. Historical snapshots are supplied by the
|
|
21
|
+
* resolver instead of being accumulated in memory.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const DEFAULT_MAX_PENDING_POLICIES_V2 = 64;
|
|
25
|
+
const DEFAULT_MAX_PENDING_POLICY_BYTES_V2 = 256 * 1024;
|
|
26
|
+
const PENDING_POLICY_ACCOUNTING_OVERHEAD_V2 = 64;
|
|
27
|
+
const MAX_UNAVAILABLE_REASON_LENGTH_V2 = 512;
|
|
28
|
+
|
|
29
|
+
const copyBytes = (bytes: Uint8Array): Uint8Array => Uint8Array.from(bytes);
|
|
30
|
+
|
|
31
|
+
const bytesKey = (bytes: Uint8Array): string => {
|
|
32
|
+
let key = "";
|
|
33
|
+
for (const byte of bytes) key += byte.toString(16).padStart(2, "0");
|
|
34
|
+
return key;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const compareKeys = (left: string, right: string): number =>
|
|
38
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
39
|
+
|
|
40
|
+
const copyPublicKey = (key: PublicSignKey): PublicSignKey =>
|
|
41
|
+
deserialize(serialize(key), PublicSignKey);
|
|
42
|
+
|
|
43
|
+
const publicKeyId = (key: PublicSignKey): string => bytesKey(serialize(key));
|
|
44
|
+
|
|
45
|
+
type ValidatedPolicySnapshotV2 = {
|
|
46
|
+
body: PolicySnapshotBodyV2;
|
|
47
|
+
digest: Uint8Array;
|
|
48
|
+
digestKey: string;
|
|
49
|
+
entryBytes: Uint8Array;
|
|
50
|
+
accountedBytes: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const copyBinding = (binding: PolicySubjectBindingV2): PolicySubjectBindingV2 =>
|
|
54
|
+
new PolicySubjectBindingV2({
|
|
55
|
+
signingKey: copyPublicKey(binding.signingKey),
|
|
56
|
+
roles: binding.roles,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const copyBody = (body: PolicySnapshotBodyV2): PolicySnapshotBodyV2 =>
|
|
60
|
+
new PolicySnapshotBodyV2({
|
|
61
|
+
networkId: copyBytes(body.networkId),
|
|
62
|
+
sequence: body.sequence,
|
|
63
|
+
previousPolicyDigest: copyBytes(body.previousPolicyDigest),
|
|
64
|
+
bindings: body.bindings.map(copyBinding),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const copySnapshot = (
|
|
68
|
+
snapshot: ValidatedPolicySnapshotV2,
|
|
69
|
+
): ValidatedPolicySnapshotV2 => ({
|
|
70
|
+
body: copyBody(snapshot.body),
|
|
71
|
+
digest: copyBytes(snapshot.digest),
|
|
72
|
+
digestKey: snapshot.digestKey,
|
|
73
|
+
entryBytes: copyBytes(snapshot.entryBytes),
|
|
74
|
+
accountedBytes: snapshot.accountedBytes,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export type PolicySnapshotResolverV2 = (
|
|
78
|
+
digest: Uint8Array,
|
|
79
|
+
) => EntryV0<Uint8Array> | undefined | Promise<EntryV0<Uint8Array> | undefined>;
|
|
80
|
+
|
|
81
|
+
export type PolicyParentFetchHintV2 = {
|
|
82
|
+
kind: "policy-parent";
|
|
83
|
+
digest: Uint8Array;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type PolicyHeadProjectionV2 = {
|
|
87
|
+
sequence: bigint;
|
|
88
|
+
digest: Uint8Array;
|
|
89
|
+
bindings: PolicySubjectBindingV2[];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type PolicyForkChildProofV2 = {
|
|
93
|
+
sequence: bigint;
|
|
94
|
+
digest: Uint8Array;
|
|
95
|
+
entryBytes: Uint8Array;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type PolicyForkEvidenceV2 = {
|
|
99
|
+
commonParent: PolicyHeadProjectionV2;
|
|
100
|
+
children: [PolicyForkChildProofV2, PolicyForkChildProofV2];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type PolicyAdmissionStatusV2 =
|
|
104
|
+
| "accepted"
|
|
105
|
+
| "duplicate"
|
|
106
|
+
| "pending"
|
|
107
|
+
| "unavailable"
|
|
108
|
+
| "capacity"
|
|
109
|
+
| "rejected"
|
|
110
|
+
| "forked"
|
|
111
|
+
| "halted";
|
|
112
|
+
|
|
113
|
+
export type PolicyAdmissionResultV2 = {
|
|
114
|
+
status: PolicyAdmissionStatusV2;
|
|
115
|
+
reason?: string;
|
|
116
|
+
head?: PolicyHeadProjectionV2;
|
|
117
|
+
fetchHints: PolicyParentFetchHintV2[];
|
|
118
|
+
pendingCount: number;
|
|
119
|
+
pendingBytes: number;
|
|
120
|
+
evictedPolicyDigests?: Uint8Array[];
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type PendingPolicySnapshotV2 = {
|
|
124
|
+
snapshot: ValidatedPolicySnapshotV2;
|
|
125
|
+
missingParentDigest: Uint8Array;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
type UnavailablePolicyComparisonV2 = {
|
|
129
|
+
acceptedAncestorDigest: Uint8Array;
|
|
130
|
+
candidateDigestKey: string;
|
|
131
|
+
reason: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
type ParentResolutionV2 =
|
|
135
|
+
| { status: "found"; parent: ValidatedPolicySnapshotV2 }
|
|
136
|
+
| { status: "missing"; digest: Uint8Array }
|
|
137
|
+
| { status: "reject"; digest: Uint8Array; reason: string };
|
|
138
|
+
|
|
139
|
+
type SnapshotResolutionCacheV2 = Map<
|
|
140
|
+
string,
|
|
141
|
+
Promise<ValidatedPolicySnapshotV2 | undefined>
|
|
142
|
+
>;
|
|
143
|
+
|
|
144
|
+
type EvaluationV2 =
|
|
145
|
+
| { status: "accept" }
|
|
146
|
+
| { status: "duplicate" }
|
|
147
|
+
| { status: "missing"; digest: Uint8Array }
|
|
148
|
+
| { status: "reject"; reason: string }
|
|
149
|
+
| { status: "unavailable"; digest: Uint8Array; reason: string }
|
|
150
|
+
| {
|
|
151
|
+
status: "fork";
|
|
152
|
+
commonParent: ValidatedPolicySnapshotV2;
|
|
153
|
+
candidateChild: ValidatedPolicySnapshotV2;
|
|
154
|
+
acceptedChild: ValidatedPolicySnapshotV2;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
type PendingDrainOutcomeV2 =
|
|
158
|
+
| { status: "accepted" }
|
|
159
|
+
| { status: "forked" }
|
|
160
|
+
| {
|
|
161
|
+
status: "unavailable";
|
|
162
|
+
retained: boolean;
|
|
163
|
+
evictedPolicyDigests: Uint8Array[];
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const validationMessage = (error: unknown): string =>
|
|
167
|
+
error instanceof Error ? error.message : String(error);
|
|
168
|
+
|
|
169
|
+
const boundedUnavailableReason = (reason: string): string =>
|
|
170
|
+
reason.slice(0, MAX_UNAVAILABLE_REASON_LENGTH_V2);
|
|
171
|
+
|
|
172
|
+
export const authenticatePolicySnapshotEntryV2 = async (
|
|
173
|
+
entry: unknown,
|
|
174
|
+
descriptor: NetworkDescriptorV2,
|
|
175
|
+
): Promise<ValidatedPolicySnapshotV2> => {
|
|
176
|
+
assertNetworkDescriptorV2(descriptor);
|
|
177
|
+
if (!(entry instanceof EntryV0)) {
|
|
178
|
+
throw new Error("Policy snapshot must use EntryV0");
|
|
179
|
+
}
|
|
180
|
+
if (!(entry._meta instanceof DecryptedThing)) {
|
|
181
|
+
throw new Error("Policy snapshot metadata must be public");
|
|
182
|
+
}
|
|
183
|
+
if (!(entry._payload instanceof DecryptedThing)) {
|
|
184
|
+
throw new Error("Policy snapshot payload must be public");
|
|
185
|
+
}
|
|
186
|
+
if (
|
|
187
|
+
entry._signatures === undefined ||
|
|
188
|
+
entry._signatures.signatures.length !== 1
|
|
189
|
+
) {
|
|
190
|
+
throw new Error("Policy snapshot must contain exactly one signature");
|
|
191
|
+
}
|
|
192
|
+
if (!(entry._signatures.signatures[0] instanceof DecryptedThing)) {
|
|
193
|
+
throw new Error("Policy snapshot signature must be public");
|
|
194
|
+
}
|
|
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
|
+
authenticatedEntry.init({ encoding: NO_ENCODING });
|
|
201
|
+
|
|
202
|
+
const signatures = await authenticatedEntry.getSignatures();
|
|
203
|
+
if (signatures.length !== 1) {
|
|
204
|
+
throw new Error("Policy snapshot must resolve exactly one signature");
|
|
205
|
+
}
|
|
206
|
+
const signature = signatures[0]!;
|
|
207
|
+
if (
|
|
208
|
+
!equals(
|
|
209
|
+
serialize(signature.publicKey),
|
|
210
|
+
serialize(descriptor.policyAuthority),
|
|
211
|
+
)
|
|
212
|
+
) {
|
|
213
|
+
throw new Error("Policy snapshot signer is not the policy authority");
|
|
214
|
+
}
|
|
215
|
+
if (!(await verify(signature, authenticatedEntry.getSignableBytes()))) {
|
|
216
|
+
throw new Error("Policy snapshot authority signature is invalid");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const payload = await authenticatedEntry.getPayloadValue();
|
|
220
|
+
if (!(payload instanceof Uint8Array)) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
"Policy snapshot payload must contain canonical body bytes",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const body = decodePolicySnapshotBodyV2(copyBytes(payload), descriptor);
|
|
226
|
+
const digest = digestPolicySnapshotBodyV2(body);
|
|
227
|
+
return {
|
|
228
|
+
body: copyBody(body),
|
|
229
|
+
digest: copyBytes(digest),
|
|
230
|
+
digestKey: bytesKey(digest),
|
|
231
|
+
entryBytes,
|
|
232
|
+
accountedBytes:
|
|
233
|
+
entryBytes.byteLength +
|
|
234
|
+
serialize(body).byteLength +
|
|
235
|
+
PENDING_POLICY_ACCOUNTING_OVERHEAD_V2,
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const projectionFromSnapshot = (
|
|
240
|
+
snapshot: ValidatedPolicySnapshotV2,
|
|
241
|
+
): PolicyHeadProjectionV2 => ({
|
|
242
|
+
sequence: snapshot.body.sequence,
|
|
243
|
+
digest: copyBytes(snapshot.digest),
|
|
244
|
+
bindings: snapshot.body.bindings.map(copyBinding),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const forkProofFromSnapshot = (
|
|
248
|
+
snapshot: ValidatedPolicySnapshotV2,
|
|
249
|
+
): PolicyForkChildProofV2 => ({
|
|
250
|
+
sequence: snapshot.body.sequence,
|
|
251
|
+
digest: copyBytes(snapshot.digest),
|
|
252
|
+
entryBytes: copyBytes(snapshot.entryBytes),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const copyForkChildProof = (
|
|
256
|
+
proof: PolicyForkChildProofV2,
|
|
257
|
+
): PolicyForkChildProofV2 => ({
|
|
258
|
+
sequence: proof.sequence,
|
|
259
|
+
digest: copyBytes(proof.digest),
|
|
260
|
+
entryBytes: copyBytes(proof.entryBytes),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const compareForkChildProofs = (
|
|
264
|
+
left: PolicyForkChildProofV2,
|
|
265
|
+
right: PolicyForkChildProofV2,
|
|
266
|
+
): number => {
|
|
267
|
+
const digestOrder = compare(left.digest, right.digest);
|
|
268
|
+
return digestOrder === 0
|
|
269
|
+
? compare(left.entryBytes, right.entryBytes)
|
|
270
|
+
: digestOrder;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const copyProjection = (
|
|
274
|
+
projection: PolicyHeadProjectionV2,
|
|
275
|
+
): PolicyHeadProjectionV2 => ({
|
|
276
|
+
sequence: projection.sequence,
|
|
277
|
+
digest: copyBytes(projection.digest),
|
|
278
|
+
bindings: projection.bindings.map(copyBinding),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const copyForkEvidence = (
|
|
282
|
+
evidence: PolicyForkEvidenceV2,
|
|
283
|
+
): PolicyForkEvidenceV2 => ({
|
|
284
|
+
commonParent: copyProjection(evidence.commonParent),
|
|
285
|
+
children: [
|
|
286
|
+
copyForkChildProof(evidence.children[0]),
|
|
287
|
+
copyForkChildProof(evidence.children[1]),
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
export class TrustedNetworkV2PolicyReducer {
|
|
292
|
+
private readonly descriptor: NetworkDescriptorV2;
|
|
293
|
+
private readonly resolvePolicyEntry: PolicySnapshotResolverV2;
|
|
294
|
+
private readonly maxPending: number;
|
|
295
|
+
private readonly maxPendingPolicyBytes: number;
|
|
296
|
+
private acceptedHead?: ValidatedPolicySnapshotV2;
|
|
297
|
+
private projectedRoles = new Map<string, number>();
|
|
298
|
+
private readonly pending = new Map<string, PendingPolicySnapshotV2>();
|
|
299
|
+
// Recovery is bound to this exact candidate. Unrelated admissions may enter
|
|
300
|
+
// the bounded pending set but can never restore ACTIVE authorization.
|
|
301
|
+
private unavailable?: UnavailablePolicyComparisonV2;
|
|
302
|
+
private fork?: PolicyForkEvidenceV2;
|
|
303
|
+
private admissionTail: Promise<void> = Promise.resolve();
|
|
304
|
+
|
|
305
|
+
constructor(properties: {
|
|
306
|
+
descriptor: NetworkDescriptorV2;
|
|
307
|
+
resolvePolicyEntry: PolicySnapshotResolverV2;
|
|
308
|
+
maxPending?: number;
|
|
309
|
+
maxPendingPolicyBytes?: number;
|
|
310
|
+
}) {
|
|
311
|
+
assertNetworkDescriptorV2(properties.descriptor);
|
|
312
|
+
const maxPending = properties.maxPending ?? DEFAULT_MAX_PENDING_POLICIES_V2;
|
|
313
|
+
if (!Number.isSafeInteger(maxPending) || maxPending < 1) {
|
|
314
|
+
throw new Error("maxPending must be a positive safe integer");
|
|
315
|
+
}
|
|
316
|
+
const maxPendingPolicyBytes =
|
|
317
|
+
properties.maxPendingPolicyBytes ?? DEFAULT_MAX_PENDING_POLICY_BYTES_V2;
|
|
318
|
+
if (
|
|
319
|
+
!Number.isSafeInteger(maxPendingPolicyBytes) ||
|
|
320
|
+
maxPendingPolicyBytes < 1
|
|
321
|
+
) {
|
|
322
|
+
throw new Error("maxPendingPolicyBytes must be a positive safe integer");
|
|
323
|
+
}
|
|
324
|
+
this.descriptor = deserialize(
|
|
325
|
+
serialize(properties.descriptor),
|
|
326
|
+
NetworkDescriptorV2,
|
|
327
|
+
);
|
|
328
|
+
this.resolvePolicyEntry = properties.resolvePolicyEntry;
|
|
329
|
+
this.maxPending = maxPending;
|
|
330
|
+
this.maxPendingPolicyBytes = maxPendingPolicyBytes;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
get state(): "EMPTY" | "ACTIVE" | "UNAVAILABLE" | "FORKED" {
|
|
334
|
+
if (this.fork !== undefined) return "FORKED";
|
|
335
|
+
if (this.unavailable !== undefined) return "UNAVAILABLE";
|
|
336
|
+
return this.acceptedHead === undefined ? "EMPTY" : "ACTIVE";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
get head(): PolicyHeadProjectionV2 | undefined {
|
|
340
|
+
return this.acceptedHead === undefined
|
|
341
|
+
? undefined
|
|
342
|
+
: projectionFromSnapshot(this.acceptedHead);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
get forkEvidence(): PolicyForkEvidenceV2 | undefined {
|
|
346
|
+
return this.fork === undefined ? undefined : copyForkEvidence(this.fork);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
get pendingCount(): number {
|
|
350
|
+
return this.pending.size;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
get pendingBytes(): number {
|
|
354
|
+
let total = 0;
|
|
355
|
+
for (const { snapshot } of this.pending.values()) {
|
|
356
|
+
total += snapshot.accountedBytes;
|
|
357
|
+
}
|
|
358
|
+
return total;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
get pendingDigests(): Uint8Array[] {
|
|
362
|
+
return [...this.pending.values()]
|
|
363
|
+
.sort((a, b) => compareKeys(a.snapshot.digestKey, b.snapshot.digestKey))
|
|
364
|
+
.map(({ snapshot }) => copyBytes(snapshot.digest));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
rolesFor(subject: PublicSignKey): number {
|
|
368
|
+
return this.projectedRoles.get(publicKeyId(subject)) ?? 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
isAuthorized(subject: PublicSignKey, roles: number): boolean {
|
|
372
|
+
if (
|
|
373
|
+
this.state !== "ACTIVE" ||
|
|
374
|
+
!Number.isInteger(roles) ||
|
|
375
|
+
roles === 0 ||
|
|
376
|
+
(roles & ~TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS) !== 0
|
|
377
|
+
) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
return (this.rolesFor(subject) & roles) === roles;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private fetchHints(): PolicyParentFetchHintV2[] {
|
|
384
|
+
const unique = new Map<string, Uint8Array>();
|
|
385
|
+
for (const { missingParentDigest } of this.pending.values()) {
|
|
386
|
+
unique.set(bytesKey(missingParentDigest), missingParentDigest);
|
|
387
|
+
}
|
|
388
|
+
if (this.unavailable !== undefined) {
|
|
389
|
+
unique.set(
|
|
390
|
+
bytesKey(this.unavailable.acceptedAncestorDigest),
|
|
391
|
+
this.unavailable.acceptedAncestorDigest,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return [...unique.entries()]
|
|
395
|
+
.sort(([a], [b]) => compareKeys(a, b))
|
|
396
|
+
.map(([, digest]) => ({
|
|
397
|
+
kind: "policy-parent" as const,
|
|
398
|
+
digest: copyBytes(digest),
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private result(
|
|
403
|
+
status: PolicyAdmissionStatusV2,
|
|
404
|
+
reason?: string,
|
|
405
|
+
evictedPolicyDigests?: Uint8Array[],
|
|
406
|
+
): PolicyAdmissionResultV2 {
|
|
407
|
+
return {
|
|
408
|
+
status,
|
|
409
|
+
reason,
|
|
410
|
+
head: this.head,
|
|
411
|
+
fetchHints: this.fetchHints(),
|
|
412
|
+
pendingCount: this.pending.size,
|
|
413
|
+
pendingBytes: this.pendingBytes,
|
|
414
|
+
evictedPolicyDigests:
|
|
415
|
+
evictedPolicyDigests === undefined
|
|
416
|
+
? undefined
|
|
417
|
+
: evictedPolicyDigests.map(copyBytes),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private forkedResult(): PolicyAdmissionResultV2 {
|
|
422
|
+
return this.result("forked", "Policy authority signed competing children");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private unavailableResult(retention?: {
|
|
426
|
+
retained: boolean;
|
|
427
|
+
evictedPolicyDigests: Uint8Array[];
|
|
428
|
+
}): PolicyAdmissionResultV2 {
|
|
429
|
+
const blockedCandidateRetained =
|
|
430
|
+
this.unavailable !== undefined &&
|
|
431
|
+
this.pending.has(this.unavailable.candidateDigestKey);
|
|
432
|
+
const recoverable =
|
|
433
|
+
(retention?.retained ?? true) && blockedCandidateRetained;
|
|
434
|
+
const reason = recoverable
|
|
435
|
+
? this.unavailable!.reason
|
|
436
|
+
: blockedCandidateRetained
|
|
437
|
+
? "Policy pending capacity did not retain this candidate"
|
|
438
|
+
: "Policy reducer remains unavailable because pending capacity did not retain the blocked candidate";
|
|
439
|
+
return this.result(
|
|
440
|
+
recoverable ? "unavailable" : "capacity",
|
|
441
|
+
reason,
|
|
442
|
+
retention?.evictedPolicyDigests,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private completedDrainResult(
|
|
447
|
+
outcome: PendingDrainOutcomeV2 | undefined,
|
|
448
|
+
): PolicyAdmissionResultV2 | undefined {
|
|
449
|
+
if (outcome?.status === "forked") return this.forkedResult();
|
|
450
|
+
return outcome?.status === "unavailable"
|
|
451
|
+
? this.unavailableResult(outcome)
|
|
452
|
+
: undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private project(snapshot: ValidatedPolicySnapshotV2): void {
|
|
456
|
+
this.acceptedHead = copySnapshot(snapshot);
|
|
457
|
+
this.projectedRoles = new Map(
|
|
458
|
+
snapshot.body.bindings.map((binding) => [
|
|
459
|
+
publicKeyId(binding.signingKey),
|
|
460
|
+
binding.roles,
|
|
461
|
+
]),
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private retainCanonicalHeadEntry(snapshot: ValidatedPolicySnapshotV2): void {
|
|
466
|
+
if (
|
|
467
|
+
this.acceptedHead?.digestKey === snapshot.digestKey &&
|
|
468
|
+
compare(snapshot.entryBytes, this.acceptedHead.entryBytes) < 0
|
|
469
|
+
) {
|
|
470
|
+
this.acceptedHead = copySnapshot(snapshot);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private async resolveSnapshot(
|
|
475
|
+
digest: Uint8Array,
|
|
476
|
+
cache?: SnapshotResolutionCacheV2,
|
|
477
|
+
): Promise<ValidatedPolicySnapshotV2 | undefined> {
|
|
478
|
+
const digestKey = bytesKey(digest);
|
|
479
|
+
if (this.acceptedHead?.digestKey === digestKey) {
|
|
480
|
+
return copySnapshot(this.acceptedHead);
|
|
481
|
+
}
|
|
482
|
+
const pending = this.pending.get(digestKey);
|
|
483
|
+
if (pending !== undefined) return copySnapshot(pending.snapshot);
|
|
484
|
+
let resolution = cache?.get(digestKey);
|
|
485
|
+
if (resolution === undefined) {
|
|
486
|
+
resolution = (async () => {
|
|
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
|
+
})();
|
|
498
|
+
cache?.set(digestKey, resolution);
|
|
499
|
+
}
|
|
500
|
+
const snapshot = await resolution;
|
|
501
|
+
return snapshot === undefined ? undefined : copySnapshot(snapshot);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private async parentOf(
|
|
505
|
+
child: ValidatedPolicySnapshotV2,
|
|
506
|
+
cache?: SnapshotResolutionCacheV2,
|
|
507
|
+
): Promise<ParentResolutionV2> {
|
|
508
|
+
if (child.body.sequence === 0n) {
|
|
509
|
+
return {
|
|
510
|
+
status: "reject",
|
|
511
|
+
digest: copyBytes(child.body.previousPolicyDigest),
|
|
512
|
+
reason: "Genesis policy has no parent",
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
let parent: ValidatedPolicySnapshotV2 | undefined;
|
|
516
|
+
try {
|
|
517
|
+
parent = await this.resolveSnapshot(
|
|
518
|
+
child.body.previousPolicyDigest,
|
|
519
|
+
cache,
|
|
520
|
+
);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
return {
|
|
523
|
+
status: "reject",
|
|
524
|
+
digest: copyBytes(child.body.previousPolicyDigest),
|
|
525
|
+
reason: `Policy parent validation failed: ${validationMessage(error)}`,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
if (parent === undefined) {
|
|
529
|
+
return {
|
|
530
|
+
status: "missing",
|
|
531
|
+
digest: copyBytes(child.body.previousPolicyDigest),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (child.body.sequence !== parent.body.sequence + 1n) {
|
|
535
|
+
return {
|
|
536
|
+
status: "reject",
|
|
537
|
+
digest: copyBytes(child.body.previousPolicyDigest),
|
|
538
|
+
reason: "Policy sequence is not contiguous with its parent",
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return { status: "found", parent };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private acceptedAncestryUnavailable(
|
|
545
|
+
resolution: Exclude<ParentResolutionV2, { status: "found" }>,
|
|
546
|
+
): Extract<EvaluationV2, { status: "unavailable" }> {
|
|
547
|
+
return {
|
|
548
|
+
status: "unavailable",
|
|
549
|
+
digest: copyBytes(resolution.digest),
|
|
550
|
+
reason:
|
|
551
|
+
resolution.status === "missing"
|
|
552
|
+
? "Accepted policy ancestry is unavailable from the resolver"
|
|
553
|
+
: `Accepted policy ancestry validation failed: ${resolution.reason}`,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private async evaluate(
|
|
558
|
+
candidate: ValidatedPolicySnapshotV2,
|
|
559
|
+
): Promise<EvaluationV2> {
|
|
560
|
+
const resolutionCache: SnapshotResolutionCacheV2 = new Map();
|
|
561
|
+
if (this.acceptedHead === undefined) {
|
|
562
|
+
let cursor = candidate;
|
|
563
|
+
while (cursor.body.sequence !== 0n) {
|
|
564
|
+
const parent = await this.parentOf(cursor, resolutionCache);
|
|
565
|
+
if (parent.status !== "found") return parent;
|
|
566
|
+
cursor = parent.parent;
|
|
567
|
+
}
|
|
568
|
+
return { status: "accept" };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
let candidateCursor = candidate;
|
|
572
|
+
let acceptedCursor = this.acceptedHead;
|
|
573
|
+
let candidateChild: ValidatedPolicySnapshotV2 | undefined;
|
|
574
|
+
let acceptedChild: ValidatedPolicySnapshotV2 | undefined;
|
|
575
|
+
|
|
576
|
+
while (candidateCursor.body.sequence > acceptedCursor.body.sequence) {
|
|
577
|
+
candidateChild = candidateCursor;
|
|
578
|
+
const parent = await this.parentOf(candidateCursor, resolutionCache);
|
|
579
|
+
if (parent.status !== "found") return parent;
|
|
580
|
+
candidateCursor = parent.parent;
|
|
581
|
+
}
|
|
582
|
+
while (acceptedCursor.body.sequence > candidateCursor.body.sequence) {
|
|
583
|
+
acceptedChild = acceptedCursor;
|
|
584
|
+
const parent = await this.parentOf(acceptedCursor, resolutionCache);
|
|
585
|
+
if (parent.status !== "found") {
|
|
586
|
+
return this.acceptedAncestryUnavailable(parent);
|
|
587
|
+
}
|
|
588
|
+
acceptedCursor = parent.parent;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
while (!equals(candidateCursor.digest, acceptedCursor.digest)) {
|
|
592
|
+
if (
|
|
593
|
+
candidateCursor.body.sequence === 0n ||
|
|
594
|
+
acceptedCursor.body.sequence === 0n
|
|
595
|
+
) {
|
|
596
|
+
return {
|
|
597
|
+
status: "reject",
|
|
598
|
+
reason: "Policy branches do not share the descriptor genesis",
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
candidateChild = candidateCursor;
|
|
602
|
+
acceptedChild = acceptedCursor;
|
|
603
|
+
const [candidateParent, acceptedParent] = await Promise.all([
|
|
604
|
+
this.parentOf(candidateCursor, resolutionCache),
|
|
605
|
+
this.parentOf(acceptedCursor, resolutionCache),
|
|
606
|
+
]);
|
|
607
|
+
if (acceptedParent.status !== "found") {
|
|
608
|
+
return this.acceptedAncestryUnavailable(acceptedParent);
|
|
609
|
+
}
|
|
610
|
+
if (candidateParent.status !== "found") return candidateParent;
|
|
611
|
+
candidateCursor = candidateParent.parent;
|
|
612
|
+
acceptedCursor = acceptedParent.parent;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (candidateChild === undefined) {
|
|
616
|
+
return { status: "duplicate" };
|
|
617
|
+
}
|
|
618
|
+
if (acceptedChild === undefined) {
|
|
619
|
+
return { status: "accept" };
|
|
620
|
+
}
|
|
621
|
+
if (equals(candidateChild.digest, acceptedChild.digest)) {
|
|
622
|
+
return { status: "duplicate" };
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
status: "fork",
|
|
626
|
+
commonParent: candidateCursor,
|
|
627
|
+
candidateChild,
|
|
628
|
+
acceptedChild,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private setFork(evaluation: Extract<EvaluationV2, { status: "fork" }>): void {
|
|
633
|
+
this.project(evaluation.commonParent);
|
|
634
|
+
const children = [
|
|
635
|
+
forkProofFromSnapshot(evaluation.candidateChild),
|
|
636
|
+
forkProofFromSnapshot(evaluation.acceptedChild),
|
|
637
|
+
].sort(compareForkChildProofs) as [
|
|
638
|
+
PolicyForkChildProofV2,
|
|
639
|
+
PolicyForkChildProofV2,
|
|
640
|
+
];
|
|
641
|
+
this.fork = {
|
|
642
|
+
commonParent: projectionFromSnapshot(evaluation.commonParent),
|
|
643
|
+
children,
|
|
644
|
+
};
|
|
645
|
+
this.unavailable = undefined;
|
|
646
|
+
this.pending.clear();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private retainCanonicalForkChild(snapshot: ValidatedPolicySnapshotV2): void {
|
|
650
|
+
if (this.fork === undefined) return;
|
|
651
|
+
const byDigest = new Map<string, PolicyForkChildProofV2>();
|
|
652
|
+
for (const proof of [
|
|
653
|
+
...this.fork.children,
|
|
654
|
+
forkProofFromSnapshot(snapshot),
|
|
655
|
+
]) {
|
|
656
|
+
const digestKey = bytesKey(proof.digest);
|
|
657
|
+
const retained = byDigest.get(digestKey);
|
|
658
|
+
if (
|
|
659
|
+
retained === undefined ||
|
|
660
|
+
compare(proof.entryBytes, retained.entryBytes) < 0
|
|
661
|
+
) {
|
|
662
|
+
byDigest.set(digestKey, copyForkChildProof(proof));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const canonical = [...byDigest.values()]
|
|
666
|
+
.sort(compareForkChildProofs)
|
|
667
|
+
.slice(0, 2);
|
|
668
|
+
if (canonical.length !== 2) return;
|
|
669
|
+
this.fork.children = [canonical[0]!, canonical[1]!];
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private observeAfterFork(snapshot: ValidatedPolicySnapshotV2): void {
|
|
673
|
+
if (this.fork === undefined || this.acceptedHead === undefined) return;
|
|
674
|
+
const commonParent = this.acceptedHead;
|
|
675
|
+
if (
|
|
676
|
+
snapshot.body.sequence !== commonParent.body.sequence + 1n ||
|
|
677
|
+
!equals(snapshot.body.previousPolicyDigest, commonParent.digest)
|
|
678
|
+
) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// This bounded kernel deliberately retains only the canonical two direct
|
|
683
|
+
// child proofs. Durable storage of every authenticated fork observation is
|
|
684
|
+
// an outer-layer responsibility for a later integration slice.
|
|
685
|
+
this.retainCanonicalForkChild(snapshot);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private addPending(
|
|
689
|
+
snapshot: ValidatedPolicySnapshotV2,
|
|
690
|
+
missingParentDigest: Uint8Array,
|
|
691
|
+
): { retained: boolean; evictedPolicyDigests: Uint8Array[] } {
|
|
692
|
+
const existing = this.pending.get(snapshot.digestKey);
|
|
693
|
+
if (existing !== undefined) {
|
|
694
|
+
existing.missingParentDigest = copyBytes(missingParentDigest);
|
|
695
|
+
if (
|
|
696
|
+
snapshot.accountedBytes <= this.maxPendingPolicyBytes &&
|
|
697
|
+
compare(snapshot.entryBytes, existing.snapshot.entryBytes) < 0
|
|
698
|
+
) {
|
|
699
|
+
existing.snapshot = copySnapshot(snapshot);
|
|
700
|
+
}
|
|
701
|
+
return { retained: true, evictedPolicyDigests: [] };
|
|
702
|
+
}
|
|
703
|
+
if (snapshot.accountedBytes > this.maxPendingPolicyBytes) {
|
|
704
|
+
return {
|
|
705
|
+
retained: false,
|
|
706
|
+
evictedPolicyDigests: [copyBytes(snapshot.digest)],
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
this.pending.set(snapshot.digestKey, {
|
|
710
|
+
snapshot: copySnapshot(snapshot),
|
|
711
|
+
missingParentDigest: copyBytes(missingParentDigest),
|
|
712
|
+
});
|
|
713
|
+
const ordered = [...this.pending.entries()].sort(([a], [b]) =>
|
|
714
|
+
compareKeys(a, b),
|
|
715
|
+
);
|
|
716
|
+
const retainedKeys = new Set(
|
|
717
|
+
ordered.slice(0, this.maxPending).map(([key]) => key),
|
|
718
|
+
);
|
|
719
|
+
const evictedPolicyDigests: Uint8Array[] = [];
|
|
720
|
+
for (const [key, pending] of ordered) {
|
|
721
|
+
if (retainedKeys.has(key)) continue;
|
|
722
|
+
evictedPolicyDigests.push(copyBytes(pending.snapshot.digest));
|
|
723
|
+
this.pending.delete(key);
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
retained: retainedKeys.has(snapshot.digestKey),
|
|
727
|
+
evictedPolicyDigests,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private enterUnavailable(
|
|
732
|
+
snapshot: ValidatedPolicySnapshotV2,
|
|
733
|
+
evaluation: Extract<EvaluationV2, { status: "unavailable" }>,
|
|
734
|
+
): { retained: boolean; evictedPolicyDigests: Uint8Array[] } {
|
|
735
|
+
const retention = this.addPending(snapshot, evaluation.digest);
|
|
736
|
+
this.unavailable = {
|
|
737
|
+
acceptedAncestorDigest: copyBytes(evaluation.digest),
|
|
738
|
+
candidateDigestKey: snapshot.digestKey,
|
|
739
|
+
reason: boundedUnavailableReason(evaluation.reason),
|
|
740
|
+
};
|
|
741
|
+
return retention;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private async drainPending(): Promise<PendingDrainOutcomeV2 | undefined> {
|
|
745
|
+
let accepted = false;
|
|
746
|
+
let progress = true;
|
|
747
|
+
while (
|
|
748
|
+
progress &&
|
|
749
|
+
this.pending.size > 0 &&
|
|
750
|
+
this.fork === undefined &&
|
|
751
|
+
this.unavailable === undefined
|
|
752
|
+
) {
|
|
753
|
+
progress = false;
|
|
754
|
+
const ordered = [...this.pending.values()].sort((a, b) =>
|
|
755
|
+
compareKeys(a.snapshot.digestKey, b.snapshot.digestKey),
|
|
756
|
+
);
|
|
757
|
+
for (const pending of ordered) {
|
|
758
|
+
if (!this.pending.has(pending.snapshot.digestKey)) continue;
|
|
759
|
+
const evaluation = await this.evaluate(pending.snapshot);
|
|
760
|
+
if (evaluation.status === "missing") {
|
|
761
|
+
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (evaluation.status === "unavailable") {
|
|
765
|
+
const retention = this.enterUnavailable(pending.snapshot, evaluation);
|
|
766
|
+
return { status: "unavailable", ...retention };
|
|
767
|
+
}
|
|
768
|
+
this.pending.delete(pending.snapshot.digestKey);
|
|
769
|
+
progress = true;
|
|
770
|
+
if (evaluation.status === "accept") {
|
|
771
|
+
this.project(pending.snapshot);
|
|
772
|
+
accepted = true;
|
|
773
|
+
} else if (evaluation.status === "fork") {
|
|
774
|
+
this.setFork(evaluation);
|
|
775
|
+
return { status: "forked" };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return accepted ? { status: "accepted" } : undefined;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private enqueueAdmission(
|
|
783
|
+
operation: () => Promise<PolicyAdmissionResultV2>,
|
|
784
|
+
): Promise<PolicyAdmissionResultV2> {
|
|
785
|
+
const result = this.admissionTail.then(operation);
|
|
786
|
+
this.admissionTail = result.then(
|
|
787
|
+
(): void => {},
|
|
788
|
+
(_reason: unknown): void => {},
|
|
789
|
+
);
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
ingest(entry: unknown): Promise<PolicyAdmissionResultV2> {
|
|
794
|
+
return this.enqueueAdmission(() => this.ingestOne(entry));
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
retryUnavailable(): Promise<PolicyAdmissionResultV2> {
|
|
798
|
+
return this.enqueueAdmission(() => this.retryUnavailableOne());
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private async ingestOne(entry: unknown): Promise<PolicyAdmissionResultV2> {
|
|
802
|
+
let snapshot: ValidatedPolicySnapshotV2;
|
|
803
|
+
try {
|
|
804
|
+
snapshot = await authenticatePolicySnapshotEntryV2(
|
|
805
|
+
entry,
|
|
806
|
+
this.descriptor,
|
|
807
|
+
);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
return this.result("rejected", validationMessage(error));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (this.fork !== undefined) {
|
|
813
|
+
this.observeAfterFork(snapshot);
|
|
814
|
+
return this.result(
|
|
815
|
+
"halted",
|
|
816
|
+
"Policy reducer is halted by authority equivocation",
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
this.retainCanonicalHeadEntry(snapshot);
|
|
820
|
+
|
|
821
|
+
if (this.unavailable !== undefined) {
|
|
822
|
+
if (this.acceptedHead?.digestKey === snapshot.digestKey) {
|
|
823
|
+
return this.result("unavailable", this.unavailable.reason);
|
|
824
|
+
}
|
|
825
|
+
const existingPending = this.pending.get(snapshot.digestKey);
|
|
826
|
+
if (existingPending !== undefined) {
|
|
827
|
+
this.addPending(snapshot, existingPending.missingParentDigest);
|
|
828
|
+
return this.result("unavailable", this.unavailable.reason);
|
|
829
|
+
}
|
|
830
|
+
const retention = this.addPending(
|
|
831
|
+
snapshot,
|
|
832
|
+
this.unavailable.acceptedAncestorDigest,
|
|
833
|
+
);
|
|
834
|
+
return this.unavailableResult(retention);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const existingPending = this.pending.get(snapshot.digestKey);
|
|
838
|
+
if (existingPending !== undefined) {
|
|
839
|
+
this.addPending(snapshot, existingPending.missingParentDigest);
|
|
840
|
+
return this.result("pending", "Policy snapshot is already pending");
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const evaluation = await this.evaluate(snapshot);
|
|
844
|
+
if (evaluation.status === "reject") {
|
|
845
|
+
return this.result("rejected", evaluation.reason);
|
|
846
|
+
}
|
|
847
|
+
if (evaluation.status === "duplicate") {
|
|
848
|
+
return this.result("duplicate");
|
|
849
|
+
}
|
|
850
|
+
if (evaluation.status === "fork") {
|
|
851
|
+
this.setFork(evaluation);
|
|
852
|
+
return this.forkedResult();
|
|
853
|
+
}
|
|
854
|
+
if (evaluation.status === "unavailable") {
|
|
855
|
+
const retention = this.enterUnavailable(snapshot, evaluation);
|
|
856
|
+
return this.unavailableResult(retention);
|
|
857
|
+
}
|
|
858
|
+
if (evaluation.status === "missing") {
|
|
859
|
+
const pending = this.addPending(snapshot, evaluation.digest);
|
|
860
|
+
return this.result(
|
|
861
|
+
pending.retained ? "pending" : "capacity",
|
|
862
|
+
pending.retained
|
|
863
|
+
? "Policy parent is missing"
|
|
864
|
+
: "Policy pending capacity did not retain this candidate",
|
|
865
|
+
pending.evictedPolicyDigests,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
this.project(snapshot);
|
|
870
|
+
return (
|
|
871
|
+
this.completedDrainResult(await this.drainPending()) ??
|
|
872
|
+
this.result("accepted")
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
private async retryUnavailableOne(): Promise<PolicyAdmissionResultV2> {
|
|
877
|
+
if (this.fork !== undefined) {
|
|
878
|
+
return this.result(
|
|
879
|
+
"halted",
|
|
880
|
+
"Policy reducer is halted by authority equivocation",
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
const unavailable = this.unavailable;
|
|
884
|
+
if (unavailable === undefined) {
|
|
885
|
+
return this.result("duplicate", "Policy reducer is not unavailable");
|
|
886
|
+
}
|
|
887
|
+
const pending = this.pending.get(unavailable.candidateDigestKey);
|
|
888
|
+
if (pending === undefined) {
|
|
889
|
+
return this.result(
|
|
890
|
+
"capacity",
|
|
891
|
+
"Unavailable comparison candidate is not retained; re-ingest it before retrying",
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const evaluation = await this.evaluate(pending.snapshot);
|
|
896
|
+
if (evaluation.status === "unavailable") {
|
|
897
|
+
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
898
|
+
this.unavailable = {
|
|
899
|
+
acceptedAncestorDigest: copyBytes(evaluation.digest),
|
|
900
|
+
candidateDigestKey: pending.snapshot.digestKey,
|
|
901
|
+
reason: boundedUnavailableReason(evaluation.reason),
|
|
902
|
+
};
|
|
903
|
+
return this.result("unavailable", this.unavailable.reason);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
this.unavailable = undefined;
|
|
907
|
+
let status: PolicyAdmissionStatusV2;
|
|
908
|
+
let reason: string | undefined;
|
|
909
|
+
if (evaluation.status === "missing") {
|
|
910
|
+
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
911
|
+
status = "pending";
|
|
912
|
+
reason = "Policy parent is missing";
|
|
913
|
+
} else {
|
|
914
|
+
this.pending.delete(pending.snapshot.digestKey);
|
|
915
|
+
if (evaluation.status === "fork") {
|
|
916
|
+
this.setFork(evaluation);
|
|
917
|
+
return this.forkedResult();
|
|
918
|
+
}
|
|
919
|
+
if (evaluation.status === "accept") {
|
|
920
|
+
this.project(pending.snapshot);
|
|
921
|
+
status = "accepted";
|
|
922
|
+
} else if (evaluation.status === "duplicate") {
|
|
923
|
+
status = "duplicate";
|
|
924
|
+
} else {
|
|
925
|
+
status = "rejected";
|
|
926
|
+
reason = evaluation.reason;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return (
|
|
931
|
+
this.completedDrainResult(await this.drainPending()) ??
|
|
932
|
+
this.result(status, reason)
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
}
|