@peerbit/trusted-network 6.0.105 → 6.0.107

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.
@@ -122,12 +122,50 @@ export type PolicyAdmissionResultV2 = {
122
122
  status: PolicyAdmissionStatusV2;
123
123
  reason?: string;
124
124
  head?: PolicyHeadProjectionV2;
125
+ /**
126
+ * Authenticated direct children surfaced while entering or already in
127
+ * FORKED. The outer durable layer can persist these proofs without decoding
128
+ * or verifying them a second time. They are bounded by one admission plus the
129
+ * pending working set and are not accumulated by this reducer.
130
+ */
131
+ forkObservations?: PolicyForkChildProofV2[];
125
132
  fetchHints: PolicyParentFetchHintV2[];
126
133
  pendingCount: number;
127
134
  pendingBytes: number;
128
135
  evictedPolicyDigests?: Uint8Array[];
129
136
  };
130
137
 
138
+ export type PolicyReducerDurableStateV2 =
139
+ | { formatVersion: 1; state: "EMPTY" }
140
+ | {
141
+ formatVersion: 1;
142
+ state: "ACTIVE";
143
+ acceptedHeadEntryBytes: Uint8Array;
144
+ }
145
+ | {
146
+ formatVersion: 1;
147
+ state: "UNAVAILABLE";
148
+ acceptedHeadEntryBytes: Uint8Array;
149
+ comparisonCandidateEntryBytes: Uint8Array;
150
+ acceptedAncestorDigest: Uint8Array;
151
+ reason: string;
152
+ }
153
+ | {
154
+ formatVersion: 1;
155
+ state: "FORKED";
156
+ commonParentEntryBytes: Uint8Array;
157
+ childEntryBytes: [Uint8Array, Uint8Array];
158
+ };
159
+
160
+ export type TrustedNetworkV2PolicyReducerProperties = {
161
+ descriptor: NetworkDescriptorV2;
162
+ resolvePolicyEntry: PolicySnapshotResolverV2;
163
+ resolveTimeoutMs?: number;
164
+ signal?: AbortSignal;
165
+ maxPending?: number;
166
+ maxPendingPolicyBytes?: number;
167
+ };
168
+
131
169
  type PendingPolicySnapshotV2 = {
132
170
  snapshot: ValidatedPolicySnapshotV2;
133
171
  missingParentDigest: Uint8Array;
@@ -135,7 +173,7 @@ type PendingPolicySnapshotV2 = {
135
173
 
136
174
  type UnavailablePolicyComparisonV2 = {
137
175
  acceptedAncestorDigest: Uint8Array;
138
- candidateDigestKey: string;
176
+ comparisonCandidate: ValidatedPolicySnapshotV2;
139
177
  reason: string;
140
178
  };
141
179
 
@@ -165,7 +203,7 @@ type EvaluationV2 =
165
203
 
166
204
  type PendingDrainOutcomeV2 =
167
205
  | { status: "accepted" }
168
- | { status: "forked" }
206
+ | { status: "forked"; forkObservations: PolicyForkChildProofV2[] }
169
207
  | { status: "halted" }
170
208
  | {
171
209
  status: "unavailable";
@@ -336,6 +374,84 @@ const copyForkEvidence = (
336
374
  ],
337
375
  });
338
376
 
377
+ const captureDurableStateV2 = (
378
+ durableState: PolicyReducerDurableStateV2,
379
+ ): PolicyReducerDurableStateV2 => {
380
+ if (
381
+ durableState === null ||
382
+ typeof durableState !== "object" ||
383
+ durableState.formatVersion !== 1
384
+ ) {
385
+ throw new Error("Unsupported TrustedNetwork v2 reducer state format");
386
+ }
387
+
388
+ switch (durableState.state) {
389
+ case "EMPTY":
390
+ return { formatVersion: 1, state: "EMPTY" };
391
+ case "ACTIVE":
392
+ return {
393
+ formatVersion: 1,
394
+ state: "ACTIVE",
395
+ acceptedHeadEntryBytes: capturePolicySnapshotEntryBytesV2(
396
+ durableState.acceptedHeadEntryBytes,
397
+ ),
398
+ };
399
+ case "UNAVAILABLE": {
400
+ if (
401
+ !(durableState.acceptedAncestorDigest instanceof Uint8Array) ||
402
+ durableState.acceptedAncestorDigest.byteLength !== 32
403
+ ) {
404
+ throw new Error(
405
+ "Unavailable accepted ancestor digest must contain exactly 32 bytes",
406
+ );
407
+ }
408
+ if (
409
+ typeof durableState.reason !== "string" ||
410
+ durableState.reason.length === 0 ||
411
+ durableState.reason.length > MAX_UNAVAILABLE_REASON_LENGTH_V2
412
+ ) {
413
+ throw new Error(
414
+ `Unavailable reason must contain 1-${MAX_UNAVAILABLE_REASON_LENGTH_V2} characters`,
415
+ );
416
+ }
417
+ return {
418
+ formatVersion: 1,
419
+ state: "UNAVAILABLE",
420
+ acceptedHeadEntryBytes: capturePolicySnapshotEntryBytesV2(
421
+ durableState.acceptedHeadEntryBytes,
422
+ ),
423
+ comparisonCandidateEntryBytes: capturePolicySnapshotEntryBytesV2(
424
+ durableState.comparisonCandidateEntryBytes,
425
+ ),
426
+ acceptedAncestorDigest: copyBytes(durableState.acceptedAncestorDigest),
427
+ reason: durableState.reason,
428
+ };
429
+ }
430
+ case "FORKED":
431
+ if (
432
+ !Array.isArray(durableState.childEntryBytes) ||
433
+ durableState.childEntryBytes.length !== 2
434
+ ) {
435
+ throw new Error(
436
+ "Forked reducer state must contain exactly two children",
437
+ );
438
+ }
439
+ return {
440
+ formatVersion: 1,
441
+ state: "FORKED",
442
+ commonParentEntryBytes: capturePolicySnapshotEntryBytesV2(
443
+ durableState.commonParentEntryBytes,
444
+ ),
445
+ childEntryBytes: [
446
+ capturePolicySnapshotEntryBytesV2(durableState.childEntryBytes[0]),
447
+ capturePolicySnapshotEntryBytesV2(durableState.childEntryBytes[1]),
448
+ ],
449
+ };
450
+ default:
451
+ throw new Error("Unsupported TrustedNetwork v2 reducer state");
452
+ }
453
+ };
454
+
339
455
  export class TrustedNetworkV2PolicyReducer {
340
456
  private readonly descriptor: NetworkDescriptorV2;
341
457
  private readonly resolvePolicyEntry: PolicySnapshotResolverV2;
@@ -354,14 +470,7 @@ export class TrustedNetworkV2PolicyReducer {
354
470
  private fork?: PolicyForkEvidenceV2;
355
471
  private admissionTail: Promise<void> = Promise.resolve();
356
472
 
357
- constructor(properties: {
358
- descriptor: NetworkDescriptorV2;
359
- resolvePolicyEntry: PolicySnapshotResolverV2;
360
- resolveTimeoutMs?: number;
361
- signal?: AbortSignal;
362
- maxPending?: number;
363
- maxPendingPolicyBytes?: number;
364
- }) {
473
+ constructor(properties: TrustedNetworkV2PolicyReducerProperties) {
365
474
  assertNetworkDescriptorV2(properties.descriptor);
366
475
  const maxPending = properties.maxPending ?? DEFAULT_MAX_PENDING_POLICIES_V2;
367
476
  if (!Number.isSafeInteger(maxPending) || maxPending < 1) {
@@ -415,6 +524,101 @@ export class TrustedNetworkV2PolicyReducer {
415
524
  }
416
525
  }
417
526
 
527
+ static async restore(
528
+ properties: TrustedNetworkV2PolicyReducerProperties & {
529
+ durableState: PolicyReducerDurableStateV2;
530
+ },
531
+ ): Promise<TrustedNetworkV2PolicyReducer> {
532
+ // Capture the complete checkpoint before the first await. Persistence
533
+ // adapters commonly reuse read buffers, and mutation during authentication
534
+ // must not change what is restored.
535
+ const durableState = captureDurableStateV2(properties.durableState);
536
+ const reducer = new TrustedNetworkV2PolicyReducer(properties);
537
+ const authenticate = (
538
+ entryBytes: Uint8Array,
539
+ ): Promise<ValidatedPolicySnapshotV2> =>
540
+ authenticateCapturedPolicySnapshotEntryV2(entryBytes, reducer.descriptor);
541
+
542
+ try {
543
+ switch (durableState.state) {
544
+ case "EMPTY":
545
+ return reducer;
546
+ case "ACTIVE": {
547
+ const acceptedHead = await authenticate(
548
+ durableState.acceptedHeadEntryBytes,
549
+ );
550
+ // A durable ACTIVE head is a trusted prior-validation checkpoint. Its
551
+ // authority signature and network binding are re-authenticated above,
552
+ // but restore deliberately does not require historical resolver data.
553
+ reducer.project(acceptedHead);
554
+ return reducer;
555
+ }
556
+ case "UNAVAILABLE": {
557
+ const [acceptedHead, comparisonCandidate] = await Promise.all([
558
+ authenticate(durableState.acceptedHeadEntryBytes),
559
+ authenticate(durableState.comparisonCandidateEntryBytes),
560
+ ]);
561
+ if (equals(acceptedHead.digest, comparisonCandidate.digest)) {
562
+ throw new Error(
563
+ "Unavailable comparison candidate must differ from the accepted head",
564
+ );
565
+ }
566
+ reducer.project(acceptedHead);
567
+ reducer.addPending(
568
+ comparisonCandidate,
569
+ durableState.acceptedAncestorDigest,
570
+ );
571
+ reducer.unavailable = {
572
+ acceptedAncestorDigest: copyBytes(
573
+ durableState.acceptedAncestorDigest,
574
+ ),
575
+ comparisonCandidate: copySnapshot(comparisonCandidate),
576
+ reason: durableState.reason,
577
+ };
578
+ return reducer;
579
+ }
580
+ case "FORKED": {
581
+ const [commonParent, firstChild, secondChild] = await Promise.all([
582
+ authenticate(durableState.commonParentEntryBytes),
583
+ authenticate(durableState.childEntryBytes[0]),
584
+ authenticate(durableState.childEntryBytes[1]),
585
+ ]);
586
+ for (const child of [firstChild, secondChild]) {
587
+ if (
588
+ child.body.sequence !== commonParent.body.sequence + 1n ||
589
+ !equals(child.body.previousPolicyDigest, commonParent.digest)
590
+ ) {
591
+ throw new Error(
592
+ "Fork child must be a direct successor of the common parent",
593
+ );
594
+ }
595
+ }
596
+ if (equals(firstChild.digest, secondChild.digest)) {
597
+ throw new Error("Fork children must have distinct policy digests");
598
+ }
599
+
600
+ const children = [
601
+ forkProofFromSnapshot(firstChild),
602
+ forkProofFromSnapshot(secondChild),
603
+ ].sort(compareForkChildProofs) as [
604
+ PolicyForkChildProofV2,
605
+ PolicyForkChildProofV2,
606
+ ];
607
+ reducer.project(commonParent);
608
+ reducer.fork = {
609
+ commonParent: projectionFromSnapshot(commonParent),
610
+ children,
611
+ };
612
+ return reducer;
613
+ }
614
+ }
615
+ } catch (error) {
616
+ // Do not retain a caller-owned AbortSignal listener when restore rejects.
617
+ reducer.abort();
618
+ throw error;
619
+ }
620
+ }
621
+
418
622
  get state(): "EMPTY" | "ACTIVE" | "UNAVAILABLE" | "FORKED" | "HALTED" {
419
623
  if (this.lifecycleController.signal.aborted) return "HALTED";
420
624
  if (this.fork !== undefined) return "FORKED";
@@ -450,6 +654,50 @@ export class TrustedNetworkV2PolicyReducer {
450
654
  .map(({ snapshot }) => copyBytes(snapshot.digest));
451
655
  }
452
656
 
657
+ exportDurableState(): PolicyReducerDurableStateV2 {
658
+ // Lifecycle cancellation is process-local. Export the underlying protocol
659
+ // safety state so a replacement process cannot erase UNAVAILABLE/FORKED.
660
+ if (this.fork !== undefined) {
661
+ if (this.acceptedHead === undefined) {
662
+ throw new Error("Forked reducer is missing its common-parent entry");
663
+ }
664
+ return {
665
+ formatVersion: 1,
666
+ state: "FORKED",
667
+ commonParentEntryBytes: copyBytes(this.acceptedHead.entryBytes),
668
+ childEntryBytes: [
669
+ copyBytes(this.fork.children[0].entryBytes),
670
+ copyBytes(this.fork.children[1].entryBytes),
671
+ ],
672
+ };
673
+ }
674
+ if (this.unavailable !== undefined) {
675
+ if (this.acceptedHead === undefined) {
676
+ throw new Error("Unavailable reducer is missing its accepted head");
677
+ }
678
+ return {
679
+ formatVersion: 1,
680
+ state: "UNAVAILABLE",
681
+ acceptedHeadEntryBytes: copyBytes(this.acceptedHead.entryBytes),
682
+ comparisonCandidateEntryBytes: copyBytes(
683
+ this.unavailable.comparisonCandidate.entryBytes,
684
+ ),
685
+ acceptedAncestorDigest: copyBytes(
686
+ this.unavailable.acceptedAncestorDigest,
687
+ ),
688
+ reason: this.unavailable.reason,
689
+ };
690
+ }
691
+ if (this.acceptedHead !== undefined) {
692
+ return {
693
+ formatVersion: 1,
694
+ state: "ACTIVE",
695
+ acceptedHeadEntryBytes: copyBytes(this.acceptedHead.entryBytes),
696
+ };
697
+ }
698
+ return { formatVersion: 1, state: "EMPTY" };
699
+ }
700
+
453
701
  rolesFor(subject: PublicSignKey): number {
454
702
  return this.projectedRoles.get(publicKeyId(subject)) ?? 0;
455
703
  }
@@ -504,11 +752,16 @@ export class TrustedNetworkV2PolicyReducer {
504
752
  status: PolicyAdmissionStatusV2,
505
753
  reason?: string,
506
754
  evictedPolicyDigests?: Uint8Array[],
755
+ forkObservations?: PolicyForkChildProofV2[],
507
756
  ): PolicyAdmissionResultV2 {
508
757
  return {
509
758
  status,
510
759
  reason,
511
760
  head: this.head,
761
+ forkObservations:
762
+ forkObservations === undefined
763
+ ? undefined
764
+ : forkObservations.map(copyForkChildProof),
512
765
  fetchHints: this.fetchHints(),
513
766
  pendingCount: this.pending.size,
514
767
  pendingBytes: this.pendingBytes,
@@ -519,8 +772,15 @@ export class TrustedNetworkV2PolicyReducer {
519
772
  };
520
773
  }
521
774
 
522
- private forkedResult(): PolicyAdmissionResultV2 {
523
- return this.result("forked", "Policy authority signed competing children");
775
+ private forkedResult(
776
+ forkObservations?: PolicyForkChildProofV2[],
777
+ ): PolicyAdmissionResultV2 {
778
+ return this.result(
779
+ "forked",
780
+ "Policy authority signed competing children",
781
+ undefined,
782
+ forkObservations,
783
+ );
524
784
  }
525
785
 
526
786
  private haltedResult(): PolicyAdmissionResultV2 {
@@ -533,7 +793,7 @@ export class TrustedNetworkV2PolicyReducer {
533
793
  }): PolicyAdmissionResultV2 {
534
794
  const blockedCandidateRetained =
535
795
  this.unavailable !== undefined &&
536
- this.pending.has(this.unavailable.candidateDigestKey);
796
+ this.pending.has(this.unavailable.comparisonCandidate.digestKey);
537
797
  const recoverable =
538
798
  (retention?.retained ?? true) && blockedCandidateRetained;
539
799
  const reason = recoverable
@@ -551,7 +811,9 @@ export class TrustedNetworkV2PolicyReducer {
551
811
  private completedDrainResult(
552
812
  outcome: PendingDrainOutcomeV2 | undefined,
553
813
  ): PolicyAdmissionResultV2 | undefined {
554
- if (outcome?.status === "forked") return this.forkedResult();
814
+ if (outcome?.status === "forked") {
815
+ return this.forkedResult(outcome.forkObservations);
816
+ }
555
817
  if (outcome?.status === "halted") return this.haltedResult();
556
818
  return outcome?.status === "unavailable"
557
819
  ? this.unavailableResult(outcome)
@@ -826,7 +1088,39 @@ export class TrustedNetworkV2PolicyReducer {
826
1088
  };
827
1089
  }
828
1090
 
829
- private setFork(evaluation: Extract<EvaluationV2, { status: "fork" }>): void {
1091
+ private setFork(
1092
+ evaluation: Extract<EvaluationV2, { status: "fork" }>,
1093
+ ): PolicyForkChildProofV2[] {
1094
+ // Pending snapshots were authenticated when admitted. Combine every one
1095
+ // that is already provably a direct child with the pair that first exposed
1096
+ // the fork. Canonical selection below may displace either initial child, so
1097
+ // the durable layer needs the complete bounded observation set and removes
1098
+ // whichever final pair is carried by exportDurableState().
1099
+ const pendingForkObservationSnapshots = [...this.pending.values()]
1100
+ .map(({ snapshot }) => snapshot)
1101
+ .filter(
1102
+ (snapshot) =>
1103
+ snapshot.body.sequence ===
1104
+ evaluation.commonParent.body.sequence + 1n &&
1105
+ equals(
1106
+ snapshot.body.previousPolicyDigest,
1107
+ evaluation.commonParent.digest,
1108
+ ),
1109
+ );
1110
+ const forkObservationSnapshots = [
1111
+ evaluation.candidateChild,
1112
+ evaluation.acceptedChild,
1113
+ ...pendingForkObservationSnapshots,
1114
+ ];
1115
+ const forkObservations = forkObservationSnapshots
1116
+ .map(forkProofFromSnapshot)
1117
+ .sort(compareForkChildProofs)
1118
+ .filter(
1119
+ (proof, index, observations) =>
1120
+ index === 0 ||
1121
+ !equals(proof.entryBytes, observations[index - 1]!.entryBytes),
1122
+ );
1123
+
830
1124
  this.project(evaluation.commonParent);
831
1125
  const children = [
832
1126
  forkProofFromSnapshot(evaluation.candidateChild),
@@ -839,8 +1133,12 @@ export class TrustedNetworkV2PolicyReducer {
839
1133
  commonParent: projectionFromSnapshot(evaluation.commonParent),
840
1134
  children,
841
1135
  };
1136
+ for (const snapshot of forkObservationSnapshots) {
1137
+ this.retainCanonicalForkChild(snapshot);
1138
+ }
842
1139
  this.unavailable = undefined;
843
1140
  this.pending.clear();
1141
+ return forkObservations;
844
1142
  }
845
1143
 
846
1144
  private retainCanonicalForkChild(snapshot: ValidatedPolicySnapshotV2): void {
@@ -866,20 +1164,26 @@ export class TrustedNetworkV2PolicyReducer {
866
1164
  this.fork.children = [canonical[0]!, canonical[1]!];
867
1165
  }
868
1166
 
869
- private observeAfterFork(snapshot: ValidatedPolicySnapshotV2): void {
870
- if (this.fork === undefined || this.acceptedHead === undefined) return;
1167
+ private observeAfterFork(
1168
+ snapshot: ValidatedPolicySnapshotV2,
1169
+ ): PolicyForkChildProofV2 | undefined {
1170
+ if (this.fork === undefined || this.acceptedHead === undefined) {
1171
+ return undefined;
1172
+ }
871
1173
  const commonParent = this.acceptedHead;
872
1174
  if (
873
1175
  snapshot.body.sequence !== commonParent.body.sequence + 1n ||
874
1176
  !equals(snapshot.body.previousPolicyDigest, commonParent.digest)
875
1177
  ) {
876
- return;
1178
+ return undefined;
877
1179
  }
878
1180
 
879
- // This bounded kernel deliberately retains only the canonical two direct
880
- // child proofs. Durable storage of every authenticated fork observation is
881
- // an outer-layer responsibility for a later integration slice.
1181
+ // This bounded kernel retains only the canonical two direct child proofs.
1182
+ // Return this already-authenticated observation so the durable outer layer
1183
+ // can retain every proof without verifying it twice.
1184
+ const observation = forkProofFromSnapshot(snapshot);
882
1185
  this.retainCanonicalForkChild(snapshot);
1186
+ return observation;
883
1187
  }
884
1188
 
885
1189
  private addPending(
@@ -932,7 +1236,7 @@ export class TrustedNetworkV2PolicyReducer {
932
1236
  const retention = this.addPending(snapshot, evaluation.digest);
933
1237
  this.unavailable = {
934
1238
  acceptedAncestorDigest: copyBytes(evaluation.digest),
935
- candidateDigestKey: snapshot.digestKey,
1239
+ comparisonCandidate: copySnapshot(snapshot),
936
1240
  reason: boundedUnavailableReason(evaluation.reason),
937
1241
  };
938
1242
  return retention;
@@ -974,8 +1278,10 @@ export class TrustedNetworkV2PolicyReducer {
974
1278
  this.project(pending.snapshot);
975
1279
  accepted = true;
976
1280
  } else if (evaluation.status === "fork") {
977
- this.setFork(evaluation);
978
- return { status: "forked" };
1281
+ return {
1282
+ status: "forked",
1283
+ forkObservations: this.setFork(evaluation),
1284
+ };
979
1285
  }
980
1286
  }
981
1287
  }
@@ -1036,10 +1342,12 @@ export class TrustedNetworkV2PolicyReducer {
1036
1342
  if (this.lifecycleController.signal.aborted) return this.haltedResult();
1037
1343
 
1038
1344
  if (this.fork !== undefined) {
1039
- this.observeAfterFork(snapshot);
1345
+ const forkObservation = this.observeAfterFork(snapshot);
1040
1346
  return this.result(
1041
1347
  "halted",
1042
1348
  "Policy reducer is halted by authority equivocation",
1349
+ undefined,
1350
+ forkObservation === undefined ? undefined : [forkObservation],
1043
1351
  );
1044
1352
  }
1045
1353
  this.retainCanonicalHeadEntry(snapshot);
@@ -1048,6 +1356,15 @@ export class TrustedNetworkV2PolicyReducer {
1048
1356
  if (this.acceptedHead?.digestKey === snapshot.digestKey) {
1049
1357
  return this.result("unavailable", this.unavailable.reason);
1050
1358
  }
1359
+ if (
1360
+ this.unavailable.comparisonCandidate.digestKey === snapshot.digestKey &&
1361
+ compare(
1362
+ snapshot.entryBytes,
1363
+ this.unavailable.comparisonCandidate.entryBytes,
1364
+ ) < 0
1365
+ ) {
1366
+ this.unavailable.comparisonCandidate = copySnapshot(snapshot);
1367
+ }
1051
1368
  const existingPending = this.pending.get(snapshot.digestKey);
1052
1369
  if (existingPending !== undefined) {
1053
1370
  this.addPending(snapshot, existingPending.missingParentDigest);
@@ -1075,8 +1392,7 @@ export class TrustedNetworkV2PolicyReducer {
1075
1392
  return this.result("duplicate");
1076
1393
  }
1077
1394
  if (evaluation.status === "fork") {
1078
- this.setFork(evaluation);
1079
- return this.forkedResult();
1395
+ return this.forkedResult(this.setFork(evaluation));
1080
1396
  }
1081
1397
  if (evaluation.status === "unavailable") {
1082
1398
  const retention = this.enterUnavailable(snapshot, evaluation);
@@ -1112,7 +1428,7 @@ export class TrustedNetworkV2PolicyReducer {
1112
1428
  if (unavailable === undefined) {
1113
1429
  return this.result("duplicate", "Policy reducer is not unavailable");
1114
1430
  }
1115
- const pending = this.pending.get(unavailable.candidateDigestKey);
1431
+ const pending = this.pending.get(unavailable.comparisonCandidate.digestKey);
1116
1432
  if (pending === undefined) {
1117
1433
  return this.result(
1118
1434
  "capacity",
@@ -1126,7 +1442,7 @@ export class TrustedNetworkV2PolicyReducer {
1126
1442
  pending.missingParentDigest = copyBytes(evaluation.digest);
1127
1443
  this.unavailable = {
1128
1444
  acceptedAncestorDigest: copyBytes(evaluation.digest),
1129
- candidateDigestKey: pending.snapshot.digestKey,
1445
+ comparisonCandidate: copySnapshot(pending.snapshot),
1130
1446
  reason: boundedUnavailableReason(evaluation.reason),
1131
1447
  };
1132
1448
  return this.result("unavailable", this.unavailable.reason);
@@ -1142,8 +1458,7 @@ export class TrustedNetworkV2PolicyReducer {
1142
1458
  } else {
1143
1459
  this.pending.delete(pending.snapshot.digestKey);
1144
1460
  if (evaluation.status === "fork") {
1145
- this.setFork(evaluation);
1146
- return this.forkedResult();
1461
+ return this.forkedResult(this.setFork(evaluation));
1147
1462
  }
1148
1463
  if (evaluation.status === "accept") {
1149
1464
  this.project(pending.snapshot);