@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,645 @@
|
|
|
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 { NetworkDescriptorV2, PolicySnapshotBodyV2, PolicySubjectBindingV2, TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS, assertNetworkDescriptorV2, decodePolicySnapshotBodyV2, digestPolicySnapshotBodyV2, } from "./v2.js";
|
|
6
|
+
/**
|
|
7
|
+
* Internal policy reducer for the non-activatable TrustedNetwork v2 scaffold.
|
|
8
|
+
*
|
|
9
|
+
* The reducer intentionally is not exported from the package entry point. It
|
|
10
|
+
* retains one accepted snapshot, a bounded pending working set, and (after
|
|
11
|
+
* equivocation) two child proofs. Historical snapshots are supplied by the
|
|
12
|
+
* resolver instead of being accumulated in memory.
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_MAX_PENDING_POLICIES_V2 = 64;
|
|
15
|
+
const DEFAULT_MAX_PENDING_POLICY_BYTES_V2 = 256 * 1024;
|
|
16
|
+
const PENDING_POLICY_ACCOUNTING_OVERHEAD_V2 = 64;
|
|
17
|
+
const MAX_UNAVAILABLE_REASON_LENGTH_V2 = 512;
|
|
18
|
+
const copyBytes = (bytes) => Uint8Array.from(bytes);
|
|
19
|
+
const bytesKey = (bytes) => {
|
|
20
|
+
let key = "";
|
|
21
|
+
for (const byte of bytes)
|
|
22
|
+
key += byte.toString(16).padStart(2, "0");
|
|
23
|
+
return key;
|
|
24
|
+
};
|
|
25
|
+
const compareKeys = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
26
|
+
const copyPublicKey = (key) => deserialize(serialize(key), PublicSignKey);
|
|
27
|
+
const publicKeyId = (key) => bytesKey(serialize(key));
|
|
28
|
+
const copyBinding = (binding) => new PolicySubjectBindingV2({
|
|
29
|
+
signingKey: copyPublicKey(binding.signingKey),
|
|
30
|
+
roles: binding.roles,
|
|
31
|
+
});
|
|
32
|
+
const copyBody = (body) => new PolicySnapshotBodyV2({
|
|
33
|
+
networkId: copyBytes(body.networkId),
|
|
34
|
+
sequence: body.sequence,
|
|
35
|
+
previousPolicyDigest: copyBytes(body.previousPolicyDigest),
|
|
36
|
+
bindings: body.bindings.map(copyBinding),
|
|
37
|
+
});
|
|
38
|
+
const copySnapshot = (snapshot) => ({
|
|
39
|
+
body: copyBody(snapshot.body),
|
|
40
|
+
digest: copyBytes(snapshot.digest),
|
|
41
|
+
digestKey: snapshot.digestKey,
|
|
42
|
+
entryBytes: copyBytes(snapshot.entryBytes),
|
|
43
|
+
accountedBytes: snapshot.accountedBytes,
|
|
44
|
+
});
|
|
45
|
+
const validationMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
46
|
+
const boundedUnavailableReason = (reason) => reason.slice(0, MAX_UNAVAILABLE_REASON_LENGTH_V2);
|
|
47
|
+
export const authenticatePolicySnapshotEntryV2 = async (entry, descriptor) => {
|
|
48
|
+
assertNetworkDescriptorV2(descriptor);
|
|
49
|
+
if (!(entry instanceof EntryV0)) {
|
|
50
|
+
throw new Error("Policy snapshot must use EntryV0");
|
|
51
|
+
}
|
|
52
|
+
if (!(entry._meta instanceof DecryptedThing)) {
|
|
53
|
+
throw new Error("Policy snapshot metadata must be public");
|
|
54
|
+
}
|
|
55
|
+
if (!(entry._payload instanceof DecryptedThing)) {
|
|
56
|
+
throw new Error("Policy snapshot payload must be public");
|
|
57
|
+
}
|
|
58
|
+
if (entry._signatures === undefined ||
|
|
59
|
+
entry._signatures.signatures.length !== 1) {
|
|
60
|
+
throw new Error("Policy snapshot must contain exactly one signature");
|
|
61
|
+
}
|
|
62
|
+
if (!(entry._signatures.signatures[0] instanceof DecryptedThing)) {
|
|
63
|
+
throw new Error("Policy snapshot signature must be public");
|
|
64
|
+
}
|
|
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
|
+
authenticatedEntry.init({ encoding: NO_ENCODING });
|
|
71
|
+
const signatures = await authenticatedEntry.getSignatures();
|
|
72
|
+
if (signatures.length !== 1) {
|
|
73
|
+
throw new Error("Policy snapshot must resolve exactly one signature");
|
|
74
|
+
}
|
|
75
|
+
const signature = signatures[0];
|
|
76
|
+
if (!equals(serialize(signature.publicKey), serialize(descriptor.policyAuthority))) {
|
|
77
|
+
throw new Error("Policy snapshot signer is not the policy authority");
|
|
78
|
+
}
|
|
79
|
+
if (!(await verify(signature, authenticatedEntry.getSignableBytes()))) {
|
|
80
|
+
throw new Error("Policy snapshot authority signature is invalid");
|
|
81
|
+
}
|
|
82
|
+
const payload = await authenticatedEntry.getPayloadValue();
|
|
83
|
+
if (!(payload instanceof Uint8Array)) {
|
|
84
|
+
throw new Error("Policy snapshot payload must contain canonical body bytes");
|
|
85
|
+
}
|
|
86
|
+
const body = decodePolicySnapshotBodyV2(copyBytes(payload), descriptor);
|
|
87
|
+
const digest = digestPolicySnapshotBodyV2(body);
|
|
88
|
+
return {
|
|
89
|
+
body: copyBody(body),
|
|
90
|
+
digest: copyBytes(digest),
|
|
91
|
+
digestKey: bytesKey(digest),
|
|
92
|
+
entryBytes,
|
|
93
|
+
accountedBytes: entryBytes.byteLength +
|
|
94
|
+
serialize(body).byteLength +
|
|
95
|
+
PENDING_POLICY_ACCOUNTING_OVERHEAD_V2,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
const projectionFromSnapshot = (snapshot) => ({
|
|
99
|
+
sequence: snapshot.body.sequence,
|
|
100
|
+
digest: copyBytes(snapshot.digest),
|
|
101
|
+
bindings: snapshot.body.bindings.map(copyBinding),
|
|
102
|
+
});
|
|
103
|
+
const forkProofFromSnapshot = (snapshot) => ({
|
|
104
|
+
sequence: snapshot.body.sequence,
|
|
105
|
+
digest: copyBytes(snapshot.digest),
|
|
106
|
+
entryBytes: copyBytes(snapshot.entryBytes),
|
|
107
|
+
});
|
|
108
|
+
const copyForkChildProof = (proof) => ({
|
|
109
|
+
sequence: proof.sequence,
|
|
110
|
+
digest: copyBytes(proof.digest),
|
|
111
|
+
entryBytes: copyBytes(proof.entryBytes),
|
|
112
|
+
});
|
|
113
|
+
const compareForkChildProofs = (left, right) => {
|
|
114
|
+
const digestOrder = compare(left.digest, right.digest);
|
|
115
|
+
return digestOrder === 0
|
|
116
|
+
? compare(left.entryBytes, right.entryBytes)
|
|
117
|
+
: digestOrder;
|
|
118
|
+
};
|
|
119
|
+
const copyProjection = (projection) => ({
|
|
120
|
+
sequence: projection.sequence,
|
|
121
|
+
digest: copyBytes(projection.digest),
|
|
122
|
+
bindings: projection.bindings.map(copyBinding),
|
|
123
|
+
});
|
|
124
|
+
const copyForkEvidence = (evidence) => ({
|
|
125
|
+
commonParent: copyProjection(evidence.commonParent),
|
|
126
|
+
children: [
|
|
127
|
+
copyForkChildProof(evidence.children[0]),
|
|
128
|
+
copyForkChildProof(evidence.children[1]),
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
export class TrustedNetworkV2PolicyReducer {
|
|
132
|
+
descriptor;
|
|
133
|
+
resolvePolicyEntry;
|
|
134
|
+
maxPending;
|
|
135
|
+
maxPendingPolicyBytes;
|
|
136
|
+
acceptedHead;
|
|
137
|
+
projectedRoles = new Map();
|
|
138
|
+
pending = new Map();
|
|
139
|
+
// Recovery is bound to this exact candidate. Unrelated admissions may enter
|
|
140
|
+
// the bounded pending set but can never restore ACTIVE authorization.
|
|
141
|
+
unavailable;
|
|
142
|
+
fork;
|
|
143
|
+
admissionTail = Promise.resolve();
|
|
144
|
+
constructor(properties) {
|
|
145
|
+
assertNetworkDescriptorV2(properties.descriptor);
|
|
146
|
+
const maxPending = properties.maxPending ?? DEFAULT_MAX_PENDING_POLICIES_V2;
|
|
147
|
+
if (!Number.isSafeInteger(maxPending) || maxPending < 1) {
|
|
148
|
+
throw new Error("maxPending must be a positive safe integer");
|
|
149
|
+
}
|
|
150
|
+
const maxPendingPolicyBytes = properties.maxPendingPolicyBytes ?? DEFAULT_MAX_PENDING_POLICY_BYTES_V2;
|
|
151
|
+
if (!Number.isSafeInteger(maxPendingPolicyBytes) ||
|
|
152
|
+
maxPendingPolicyBytes < 1) {
|
|
153
|
+
throw new Error("maxPendingPolicyBytes must be a positive safe integer");
|
|
154
|
+
}
|
|
155
|
+
this.descriptor = deserialize(serialize(properties.descriptor), NetworkDescriptorV2);
|
|
156
|
+
this.resolvePolicyEntry = properties.resolvePolicyEntry;
|
|
157
|
+
this.maxPending = maxPending;
|
|
158
|
+
this.maxPendingPolicyBytes = maxPendingPolicyBytes;
|
|
159
|
+
}
|
|
160
|
+
get state() {
|
|
161
|
+
if (this.fork !== undefined)
|
|
162
|
+
return "FORKED";
|
|
163
|
+
if (this.unavailable !== undefined)
|
|
164
|
+
return "UNAVAILABLE";
|
|
165
|
+
return this.acceptedHead === undefined ? "EMPTY" : "ACTIVE";
|
|
166
|
+
}
|
|
167
|
+
get head() {
|
|
168
|
+
return this.acceptedHead === undefined
|
|
169
|
+
? undefined
|
|
170
|
+
: projectionFromSnapshot(this.acceptedHead);
|
|
171
|
+
}
|
|
172
|
+
get forkEvidence() {
|
|
173
|
+
return this.fork === undefined ? undefined : copyForkEvidence(this.fork);
|
|
174
|
+
}
|
|
175
|
+
get pendingCount() {
|
|
176
|
+
return this.pending.size;
|
|
177
|
+
}
|
|
178
|
+
get pendingBytes() {
|
|
179
|
+
let total = 0;
|
|
180
|
+
for (const { snapshot } of this.pending.values()) {
|
|
181
|
+
total += snapshot.accountedBytes;
|
|
182
|
+
}
|
|
183
|
+
return total;
|
|
184
|
+
}
|
|
185
|
+
get pendingDigests() {
|
|
186
|
+
return [...this.pending.values()]
|
|
187
|
+
.sort((a, b) => compareKeys(a.snapshot.digestKey, b.snapshot.digestKey))
|
|
188
|
+
.map(({ snapshot }) => copyBytes(snapshot.digest));
|
|
189
|
+
}
|
|
190
|
+
rolesFor(subject) {
|
|
191
|
+
return this.projectedRoles.get(publicKeyId(subject)) ?? 0;
|
|
192
|
+
}
|
|
193
|
+
isAuthorized(subject, roles) {
|
|
194
|
+
if (this.state !== "ACTIVE" ||
|
|
195
|
+
!Number.isInteger(roles) ||
|
|
196
|
+
roles === 0 ||
|
|
197
|
+
(roles & ~TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS) !== 0) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return (this.rolesFor(subject) & roles) === roles;
|
|
201
|
+
}
|
|
202
|
+
fetchHints() {
|
|
203
|
+
const unique = new Map();
|
|
204
|
+
for (const { missingParentDigest } of this.pending.values()) {
|
|
205
|
+
unique.set(bytesKey(missingParentDigest), missingParentDigest);
|
|
206
|
+
}
|
|
207
|
+
if (this.unavailable !== undefined) {
|
|
208
|
+
unique.set(bytesKey(this.unavailable.acceptedAncestorDigest), this.unavailable.acceptedAncestorDigest);
|
|
209
|
+
}
|
|
210
|
+
return [...unique.entries()]
|
|
211
|
+
.sort(([a], [b]) => compareKeys(a, b))
|
|
212
|
+
.map(([, digest]) => ({
|
|
213
|
+
kind: "policy-parent",
|
|
214
|
+
digest: copyBytes(digest),
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
result(status, reason, evictedPolicyDigests) {
|
|
218
|
+
return {
|
|
219
|
+
status,
|
|
220
|
+
reason,
|
|
221
|
+
head: this.head,
|
|
222
|
+
fetchHints: this.fetchHints(),
|
|
223
|
+
pendingCount: this.pending.size,
|
|
224
|
+
pendingBytes: this.pendingBytes,
|
|
225
|
+
evictedPolicyDigests: evictedPolicyDigests === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: evictedPolicyDigests.map(copyBytes),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
forkedResult() {
|
|
231
|
+
return this.result("forked", "Policy authority signed competing children");
|
|
232
|
+
}
|
|
233
|
+
unavailableResult(retention) {
|
|
234
|
+
const blockedCandidateRetained = this.unavailable !== undefined &&
|
|
235
|
+
this.pending.has(this.unavailable.candidateDigestKey);
|
|
236
|
+
const recoverable = (retention?.retained ?? true) && blockedCandidateRetained;
|
|
237
|
+
const reason = recoverable
|
|
238
|
+
? this.unavailable.reason
|
|
239
|
+
: blockedCandidateRetained
|
|
240
|
+
? "Policy pending capacity did not retain this candidate"
|
|
241
|
+
: "Policy reducer remains unavailable because pending capacity did not retain the blocked candidate";
|
|
242
|
+
return this.result(recoverable ? "unavailable" : "capacity", reason, retention?.evictedPolicyDigests);
|
|
243
|
+
}
|
|
244
|
+
completedDrainResult(outcome) {
|
|
245
|
+
if (outcome?.status === "forked")
|
|
246
|
+
return this.forkedResult();
|
|
247
|
+
return outcome?.status === "unavailable"
|
|
248
|
+
? this.unavailableResult(outcome)
|
|
249
|
+
: undefined;
|
|
250
|
+
}
|
|
251
|
+
project(snapshot) {
|
|
252
|
+
this.acceptedHead = copySnapshot(snapshot);
|
|
253
|
+
this.projectedRoles = new Map(snapshot.body.bindings.map((binding) => [
|
|
254
|
+
publicKeyId(binding.signingKey),
|
|
255
|
+
binding.roles,
|
|
256
|
+
]));
|
|
257
|
+
}
|
|
258
|
+
retainCanonicalHeadEntry(snapshot) {
|
|
259
|
+
if (this.acceptedHead?.digestKey === snapshot.digestKey &&
|
|
260
|
+
compare(snapshot.entryBytes, this.acceptedHead.entryBytes) < 0) {
|
|
261
|
+
this.acceptedHead = copySnapshot(snapshot);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async resolveSnapshot(digest, cache) {
|
|
265
|
+
const digestKey = bytesKey(digest);
|
|
266
|
+
if (this.acceptedHead?.digestKey === digestKey) {
|
|
267
|
+
return copySnapshot(this.acceptedHead);
|
|
268
|
+
}
|
|
269
|
+
const pending = this.pending.get(digestKey);
|
|
270
|
+
if (pending !== undefined)
|
|
271
|
+
return copySnapshot(pending.snapshot);
|
|
272
|
+
let resolution = cache?.get(digestKey);
|
|
273
|
+
if (resolution === undefined) {
|
|
274
|
+
resolution = (async () => {
|
|
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
|
+
})();
|
|
284
|
+
cache?.set(digestKey, resolution);
|
|
285
|
+
}
|
|
286
|
+
const snapshot = await resolution;
|
|
287
|
+
return snapshot === undefined ? undefined : copySnapshot(snapshot);
|
|
288
|
+
}
|
|
289
|
+
async parentOf(child, cache) {
|
|
290
|
+
if (child.body.sequence === 0n) {
|
|
291
|
+
return {
|
|
292
|
+
status: "reject",
|
|
293
|
+
digest: copyBytes(child.body.previousPolicyDigest),
|
|
294
|
+
reason: "Genesis policy has no parent",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
let parent;
|
|
298
|
+
try {
|
|
299
|
+
parent = await this.resolveSnapshot(child.body.previousPolicyDigest, cache);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
return {
|
|
303
|
+
status: "reject",
|
|
304
|
+
digest: copyBytes(child.body.previousPolicyDigest),
|
|
305
|
+
reason: `Policy parent validation failed: ${validationMessage(error)}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (parent === undefined) {
|
|
309
|
+
return {
|
|
310
|
+
status: "missing",
|
|
311
|
+
digest: copyBytes(child.body.previousPolicyDigest),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (child.body.sequence !== parent.body.sequence + 1n) {
|
|
315
|
+
return {
|
|
316
|
+
status: "reject",
|
|
317
|
+
digest: copyBytes(child.body.previousPolicyDigest),
|
|
318
|
+
reason: "Policy sequence is not contiguous with its parent",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return { status: "found", parent };
|
|
322
|
+
}
|
|
323
|
+
acceptedAncestryUnavailable(resolution) {
|
|
324
|
+
return {
|
|
325
|
+
status: "unavailable",
|
|
326
|
+
digest: copyBytes(resolution.digest),
|
|
327
|
+
reason: resolution.status === "missing"
|
|
328
|
+
? "Accepted policy ancestry is unavailable from the resolver"
|
|
329
|
+
: `Accepted policy ancestry validation failed: ${resolution.reason}`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async evaluate(candidate) {
|
|
333
|
+
const resolutionCache = new Map();
|
|
334
|
+
if (this.acceptedHead === undefined) {
|
|
335
|
+
let cursor = candidate;
|
|
336
|
+
while (cursor.body.sequence !== 0n) {
|
|
337
|
+
const parent = await this.parentOf(cursor, resolutionCache);
|
|
338
|
+
if (parent.status !== "found")
|
|
339
|
+
return parent;
|
|
340
|
+
cursor = parent.parent;
|
|
341
|
+
}
|
|
342
|
+
return { status: "accept" };
|
|
343
|
+
}
|
|
344
|
+
let candidateCursor = candidate;
|
|
345
|
+
let acceptedCursor = this.acceptedHead;
|
|
346
|
+
let candidateChild;
|
|
347
|
+
let acceptedChild;
|
|
348
|
+
while (candidateCursor.body.sequence > acceptedCursor.body.sequence) {
|
|
349
|
+
candidateChild = candidateCursor;
|
|
350
|
+
const parent = await this.parentOf(candidateCursor, resolutionCache);
|
|
351
|
+
if (parent.status !== "found")
|
|
352
|
+
return parent;
|
|
353
|
+
candidateCursor = parent.parent;
|
|
354
|
+
}
|
|
355
|
+
while (acceptedCursor.body.sequence > candidateCursor.body.sequence) {
|
|
356
|
+
acceptedChild = acceptedCursor;
|
|
357
|
+
const parent = await this.parentOf(acceptedCursor, resolutionCache);
|
|
358
|
+
if (parent.status !== "found") {
|
|
359
|
+
return this.acceptedAncestryUnavailable(parent);
|
|
360
|
+
}
|
|
361
|
+
acceptedCursor = parent.parent;
|
|
362
|
+
}
|
|
363
|
+
while (!equals(candidateCursor.digest, acceptedCursor.digest)) {
|
|
364
|
+
if (candidateCursor.body.sequence === 0n ||
|
|
365
|
+
acceptedCursor.body.sequence === 0n) {
|
|
366
|
+
return {
|
|
367
|
+
status: "reject",
|
|
368
|
+
reason: "Policy branches do not share the descriptor genesis",
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
candidateChild = candidateCursor;
|
|
372
|
+
acceptedChild = acceptedCursor;
|
|
373
|
+
const [candidateParent, acceptedParent] = await Promise.all([
|
|
374
|
+
this.parentOf(candidateCursor, resolutionCache),
|
|
375
|
+
this.parentOf(acceptedCursor, resolutionCache),
|
|
376
|
+
]);
|
|
377
|
+
if (acceptedParent.status !== "found") {
|
|
378
|
+
return this.acceptedAncestryUnavailable(acceptedParent);
|
|
379
|
+
}
|
|
380
|
+
if (candidateParent.status !== "found")
|
|
381
|
+
return candidateParent;
|
|
382
|
+
candidateCursor = candidateParent.parent;
|
|
383
|
+
acceptedCursor = acceptedParent.parent;
|
|
384
|
+
}
|
|
385
|
+
if (candidateChild === undefined) {
|
|
386
|
+
return { status: "duplicate" };
|
|
387
|
+
}
|
|
388
|
+
if (acceptedChild === undefined) {
|
|
389
|
+
return { status: "accept" };
|
|
390
|
+
}
|
|
391
|
+
if (equals(candidateChild.digest, acceptedChild.digest)) {
|
|
392
|
+
return { status: "duplicate" };
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
status: "fork",
|
|
396
|
+
commonParent: candidateCursor,
|
|
397
|
+
candidateChild,
|
|
398
|
+
acceptedChild,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
setFork(evaluation) {
|
|
402
|
+
this.project(evaluation.commonParent);
|
|
403
|
+
const children = [
|
|
404
|
+
forkProofFromSnapshot(evaluation.candidateChild),
|
|
405
|
+
forkProofFromSnapshot(evaluation.acceptedChild),
|
|
406
|
+
].sort(compareForkChildProofs);
|
|
407
|
+
this.fork = {
|
|
408
|
+
commonParent: projectionFromSnapshot(evaluation.commonParent),
|
|
409
|
+
children,
|
|
410
|
+
};
|
|
411
|
+
this.unavailable = undefined;
|
|
412
|
+
this.pending.clear();
|
|
413
|
+
}
|
|
414
|
+
retainCanonicalForkChild(snapshot) {
|
|
415
|
+
if (this.fork === undefined)
|
|
416
|
+
return;
|
|
417
|
+
const byDigest = new Map();
|
|
418
|
+
for (const proof of [
|
|
419
|
+
...this.fork.children,
|
|
420
|
+
forkProofFromSnapshot(snapshot),
|
|
421
|
+
]) {
|
|
422
|
+
const digestKey = bytesKey(proof.digest);
|
|
423
|
+
const retained = byDigest.get(digestKey);
|
|
424
|
+
if (retained === undefined ||
|
|
425
|
+
compare(proof.entryBytes, retained.entryBytes) < 0) {
|
|
426
|
+
byDigest.set(digestKey, copyForkChildProof(proof));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const canonical = [...byDigest.values()]
|
|
430
|
+
.sort(compareForkChildProofs)
|
|
431
|
+
.slice(0, 2);
|
|
432
|
+
if (canonical.length !== 2)
|
|
433
|
+
return;
|
|
434
|
+
this.fork.children = [canonical[0], canonical[1]];
|
|
435
|
+
}
|
|
436
|
+
observeAfterFork(snapshot) {
|
|
437
|
+
if (this.fork === undefined || this.acceptedHead === undefined)
|
|
438
|
+
return;
|
|
439
|
+
const commonParent = this.acceptedHead;
|
|
440
|
+
if (snapshot.body.sequence !== commonParent.body.sequence + 1n ||
|
|
441
|
+
!equals(snapshot.body.previousPolicyDigest, commonParent.digest)) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
// This bounded kernel deliberately retains only the canonical two direct
|
|
445
|
+
// child proofs. Durable storage of every authenticated fork observation is
|
|
446
|
+
// an outer-layer responsibility for a later integration slice.
|
|
447
|
+
this.retainCanonicalForkChild(snapshot);
|
|
448
|
+
}
|
|
449
|
+
addPending(snapshot, missingParentDigest) {
|
|
450
|
+
const existing = this.pending.get(snapshot.digestKey);
|
|
451
|
+
if (existing !== undefined) {
|
|
452
|
+
existing.missingParentDigest = copyBytes(missingParentDigest);
|
|
453
|
+
if (snapshot.accountedBytes <= this.maxPendingPolicyBytes &&
|
|
454
|
+
compare(snapshot.entryBytes, existing.snapshot.entryBytes) < 0) {
|
|
455
|
+
existing.snapshot = copySnapshot(snapshot);
|
|
456
|
+
}
|
|
457
|
+
return { retained: true, evictedPolicyDigests: [] };
|
|
458
|
+
}
|
|
459
|
+
if (snapshot.accountedBytes > this.maxPendingPolicyBytes) {
|
|
460
|
+
return {
|
|
461
|
+
retained: false,
|
|
462
|
+
evictedPolicyDigests: [copyBytes(snapshot.digest)],
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
this.pending.set(snapshot.digestKey, {
|
|
466
|
+
snapshot: copySnapshot(snapshot),
|
|
467
|
+
missingParentDigest: copyBytes(missingParentDigest),
|
|
468
|
+
});
|
|
469
|
+
const ordered = [...this.pending.entries()].sort(([a], [b]) => compareKeys(a, b));
|
|
470
|
+
const retainedKeys = new Set(ordered.slice(0, this.maxPending).map(([key]) => key));
|
|
471
|
+
const evictedPolicyDigests = [];
|
|
472
|
+
for (const [key, pending] of ordered) {
|
|
473
|
+
if (retainedKeys.has(key))
|
|
474
|
+
continue;
|
|
475
|
+
evictedPolicyDigests.push(copyBytes(pending.snapshot.digest));
|
|
476
|
+
this.pending.delete(key);
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
retained: retainedKeys.has(snapshot.digestKey),
|
|
480
|
+
evictedPolicyDigests,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
enterUnavailable(snapshot, evaluation) {
|
|
484
|
+
const retention = this.addPending(snapshot, evaluation.digest);
|
|
485
|
+
this.unavailable = {
|
|
486
|
+
acceptedAncestorDigest: copyBytes(evaluation.digest),
|
|
487
|
+
candidateDigestKey: snapshot.digestKey,
|
|
488
|
+
reason: boundedUnavailableReason(evaluation.reason),
|
|
489
|
+
};
|
|
490
|
+
return retention;
|
|
491
|
+
}
|
|
492
|
+
async drainPending() {
|
|
493
|
+
let accepted = false;
|
|
494
|
+
let progress = true;
|
|
495
|
+
while (progress &&
|
|
496
|
+
this.pending.size > 0 &&
|
|
497
|
+
this.fork === undefined &&
|
|
498
|
+
this.unavailable === undefined) {
|
|
499
|
+
progress = false;
|
|
500
|
+
const ordered = [...this.pending.values()].sort((a, b) => compareKeys(a.snapshot.digestKey, b.snapshot.digestKey));
|
|
501
|
+
for (const pending of ordered) {
|
|
502
|
+
if (!this.pending.has(pending.snapshot.digestKey))
|
|
503
|
+
continue;
|
|
504
|
+
const evaluation = await this.evaluate(pending.snapshot);
|
|
505
|
+
if (evaluation.status === "missing") {
|
|
506
|
+
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (evaluation.status === "unavailable") {
|
|
510
|
+
const retention = this.enterUnavailable(pending.snapshot, evaluation);
|
|
511
|
+
return { status: "unavailable", ...retention };
|
|
512
|
+
}
|
|
513
|
+
this.pending.delete(pending.snapshot.digestKey);
|
|
514
|
+
progress = true;
|
|
515
|
+
if (evaluation.status === "accept") {
|
|
516
|
+
this.project(pending.snapshot);
|
|
517
|
+
accepted = true;
|
|
518
|
+
}
|
|
519
|
+
else if (evaluation.status === "fork") {
|
|
520
|
+
this.setFork(evaluation);
|
|
521
|
+
return { status: "forked" };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return accepted ? { status: "accepted" } : undefined;
|
|
526
|
+
}
|
|
527
|
+
enqueueAdmission(operation) {
|
|
528
|
+
const result = this.admissionTail.then(operation);
|
|
529
|
+
this.admissionTail = result.then(() => { }, (_reason) => { });
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
ingest(entry) {
|
|
533
|
+
return this.enqueueAdmission(() => this.ingestOne(entry));
|
|
534
|
+
}
|
|
535
|
+
retryUnavailable() {
|
|
536
|
+
return this.enqueueAdmission(() => this.retryUnavailableOne());
|
|
537
|
+
}
|
|
538
|
+
async ingestOne(entry) {
|
|
539
|
+
let snapshot;
|
|
540
|
+
try {
|
|
541
|
+
snapshot = await authenticatePolicySnapshotEntryV2(entry, this.descriptor);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
return this.result("rejected", validationMessage(error));
|
|
545
|
+
}
|
|
546
|
+
if (this.fork !== undefined) {
|
|
547
|
+
this.observeAfterFork(snapshot);
|
|
548
|
+
return this.result("halted", "Policy reducer is halted by authority equivocation");
|
|
549
|
+
}
|
|
550
|
+
this.retainCanonicalHeadEntry(snapshot);
|
|
551
|
+
if (this.unavailable !== undefined) {
|
|
552
|
+
if (this.acceptedHead?.digestKey === snapshot.digestKey) {
|
|
553
|
+
return this.result("unavailable", this.unavailable.reason);
|
|
554
|
+
}
|
|
555
|
+
const existingPending = this.pending.get(snapshot.digestKey);
|
|
556
|
+
if (existingPending !== undefined) {
|
|
557
|
+
this.addPending(snapshot, existingPending.missingParentDigest);
|
|
558
|
+
return this.result("unavailable", this.unavailable.reason);
|
|
559
|
+
}
|
|
560
|
+
const retention = this.addPending(snapshot, this.unavailable.acceptedAncestorDigest);
|
|
561
|
+
return this.unavailableResult(retention);
|
|
562
|
+
}
|
|
563
|
+
const existingPending = this.pending.get(snapshot.digestKey);
|
|
564
|
+
if (existingPending !== undefined) {
|
|
565
|
+
this.addPending(snapshot, existingPending.missingParentDigest);
|
|
566
|
+
return this.result("pending", "Policy snapshot is already pending");
|
|
567
|
+
}
|
|
568
|
+
const evaluation = await this.evaluate(snapshot);
|
|
569
|
+
if (evaluation.status === "reject") {
|
|
570
|
+
return this.result("rejected", evaluation.reason);
|
|
571
|
+
}
|
|
572
|
+
if (evaluation.status === "duplicate") {
|
|
573
|
+
return this.result("duplicate");
|
|
574
|
+
}
|
|
575
|
+
if (evaluation.status === "fork") {
|
|
576
|
+
this.setFork(evaluation);
|
|
577
|
+
return this.forkedResult();
|
|
578
|
+
}
|
|
579
|
+
if (evaluation.status === "unavailable") {
|
|
580
|
+
const retention = this.enterUnavailable(snapshot, evaluation);
|
|
581
|
+
return this.unavailableResult(retention);
|
|
582
|
+
}
|
|
583
|
+
if (evaluation.status === "missing") {
|
|
584
|
+
const pending = this.addPending(snapshot, evaluation.digest);
|
|
585
|
+
return this.result(pending.retained ? "pending" : "capacity", pending.retained
|
|
586
|
+
? "Policy parent is missing"
|
|
587
|
+
: "Policy pending capacity did not retain this candidate", pending.evictedPolicyDigests);
|
|
588
|
+
}
|
|
589
|
+
this.project(snapshot);
|
|
590
|
+
return (this.completedDrainResult(await this.drainPending()) ??
|
|
591
|
+
this.result("accepted"));
|
|
592
|
+
}
|
|
593
|
+
async retryUnavailableOne() {
|
|
594
|
+
if (this.fork !== undefined) {
|
|
595
|
+
return this.result("halted", "Policy reducer is halted by authority equivocation");
|
|
596
|
+
}
|
|
597
|
+
const unavailable = this.unavailable;
|
|
598
|
+
if (unavailable === undefined) {
|
|
599
|
+
return this.result("duplicate", "Policy reducer is not unavailable");
|
|
600
|
+
}
|
|
601
|
+
const pending = this.pending.get(unavailable.candidateDigestKey);
|
|
602
|
+
if (pending === undefined) {
|
|
603
|
+
return this.result("capacity", "Unavailable comparison candidate is not retained; re-ingest it before retrying");
|
|
604
|
+
}
|
|
605
|
+
const evaluation = await this.evaluate(pending.snapshot);
|
|
606
|
+
if (evaluation.status === "unavailable") {
|
|
607
|
+
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
608
|
+
this.unavailable = {
|
|
609
|
+
acceptedAncestorDigest: copyBytes(evaluation.digest),
|
|
610
|
+
candidateDigestKey: pending.snapshot.digestKey,
|
|
611
|
+
reason: boundedUnavailableReason(evaluation.reason),
|
|
612
|
+
};
|
|
613
|
+
return this.result("unavailable", this.unavailable.reason);
|
|
614
|
+
}
|
|
615
|
+
this.unavailable = undefined;
|
|
616
|
+
let status;
|
|
617
|
+
let reason;
|
|
618
|
+
if (evaluation.status === "missing") {
|
|
619
|
+
pending.missingParentDigest = copyBytes(evaluation.digest);
|
|
620
|
+
status = "pending";
|
|
621
|
+
reason = "Policy parent is missing";
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
this.pending.delete(pending.snapshot.digestKey);
|
|
625
|
+
if (evaluation.status === "fork") {
|
|
626
|
+
this.setFork(evaluation);
|
|
627
|
+
return this.forkedResult();
|
|
628
|
+
}
|
|
629
|
+
if (evaluation.status === "accept") {
|
|
630
|
+
this.project(pending.snapshot);
|
|
631
|
+
status = "accepted";
|
|
632
|
+
}
|
|
633
|
+
else if (evaluation.status === "duplicate") {
|
|
634
|
+
status = "duplicate";
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
status = "rejected";
|
|
638
|
+
reason = evaluation.reason;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return (this.completedDrainResult(await this.drainPending()) ??
|
|
642
|
+
this.result(status, reason));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
//# sourceMappingURL=v2-policy-engine.js.map
|