@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.
- package/dist/src/v2-policy-anchor.d.ts +75 -0
- package/dist/src/v2-policy-anchor.d.ts.map +1 -0
- package/dist/src/v2-policy-anchor.js +990 -0
- package/dist/src/v2-policy-anchor.js.map +1 -0
- package/dist/src/v2-policy-engine.d.ts +40 -8
- package/dist/src/v2-policy-engine.d.ts.map +1 -1
- package/dist/src/v2-policy-engine.js +219 -23
- package/dist/src/v2-policy-engine.js.map +1 -1
- package/package.json +7 -6
- package/src/v2-policy-anchor.ts +1152 -0
- package/src/v2-policy-engine.ts +346 -31
package/src/v2-policy-engine.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
523
|
-
|
|
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.
|
|
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")
|
|
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(
|
|
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(
|
|
870
|
-
|
|
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
|
|
880
|
-
//
|
|
881
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
978
|
-
|
|
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.
|
|
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
|
-
|
|
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);
|