@peerbit/trusted-network 6.0.128 → 6.0.130
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 +33 -0
- package/dist/src/v2-policy-anchor.d.ts.map +1 -1
- package/dist/src/v2-policy-anchor.js +349 -72
- package/dist/src/v2-policy-anchor.js.map +1 -1
- package/dist/src/v2-policy-engine.d.ts +25 -0
- package/dist/src/v2-policy-engine.d.ts.map +1 -1
- package/dist/src/v2-policy-engine.js +187 -9
- package/dist/src/v2-policy-engine.js.map +1 -1
- package/package.json +7 -7
- package/src/v2-policy-anchor.ts +472 -81
- package/src/v2-policy-engine.ts +268 -8
package/src/v2-policy-engine.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { deserialize, serialize } from "@dao-xyz/borsh";
|
|
2
|
+
import {
|
|
3
|
+
calculateRawCid,
|
|
4
|
+
cidifyString,
|
|
5
|
+
codecMap,
|
|
6
|
+
defaultHasher,
|
|
7
|
+
stringifyCid,
|
|
8
|
+
} from "@peerbit/blocks-interface";
|
|
2
9
|
import { PublicSignKey } from "@peerbit/crypto";
|
|
3
10
|
import { compare, equals } from "uint8arrays";
|
|
4
11
|
import { authenticateCapturedAuthorityEntryV0V2 } from "./v2-authority-entry.js";
|
|
@@ -24,6 +31,8 @@ import {
|
|
|
24
31
|
|
|
25
32
|
/** Internal protocol ceiling; this module is not part of the package root. */
|
|
26
33
|
export const TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES = 64;
|
|
34
|
+
/** Internal ceiling for one canonical raw policy-entry CID. */
|
|
35
|
+
export const TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_CID_CHARACTERS = 128;
|
|
27
36
|
const DEFAULT_MAX_PENDING_POLICIES_V2 = TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES;
|
|
28
37
|
const PENDING_POLICY_ACCOUNTING_OVERHEAD_V2 = 64;
|
|
29
38
|
const MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2 =
|
|
@@ -61,6 +70,33 @@ const isAbortSignalV2 = (value: unknown): value is AbortSignal => {
|
|
|
61
70
|
}
|
|
62
71
|
};
|
|
63
72
|
|
|
73
|
+
/** Capture a possibly structural signal behind a reducer-owned native signal. */
|
|
74
|
+
export const observeAbortSignalV2 = (source?: AbortSignal) => {
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
let installed = false;
|
|
77
|
+
const abort = (): void => controller.abort();
|
|
78
|
+
const dispose = (): void => {
|
|
79
|
+
controller.abort();
|
|
80
|
+
if (!installed) return;
|
|
81
|
+
installed = false;
|
|
82
|
+
try {
|
|
83
|
+
source!.removeEventListener("abort", abort);
|
|
84
|
+
} catch {}
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
if (source !== undefined) {
|
|
88
|
+
if (!isAbortSignalV2(source)) throw new TypeError();
|
|
89
|
+
installed = true;
|
|
90
|
+
source.addEventListener("abort", abort, { once: true });
|
|
91
|
+
if (source.aborted) abort();
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
dispose();
|
|
95
|
+
throw new TypeError("AbortSignal could not be observed");
|
|
96
|
+
}
|
|
97
|
+
return [controller, dispose] as const;
|
|
98
|
+
};
|
|
99
|
+
|
|
64
100
|
const exactUint8ArrayByteLength = (input: unknown): number => {
|
|
65
101
|
if (
|
|
66
102
|
!ARRAY_BUFFER_IS_VIEW(input) ||
|
|
@@ -164,6 +200,21 @@ export type PolicyHeadProjectionV2 = {
|
|
|
164
200
|
bindings: PolicySubjectBindingV2[];
|
|
165
201
|
};
|
|
166
202
|
|
|
203
|
+
export type AuthenticatedExactPolicyEntryV2 = {
|
|
204
|
+
policyEntryCid: string;
|
|
205
|
+
policy: PolicyHeadProjectionV2;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** Opaque, reducer-owned result of exact CID and authority authentication. */
|
|
209
|
+
export type PreparedExactPolicyCandidateV2 =
|
|
210
|
+
Readonly<AuthenticatedExactPolicyEntryV2>;
|
|
211
|
+
|
|
212
|
+
export type ExactPolicyAdmissionOptionsV2 = {
|
|
213
|
+
maxParentEdges: number;
|
|
214
|
+
deadline: number;
|
|
215
|
+
signal?: AbortSignal;
|
|
216
|
+
};
|
|
217
|
+
|
|
167
218
|
/** Internal read-only resolution used by the durable policy-prefix lease. */
|
|
168
219
|
export type AcceptedPolicyPrefixResolutionV2 =
|
|
169
220
|
| {
|
|
@@ -272,6 +323,12 @@ type ParentResolutionV2 =
|
|
|
272
323
|
| { status: "unavailable"; digest: Uint8Array; reason: string }
|
|
273
324
|
| { status: "reject"; digest: Uint8Array; reason: string };
|
|
274
325
|
|
|
326
|
+
type ExactPolicyTraversalV2 = {
|
|
327
|
+
remainingParentEdges: number;
|
|
328
|
+
deadline: number;
|
|
329
|
+
signal: AbortSignal;
|
|
330
|
+
};
|
|
331
|
+
|
|
275
332
|
type SnapshotResolutionCacheV2 = Map<
|
|
276
333
|
string,
|
|
277
334
|
Promise<ValidatedPolicySnapshotV2 | undefined>
|
|
@@ -282,6 +339,7 @@ type EvaluationV2 =
|
|
|
282
339
|
| { status: "duplicate" }
|
|
283
340
|
| { status: "missing"; digest: Uint8Array; reason?: string }
|
|
284
341
|
| { status: "reject"; reason: string }
|
|
342
|
+
| { status: "interrupted"; reason: string }
|
|
285
343
|
| { status: "unavailable"; digest: Uint8Array; reason: string }
|
|
286
344
|
| {
|
|
287
345
|
status: "fork";
|
|
@@ -306,6 +364,11 @@ const validationMessage = (error: unknown): string =>
|
|
|
306
364
|
const boundedUnavailableReason = (reason: string): string =>
|
|
307
365
|
reason.slice(0, MAX_UNAVAILABLE_REASON_LENGTH_V2);
|
|
308
366
|
|
|
367
|
+
const PREPARED_EXACT_POLICIES = new WeakMap<
|
|
368
|
+
object,
|
|
369
|
+
[TrustedNetworkV2PolicyReducer, ValidatedPolicySnapshotV2]
|
|
370
|
+
>();
|
|
371
|
+
|
|
309
372
|
class PolicyDependencyUnavailableErrorV2 extends Error {
|
|
310
373
|
constructor(message: string) {
|
|
311
374
|
super(message);
|
|
@@ -370,6 +433,76 @@ export const authenticatePolicySnapshotEntryV2 = async (
|
|
|
370
433
|
);
|
|
371
434
|
};
|
|
372
435
|
|
|
436
|
+
export const captureCanonicalPolicyEntryCidV2 = (value: unknown): string => {
|
|
437
|
+
if (
|
|
438
|
+
typeof value !== "string" ||
|
|
439
|
+
value.length < 1 ||
|
|
440
|
+
value.length > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_CID_CHARACTERS
|
|
441
|
+
) {
|
|
442
|
+
throw new Error("Policy entry CID must be a bounded canonical CID");
|
|
443
|
+
}
|
|
444
|
+
let parsed: ReturnType<typeof cidifyString>;
|
|
445
|
+
try {
|
|
446
|
+
parsed = cidifyString(value);
|
|
447
|
+
} catch {
|
|
448
|
+
throw new Error("Policy entry CID must be a canonical CID");
|
|
449
|
+
}
|
|
450
|
+
if (
|
|
451
|
+
parsed.version !== 1 ||
|
|
452
|
+
parsed.code !== codecMap.raw.code ||
|
|
453
|
+
parsed.multihash.code !== defaultHasher.code ||
|
|
454
|
+
parsed.multihash.digest.byteLength !== 32 ||
|
|
455
|
+
stringifyCid(parsed) !== value
|
|
456
|
+
) {
|
|
457
|
+
throw new Error("Policy entry CID must use canonical CIDv1/raw/sha2-256");
|
|
458
|
+
}
|
|
459
|
+
return value;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Authenticate one CID-addressed policy wrapper and project its policy-body
|
|
464
|
+
* identity. The wrapper CID is deliberately not the policy identity: distinct
|
|
465
|
+
* canonical EntryV0 storage wrappers may carry the same signed policy body.
|
|
466
|
+
*/
|
|
467
|
+
const authenticateExactPolicySnapshotV2 = async (properties: {
|
|
468
|
+
policyEntryCid: string;
|
|
469
|
+
entryBytes: Uint8Array;
|
|
470
|
+
descriptor: NetworkDescriptorV2;
|
|
471
|
+
}): Promise<{
|
|
472
|
+
policyEntryCid: string;
|
|
473
|
+
snapshot: ValidatedPolicySnapshotV2;
|
|
474
|
+
}> => {
|
|
475
|
+
const policyEntryCid = captureCanonicalPolicyEntryCidV2(
|
|
476
|
+
properties.policyEntryCid,
|
|
477
|
+
);
|
|
478
|
+
assertNetworkDescriptorV2(properties.descriptor);
|
|
479
|
+
const entryBytes = capturePolicySnapshotEntryBytesV2(properties.entryBytes);
|
|
480
|
+
const prepared = await calculateRawCid(entryBytes);
|
|
481
|
+
if (prepared.cid !== policyEntryCid) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
"Resolved policy entry bytes do not match the requested CID",
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const snapshot = await authenticateCapturedPolicySnapshotEntryV2(
|
|
487
|
+
entryBytes,
|
|
488
|
+
properties.descriptor,
|
|
489
|
+
);
|
|
490
|
+
return { policyEntryCid, snapshot };
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
export const authenticateExactPolicyEntryV2 = async (properties: {
|
|
494
|
+
policyEntryCid: string;
|
|
495
|
+
entryBytes: Uint8Array;
|
|
496
|
+
descriptor: NetworkDescriptorV2;
|
|
497
|
+
}): Promise<AuthenticatedExactPolicyEntryV2> => {
|
|
498
|
+
const { policyEntryCid, snapshot } =
|
|
499
|
+
await authenticateExactPolicySnapshotV2(properties);
|
|
500
|
+
return {
|
|
501
|
+
policyEntryCid,
|
|
502
|
+
policy: projectionFromSnapshot(snapshot),
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
|
|
373
506
|
const projectionFromSnapshot = (
|
|
374
507
|
snapshot: ValidatedPolicySnapshotV2,
|
|
375
508
|
): PolicyHeadProjectionV2 => ({
|
|
@@ -767,6 +900,76 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
767
900
|
return (this.rolesFor(subject) & roles) === roles;
|
|
768
901
|
}
|
|
769
902
|
|
|
903
|
+
async prepareExactPolicyEntry(
|
|
904
|
+
policyEntryCid: string,
|
|
905
|
+
entryBytes: Uint8Array,
|
|
906
|
+
): Promise<PreparedExactPolicyCandidateV2> {
|
|
907
|
+
if (this.lifecycleController.signal.aborted) {
|
|
908
|
+
throw new Error("Policy reducer lifecycle is aborted");
|
|
909
|
+
}
|
|
910
|
+
const prepared = await authenticateExactPolicySnapshotV2({
|
|
911
|
+
policyEntryCid,
|
|
912
|
+
entryBytes,
|
|
913
|
+
descriptor: this.descriptor,
|
|
914
|
+
});
|
|
915
|
+
if (this.lifecycleController.signal.aborted) {
|
|
916
|
+
throw new Error("Policy reducer lifecycle is aborted");
|
|
917
|
+
}
|
|
918
|
+
const candidate = Object.freeze({
|
|
919
|
+
policyEntryCid: prepared.policyEntryCid,
|
|
920
|
+
policy: projectionFromSnapshot(prepared.snapshot),
|
|
921
|
+
});
|
|
922
|
+
PREPARED_EXACT_POLICIES.set(candidate, [this, prepared.snapshot]);
|
|
923
|
+
return candidate;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async ingestPreparedExactPolicy(
|
|
927
|
+
candidate: PreparedExactPolicyCandidateV2,
|
|
928
|
+
options: ExactPolicyAdmissionOptionsV2,
|
|
929
|
+
): Promise<PolicyAdmissionResultV2> {
|
|
930
|
+
const prepared =
|
|
931
|
+
candidate !== null && typeof candidate === "object"
|
|
932
|
+
? PREPARED_EXACT_POLICIES.get(candidate)
|
|
933
|
+
: undefined;
|
|
934
|
+
if (prepared?.[0] !== this) {
|
|
935
|
+
return this.result(
|
|
936
|
+
"rejected",
|
|
937
|
+
"Prepared exact policy does not belong to this reducer",
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
let traversal: ExactPolicyTraversalV2;
|
|
941
|
+
let disposeSignal = (): void => {};
|
|
942
|
+
try {
|
|
943
|
+
const maxParentEdges = options.maxParentEdges;
|
|
944
|
+
const deadline = options.deadline;
|
|
945
|
+
const sourceSignal = options.signal;
|
|
946
|
+
if (
|
|
947
|
+
!Number.isSafeInteger(maxParentEdges) ||
|
|
948
|
+
maxParentEdges < 0 ||
|
|
949
|
+
maxParentEdges > TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES ||
|
|
950
|
+
!Number.isSafeInteger(deadline) ||
|
|
951
|
+
deadline < 0
|
|
952
|
+
) {
|
|
953
|
+
throw new Error();
|
|
954
|
+
}
|
|
955
|
+
const observed = observeAbortSignalV2(sourceSignal);
|
|
956
|
+
disposeSignal = observed[1];
|
|
957
|
+
traversal = {
|
|
958
|
+
remainingParentEdges: maxParentEdges,
|
|
959
|
+
deadline,
|
|
960
|
+
signal: observed[0].signal,
|
|
961
|
+
};
|
|
962
|
+
} catch {
|
|
963
|
+
return this.result(
|
|
964
|
+
"rejected",
|
|
965
|
+
"Exact policy admission bounds are invalid",
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
return this.enqueueAdmission(() =>
|
|
969
|
+
this.ingestSnapshot(prepared[1], traversal),
|
|
970
|
+
).finally(disposeSignal);
|
|
971
|
+
}
|
|
972
|
+
|
|
770
973
|
/**
|
|
771
974
|
* Resolve one exact digest on the currently accepted policy prefix.
|
|
772
975
|
*
|
|
@@ -1301,14 +1504,34 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1301
1504
|
};
|
|
1302
1505
|
}
|
|
1303
1506
|
|
|
1507
|
+
private takeExactParentEdges(
|
|
1508
|
+
traversal: ExactPolicyTraversalV2 | undefined,
|
|
1509
|
+
count: number,
|
|
1510
|
+
): Extract<EvaluationV2, { status: "interrupted" }> | undefined {
|
|
1511
|
+
if (traversal === undefined) return undefined;
|
|
1512
|
+
const reason = traversal.signal.aborted
|
|
1513
|
+
? "Exact policy admission was aborted by the caller"
|
|
1514
|
+
: Date.now() >= traversal.deadline
|
|
1515
|
+
? "Exact policy admission deadline elapsed"
|
|
1516
|
+
: traversal.remainingParentEdges < count
|
|
1517
|
+
? "Exact policy admission reached its parent-edge budget"
|
|
1518
|
+
: undefined;
|
|
1519
|
+
if (reason !== undefined) return { status: "interrupted", reason };
|
|
1520
|
+
traversal.remainingParentEdges -= count;
|
|
1521
|
+
return undefined;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1304
1524
|
private async evaluate(
|
|
1305
1525
|
candidate: ValidatedPolicySnapshotV2,
|
|
1526
|
+
traversal?: ExactPolicyTraversalV2,
|
|
1306
1527
|
): Promise<EvaluationV2> {
|
|
1307
1528
|
const resolutionCache: SnapshotResolutionCacheV2 = new Map();
|
|
1308
1529
|
if (this.acceptedHead === undefined) {
|
|
1309
1530
|
let cursor = candidate;
|
|
1310
1531
|
while (cursor.body.sequence !== 0n) {
|
|
1311
|
-
const
|
|
1532
|
+
const interrupted = this.takeExactParentEdges(traversal, 1);
|
|
1533
|
+
if (interrupted !== undefined) return interrupted;
|
|
1534
|
+
const parent = await this.parentOf(cursor, resolutionCache, traversal);
|
|
1312
1535
|
if (parent.status !== "found") {
|
|
1313
1536
|
return this.candidateAncestryResult(parent);
|
|
1314
1537
|
}
|
|
@@ -1324,7 +1547,13 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1324
1547
|
|
|
1325
1548
|
while (candidateCursor.body.sequence > acceptedCursor.body.sequence) {
|
|
1326
1549
|
candidateChild = candidateCursor;
|
|
1327
|
-
const
|
|
1550
|
+
const interrupted = this.takeExactParentEdges(traversal, 1);
|
|
1551
|
+
if (interrupted !== undefined) return interrupted;
|
|
1552
|
+
const parent = await this.parentOf(
|
|
1553
|
+
candidateCursor,
|
|
1554
|
+
resolutionCache,
|
|
1555
|
+
traversal,
|
|
1556
|
+
);
|
|
1328
1557
|
if (parent.status !== "found") {
|
|
1329
1558
|
return this.candidateAncestryResult(parent);
|
|
1330
1559
|
}
|
|
@@ -1332,7 +1561,13 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1332
1561
|
}
|
|
1333
1562
|
while (acceptedCursor.body.sequence > candidateCursor.body.sequence) {
|
|
1334
1563
|
acceptedChild = acceptedCursor;
|
|
1335
|
-
const
|
|
1564
|
+
const interrupted = this.takeExactParentEdges(traversal, 1);
|
|
1565
|
+
if (interrupted !== undefined) return interrupted;
|
|
1566
|
+
const parent = await this.parentOf(
|
|
1567
|
+
acceptedCursor,
|
|
1568
|
+
resolutionCache,
|
|
1569
|
+
traversal,
|
|
1570
|
+
);
|
|
1336
1571
|
if (parent.status !== "found") {
|
|
1337
1572
|
return this.acceptedAncestryUnavailable(parent);
|
|
1338
1573
|
}
|
|
@@ -1351,9 +1586,11 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1351
1586
|
}
|
|
1352
1587
|
candidateChild = candidateCursor;
|
|
1353
1588
|
acceptedChild = acceptedCursor;
|
|
1589
|
+
const interrupted = this.takeExactParentEdges(traversal, 2);
|
|
1590
|
+
if (interrupted !== undefined) return interrupted;
|
|
1354
1591
|
const [candidateParent, acceptedParent] = await Promise.all([
|
|
1355
|
-
this.parentOf(candidateCursor, resolutionCache),
|
|
1356
|
-
this.parentOf(acceptedCursor, resolutionCache),
|
|
1592
|
+
this.parentOf(candidateCursor, resolutionCache, traversal),
|
|
1593
|
+
this.parentOf(acceptedCursor, resolutionCache, traversal),
|
|
1357
1594
|
]);
|
|
1358
1595
|
if (acceptedParent.status !== "found") {
|
|
1359
1596
|
return this.acceptedAncestryUnavailable(acceptedParent);
|
|
@@ -1633,6 +1870,13 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1633
1870
|
} catch (error) {
|
|
1634
1871
|
return this.result("rejected", validationMessage(error));
|
|
1635
1872
|
}
|
|
1873
|
+
return this.ingestSnapshot(snapshot);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
private async ingestSnapshot(
|
|
1877
|
+
snapshot: ValidatedPolicySnapshotV2,
|
|
1878
|
+
traversal?: ExactPolicyTraversalV2,
|
|
1879
|
+
): Promise<PolicyAdmissionResultV2> {
|
|
1636
1880
|
if (this.lifecycleController.signal.aborted) return this.haltedResult();
|
|
1637
1881
|
|
|
1638
1882
|
if (this.fork !== undefined) {
|
|
@@ -1644,9 +1888,10 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1644
1888
|
forkObservation === undefined ? undefined : [forkObservation],
|
|
1645
1889
|
);
|
|
1646
1890
|
}
|
|
1647
|
-
this.retainCanonicalHeadEntry(snapshot);
|
|
1891
|
+
if (traversal === undefined) this.retainCanonicalHeadEntry(snapshot);
|
|
1648
1892
|
|
|
1649
1893
|
if (this.unavailable !== undefined) {
|
|
1894
|
+
if (traversal !== undefined) return this.unavailableResult();
|
|
1650
1895
|
if (this.acceptedHead?.digestKey === snapshot.digestKey) {
|
|
1651
1896
|
return this.result("unavailable", this.unavailable.reason);
|
|
1652
1897
|
}
|
|
@@ -1672,17 +1917,27 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1672
1917
|
}
|
|
1673
1918
|
|
|
1674
1919
|
const existingPending = this.pending.get(snapshot.digestKey);
|
|
1675
|
-
if (existingPending !== undefined) {
|
|
1920
|
+
if (existingPending !== undefined && traversal === undefined) {
|
|
1676
1921
|
this.addPending(snapshot, existingPending.missingParentDigest);
|
|
1677
1922
|
return this.result("pending", "Policy snapshot is already pending");
|
|
1678
1923
|
}
|
|
1679
1924
|
|
|
1680
|
-
const evaluation = await this.evaluate(snapshot);
|
|
1925
|
+
const evaluation = await this.evaluate(snapshot, traversal);
|
|
1681
1926
|
if (this.lifecycleController.signal.aborted) return this.haltedResult();
|
|
1927
|
+
const interrupted = this.takeExactParentEdges(traversal, 0);
|
|
1928
|
+
if (evaluation.status === "interrupted" || interrupted !== undefined) {
|
|
1929
|
+
return this.result(
|
|
1930
|
+
"unavailable",
|
|
1931
|
+
evaluation.status === "interrupted"
|
|
1932
|
+
? evaluation.reason
|
|
1933
|
+
: interrupted!.reason,
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1682
1936
|
if (evaluation.status === "reject") {
|
|
1683
1937
|
return this.result("rejected", evaluation.reason);
|
|
1684
1938
|
}
|
|
1685
1939
|
if (evaluation.status === "duplicate") {
|
|
1940
|
+
if (traversal !== undefined) this.pending.delete(snapshot.digestKey);
|
|
1686
1941
|
return this.result("duplicate");
|
|
1687
1942
|
}
|
|
1688
1943
|
if (evaluation.status === "fork") {
|
|
@@ -1703,6 +1958,11 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1703
1958
|
);
|
|
1704
1959
|
}
|
|
1705
1960
|
|
|
1961
|
+
if (traversal !== undefined) {
|
|
1962
|
+
this.pending.delete(snapshot.digestKey);
|
|
1963
|
+
this.project(snapshot);
|
|
1964
|
+
return this.result("accepted");
|
|
1965
|
+
}
|
|
1706
1966
|
this.project(snapshot);
|
|
1707
1967
|
return (
|
|
1708
1968
|
this.completedDrainResult(await this.drainPending()) ??
|