@peerbit/trusted-network 6.0.107 → 6.0.109

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.
@@ -15,6 +15,7 @@ import type {
15
15
  PolicySnapshotResolverV2,
16
16
  } from "./v2-policy-engine.js";
17
17
  import {
18
+ TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES,
18
19
  TrustedNetworkV2PolicyReducer,
19
20
  authenticatePolicySnapshotEntryV2,
20
21
  } from "./v2-policy-engine.js";
@@ -41,9 +42,16 @@ export const TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES =
41
42
  TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 4;
42
43
 
43
44
  const GENERATION_KEY_PREFIX = `${TRUSTED_NETWORK_V2_POLICY_ANCHOR_STORE_OWNER}/generation/`;
44
- const FORMAT_VERSION = 1;
45
+ const ANCHOR_FORMAT_VERSION = 2;
46
+ const REDUCER_DURABLE_FORMAT_VERSION = 1;
45
47
  const MAX_U64 = 0xffffffffffffffffn;
46
48
  const MAX_UNAVAILABLE_REASON_LENGTH = 512;
49
+ // The core admits at most 64 pending policies. A fork transition can surface
50
+ // that complete set plus its accepted and triggering children. Once the
51
+ // transition is durable the wrapper stops policy admission entirely.
52
+ const MAX_FORK_OBSERVATIONS_V2 = TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES + 2;
53
+ const MAX_QUEUED_POLICY_ENTRY_BYTES_V2 =
54
+ TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 2;
47
55
  const ZERO_DIGEST = new Uint8Array(32);
48
56
  const textEncoder = new TextEncoder();
49
57
  const DESCRIPTOR_DIGEST_DOMAIN = textEncoder.encode(
@@ -52,6 +60,9 @@ const DESCRIPTOR_DIGEST_DOMAIN = textEncoder.encode(
52
60
  const GENERATION_CHECKSUM_DOMAIN = textEncoder.encode(
53
61
  "peerbit/trusted-network/v2/policy-anchor/generation/v1",
54
62
  );
63
+ const FORK_OBSERVATION_COMMITMENT_DOMAIN = textEncoder.encode(
64
+ "peerbit/trusted-network/v2/policy-anchor/fork-observations/v1",
65
+ );
55
66
  const ANCHOR_STATE = Object.freeze({
56
67
  ACTIVE: 1,
57
68
  UNAVAILABLE: 2,
@@ -133,7 +144,17 @@ class PolicyAnchorStateGenerationPayloadV2 {
133
144
  @field({ type: PolicyAnchorCoreStateRecordV2 })
134
145
  coreState: PolicyAnchorCoreStateRecordV2;
135
146
 
136
- constructor(properties?: { coreState: PolicyAnchorCoreStateRecordV2 }) {
147
+ @field({ type: "u8" })
148
+ forkObservationCount: number;
149
+
150
+ @field({ type: fixedArray("u8", 32) })
151
+ forkObservationCommitment: Uint8Array;
152
+
153
+ constructor(properties?: {
154
+ coreState: PolicyAnchorCoreStateRecordV2;
155
+ forkObservationCount: number;
156
+ forkObservationCommitment: Uint8Array;
157
+ }) {
137
158
  if (properties) Object.assign(this, properties);
138
159
  }
139
160
  }
@@ -208,7 +229,56 @@ type PublishedProjectionV2 = {
208
229
  roles: Map<string, number>;
209
230
  };
210
231
 
211
- const copyBytes = (bytes: Uint8Array): Uint8Array => Uint8Array.from(bytes);
232
+ const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype);
233
+ const TYPED_ARRAY_BYTE_LENGTH = Object.getOwnPropertyDescriptor(
234
+ TYPED_ARRAY_PROTOTYPE,
235
+ "byteLength",
236
+ )!.get!;
237
+ const TYPED_ARRAY_TAG = Object.getOwnPropertyDescriptor(
238
+ TYPED_ARRAY_PROTOTYPE,
239
+ Symbol.toStringTag,
240
+ )!.get!;
241
+ const ARRAY_BUFFER_IS_VIEW = ArrayBuffer.isView;
242
+ const UINT8_ARRAY_SET = Uint8Array.prototype.set;
243
+
244
+ const exactUint8ArrayByteLength = (input: unknown): number => {
245
+ if (
246
+ !ARRAY_BUFFER_IS_VIEW(input) ||
247
+ TYPED_ARRAY_TAG.call(input) !== "Uint8Array"
248
+ ) {
249
+ throw new TypeError("Expected a genuine Uint8Array");
250
+ }
251
+ const byteLength = TYPED_ARRAY_BYTE_LENGTH.call(input) as number;
252
+ if (byteLength === 0) {
253
+ UINT8_ARRAY_SET.call(new Uint8Array(0), input as Uint8Array);
254
+ }
255
+ return byteLength;
256
+ };
257
+
258
+ const copyBytesWithLength = (
259
+ bytes: Uint8Array,
260
+ byteLength: number,
261
+ ): Uint8Array => {
262
+ const copy = new Uint8Array(byteLength);
263
+ UINT8_ARRAY_SET.call(copy, bytes);
264
+ return copy;
265
+ };
266
+
267
+ const copyBytes = (bytes: Uint8Array): Uint8Array => {
268
+ const byteLength = exactUint8ArrayByteLength(bytes);
269
+ return copyBytesWithLength(bytes, byteLength);
270
+ };
271
+
272
+ const hasExactUint8ArrayByteLength = (
273
+ input: unknown,
274
+ expected: number,
275
+ ): input is Uint8Array => {
276
+ try {
277
+ return exactUint8ArrayByteLength(input) === expected;
278
+ } catch {
279
+ return false;
280
+ }
281
+ };
212
282
 
213
283
  const bytesKey = (bytes: Uint8Array): string => {
214
284
  let key = "";
@@ -262,6 +332,44 @@ const generationChecksum = (body: PolicyAnchorGenerationBodyV2): Uint8Array =>
262
332
  const observationContentHash = (entryBytes: Uint8Array): Uint8Array =>
263
333
  sha256Sync(entryBytes);
264
334
 
335
+ const u32Bytes = (value: number): Uint8Array => {
336
+ const bytes = new Uint8Array(4);
337
+ new DataView(bytes.buffer).setUint32(0, value, false);
338
+ return bytes;
339
+ };
340
+
341
+ const canonicalForkObservationEntries = (
342
+ entries: Iterable<Uint8Array>,
343
+ ): Uint8Array[] =>
344
+ [...entries]
345
+ .map((entryBytes) => ({
346
+ entryBytes: copyBytes(entryBytes),
347
+ hash: observationContentHash(entryBytes),
348
+ }))
349
+ .sort((left, right) => {
350
+ const hashOrder = compare(left.hash, right.hash);
351
+ return hashOrder === 0
352
+ ? compare(left.entryBytes, right.entryBytes)
353
+ : hashOrder;
354
+ })
355
+ .map(({ entryBytes }) => entryBytes);
356
+
357
+ const forkObservationCommitment = (
358
+ descriptorHash: Uint8Array,
359
+ canonicalEntries: readonly Uint8Array[],
360
+ ): Uint8Array =>
361
+ sha256Sync(
362
+ concat([
363
+ FORK_OBSERVATION_COMMITMENT_DOMAIN,
364
+ descriptorHash,
365
+ Uint8Array.of(canonicalEntries.length),
366
+ ...canonicalEntries.flatMap((entryBytes) => [
367
+ u32Bytes(entryBytes.byteLength),
368
+ entryBytes,
369
+ ]),
370
+ ]),
371
+ );
372
+
265
373
  const generationKey = (generation: bigint): string =>
266
374
  `${GENERATION_KEY_PREFIX}${generation.toString().padStart(20, "0")}`;
267
375
 
@@ -272,10 +380,15 @@ const assertOpenNotAborted = (signal: AbortSignal | undefined): void => {
272
380
  };
273
381
 
274
382
  const assertEntryBytes = (bytes: Uint8Array, label: string): void => {
383
+ let byteLength: number;
384
+ try {
385
+ byteLength = exactUint8ArrayByteLength(bytes);
386
+ } catch {
387
+ byteLength = -1;
388
+ }
275
389
  if (
276
- !(bytes instanceof Uint8Array) ||
277
- bytes.byteLength === 0 ||
278
- bytes.byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES
390
+ byteLength < 1 ||
391
+ byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES
279
392
  ) {
280
393
  throw new Error(
281
394
  `${label} must contain 1-${TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES} bytes`,
@@ -312,7 +425,7 @@ const retainCanonicalForkChild = (
312
425
  };
313
426
 
314
427
  const assertEmptyBytes = (bytes: Uint8Array, label: string): void => {
315
- if (!(bytes instanceof Uint8Array) || bytes.byteLength !== 0) {
428
+ if (!hasExactUint8ArrayByteLength(bytes, 0)) {
316
429
  throw new Error(`${label} must be empty`);
317
430
  }
318
431
  };
@@ -373,7 +486,7 @@ const durableStateFromCoreRecord = (
373
486
  assertEmptyBytes(record.forkChildEntryBytes0, "active fork child 0");
374
487
  assertEmptyBytes(record.forkChildEntryBytes1, "active fork child 1");
375
488
  return {
376
- formatVersion: FORMAT_VERSION,
489
+ formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
377
490
  state: "ACTIVE",
378
491
  acceptedHeadEntryBytes: copyBytes(record.acceptedHeadEntryBytes),
379
492
  };
@@ -383,7 +496,7 @@ const durableStateFromCoreRecord = (
383
496
  record.comparisonCandidateEntryBytes,
384
497
  "unavailable comparison candidate",
385
498
  );
386
- if (record.acceptedAncestorDigest.byteLength !== 32) {
499
+ if (!hasExactUint8ArrayByteLength(record.acceptedAncestorDigest, 32)) {
387
500
  throw new Error("Unavailable accepted-ancestor digest must be 32 bytes");
388
501
  }
389
502
  if (
@@ -397,7 +510,7 @@ const durableStateFromCoreRecord = (
397
510
  assertEmptyBytes(record.forkChildEntryBytes0, "unavailable fork child 0");
398
511
  assertEmptyBytes(record.forkChildEntryBytes1, "unavailable fork child 1");
399
512
  return {
400
- formatVersion: FORMAT_VERSION,
513
+ formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
401
514
  state: "UNAVAILABLE",
402
515
  acceptedHeadEntryBytes: copyBytes(record.acceptedHeadEntryBytes),
403
516
  comparisonCandidateEntryBytes: copyBytes(
@@ -421,7 +534,7 @@ const durableStateFromCoreRecord = (
421
534
  assertEntryBytes(record.forkChildEntryBytes0, "fork child 0");
422
535
  assertEntryBytes(record.forkChildEntryBytes1, "fork child 1");
423
536
  return {
424
- formatVersion: FORMAT_VERSION,
537
+ formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
425
538
  state: "FORKED",
426
539
  commonParentEntryBytes: copyBytes(record.acceptedHeadEntryBytes),
427
540
  childEntryBytes: [
@@ -450,14 +563,16 @@ const decodeCanonicalPayload = <T>(
450
563
  maxBytes: number,
451
564
  label: string,
452
565
  ): T => {
453
- if (
454
- !(input instanceof Uint8Array) ||
455
- input.byteLength === 0 ||
456
- input.byteLength > maxBytes
457
- ) {
566
+ let byteLength: number;
567
+ try {
568
+ byteLength = exactUint8ArrayByteLength(input);
569
+ } catch {
570
+ byteLength = -1;
571
+ }
572
+ if (byteLength < 1 || byteLength > maxBytes) {
458
573
  throw new Error(`${label} exceeds its byte ceiling`);
459
574
  }
460
- const bytes = copyBytes(input);
575
+ const bytes = copyBytesWithLength(input, byteLength);
461
576
  const payload = deserialize(bytes, type);
462
577
  if (!equals(bytes, serialize(payload))) {
463
578
  throw new Error(`${label} is not canonical`);
@@ -475,10 +590,28 @@ const decodeGenerationPayload = (
475
590
  MAX_STATE_GENERATION_PAYLOAD_BYTES,
476
591
  "Policy-anchor state payload",
477
592
  );
593
+ const durableState = durableStateFromCoreRecord(payload.coreState);
594
+ if (durableState.state === "FORKED") {
595
+ if (
596
+ payload.forkObservationCount < 2 ||
597
+ payload.forkObservationCount > MAX_FORK_OBSERVATIONS_V2
598
+ ) {
599
+ throw new Error(
600
+ `Forked policy-anchor state must commit to 2-${MAX_FORK_OBSERVATIONS_V2} observations`,
601
+ );
602
+ }
603
+ } else if (
604
+ payload.forkObservationCount !== 0 ||
605
+ !equals(payload.forkObservationCommitment, ZERO_DIGEST)
606
+ ) {
607
+ throw new Error(
608
+ "Non-forked policy-anchor state must not contain a fork commitment",
609
+ );
610
+ }
478
611
  return {
479
612
  kind: "state",
480
613
  payload,
481
- durableState: durableStateFromCoreRecord(payload.coreState),
614
+ durableState,
482
615
  };
483
616
  }
484
617
  if (body.kind === GENERATION_KIND.FORK_OBSERVATION) {
@@ -500,19 +633,24 @@ const decodeGenerationRecord = (
500
633
  record: PolicyAnchorGenerationRecordV2;
501
634
  payload: DecodedGenerationPayloadV2;
502
635
  } => {
636
+ let byteLength: number;
637
+ try {
638
+ byteLength = exactUint8ArrayByteLength(input);
639
+ } catch {
640
+ byteLength = -1;
641
+ }
503
642
  if (
504
- !(input instanceof Uint8Array) ||
505
- input.byteLength === 0 ||
506
- input.byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES
643
+ byteLength < 1 ||
644
+ byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES
507
645
  ) {
508
646
  throw new Error("Policy-anchor generation record exceeds its byte ceiling");
509
647
  }
510
- const bytes = copyBytes(input);
648
+ const bytes = copyBytesWithLength(input, byteLength);
511
649
  const record = deserialize(bytes, PolicyAnchorGenerationRecordV2);
512
650
  if (!equals(bytes, serialize(record))) {
513
651
  throw new Error("Policy-anchor generation record is not canonical");
514
652
  }
515
- if (record.body.formatVersion !== FORMAT_VERSION) {
653
+ if (record.body.formatVersion !== ANCHOR_FORMAT_VERSION) {
516
654
  throw new Error("Unsupported policy-anchor generation format");
517
655
  }
518
656
  if (!equals(record.checksum, generationChecksum(record.body))) {
@@ -571,9 +709,10 @@ export class TrustedNetworkV2DurablePolicyReducer {
571
709
  private generation = 0n;
572
710
  private previousGenerationChecksum = copyBytes(ZERO_DIGEST);
573
711
  private durableCoreBytes?: Uint8Array;
574
- private observedHashes = new Set<string>();
575
712
  private operationTail: Promise<void> = Promise.resolve();
576
713
  private authorizationFences = 0;
714
+ private bufferedAdmissions = 0;
715
+ private bufferedAdmissionEntryBytes = 0;
577
716
  private terminalError?: Error;
578
717
 
579
718
  private constructor(properties: {
@@ -584,7 +723,6 @@ export class TrustedNetworkV2DurablePolicyReducer {
584
723
  generation?: bigint;
585
724
  previousGenerationChecksum?: Uint8Array;
586
725
  durableCoreBytes?: Uint8Array;
587
- observedHashes?: ReadonlySet<string>;
588
726
  }) {
589
727
  this.store = properties.store;
590
728
  this.durability = properties.durability;
@@ -603,7 +741,6 @@ export class TrustedNetworkV2DurablePolicyReducer {
603
741
  properties.durableCoreBytes === undefined
604
742
  ? undefined
605
743
  : copyBytes(properties.durableCoreBytes);
606
- this.observedHashes = new Set(properties.observedHashes);
607
744
  }
608
745
 
609
746
  static async open(
@@ -652,11 +789,15 @@ export class TrustedNetworkV2DurablePolicyReducer {
652
789
  if (!/^\d{20}$/.test(suffix)) {
653
790
  throw new Error("Malformed policy-anchor generation key");
654
791
  }
792
+ let byteLength: number;
793
+ try {
794
+ byteLength = exactUint8ArrayByteLength(input);
795
+ } catch {
796
+ byteLength = -1;
797
+ }
655
798
  if (
656
- !(input instanceof Uint8Array) ||
657
- input.byteLength === 0 ||
658
- input.byteLength >
659
- TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES
799
+ byteLength < 1 ||
800
+ byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES
660
801
  ) {
661
802
  throw new Error(
662
803
  "Policy-anchor generation record exceeds its byte ceiling",
@@ -687,8 +828,9 @@ export class TrustedNetworkV2DurablePolicyReducer {
687
828
  let core: TrustedNetworkV2PolicyReducer | undefined;
688
829
  let durableCoreBytes: Uint8Array | undefined;
689
830
  let forkEvidence: PolicyForkEvidenceV2 | undefined;
690
- const observedHashes = new Set<string>();
691
- const authenticatedEvidenceEntryByHash = new Map<string, Uint8Array>();
831
+ let expectedForkObservationCount: number | undefined;
832
+ let expectedForkObservationCommitment: Uint8Array | undefined;
833
+ const observedEntries = new Map<string, Uint8Array>();
692
834
  const canonicalChildren: CanonicalForkChildV2[] = [];
693
835
  try {
694
836
  for (let index = 0; index < generationKeys.length; index++) {
@@ -731,6 +873,10 @@ export class TrustedNetworkV2DurablePolicyReducer {
731
873
  latestCoreBytes = serialize(payload.payload.coreState);
732
874
  latestDurableState = payload.durableState;
733
875
  if (latestDurableState.state === "FORKED") {
876
+ expectedForkObservationCount = payload.payload.forkObservationCount;
877
+ expectedForkObservationCommitment = copyBytes(
878
+ payload.payload.forkObservationCommitment,
879
+ );
734
880
  core = await TrustedNetworkV2PolicyReducer.restore({
735
881
  ...coreProperties,
736
882
  durableState: latestDurableState,
@@ -748,17 +894,12 @@ export class TrustedNetworkV2DurablePolicyReducer {
748
894
  const hashKey = bytesKey(
749
895
  observationContentHash(child.entryBytes),
750
896
  );
751
- const retained = authenticatedEvidenceEntryByHash.get(hashKey);
752
- if (
753
- retained !== undefined &&
754
- !equals(retained, child.entryBytes)
755
- ) {
897
+ if (observedEntries.has(hashKey)) {
756
898
  throw new Error(
757
- "Policy fork-observation content hash collision",
899
+ "Duplicate policy fork observation in durable state",
758
900
  );
759
901
  }
760
- authenticatedEvidenceEntryByHash.set(hashKey, child.entryBytes);
761
- observedHashes.add(hashKey);
902
+ observedEntries.set(hashKey, copyBytes(child.entryBytes));
762
903
  retainCanonicalForkChild(canonicalChildren, child);
763
904
  }
764
905
  }
@@ -774,38 +915,36 @@ export class TrustedNetworkV2DurablePolicyReducer {
774
915
  }
775
916
  const entryBytes = payload.payload.entryBytes;
776
917
  const hashKey = bytesKey(observationContentHash(entryBytes));
777
- if (observedHashes.has(hashKey)) {
778
- const evidenceEntry = authenticatedEvidenceEntryByHash.get(hashKey);
779
- if (
780
- evidenceEntry !== undefined &&
781
- !equals(evidenceEntry, entryBytes)
782
- ) {
783
- throw new Error("Policy fork-observation content hash collision");
784
- }
785
- } else {
786
- const authenticated = await authenticatePolicySnapshotEntryV2(
787
- entryBytes,
788
- descriptor,
918
+ if (observedEntries.has(hashKey)) {
919
+ throw new Error("Duplicate policy fork-observation generation");
920
+ }
921
+ if (observedEntries.size >= MAX_FORK_OBSERVATIONS_V2) {
922
+ throw new Error(
923
+ `Policy fork evidence exceeds ${MAX_FORK_OBSERVATIONS_V2} children`,
789
924
  );
790
- assertOpenNotAborted(signal);
791
- if (
792
- authenticated.body.sequence !==
793
- forkEvidence.commonParent.sequence + 1n ||
794
- !equals(
795
- authenticated.body.previousPolicyDigest,
796
- forkEvidence.commonParent.digest,
797
- )
798
- ) {
799
- throw new Error(
800
- "Stored policy fork observation is not a direct child of the common parent",
801
- );
802
- }
803
- observedHashes.add(hashKey);
804
- retainCanonicalForkChild(canonicalChildren, {
805
- digest: authenticated.digest,
806
- entryBytes,
807
- });
808
925
  }
926
+ const authenticated = await authenticatePolicySnapshotEntryV2(
927
+ entryBytes,
928
+ descriptor,
929
+ );
930
+ assertOpenNotAborted(signal);
931
+ if (
932
+ authenticated.body.sequence !==
933
+ forkEvidence.commonParent.sequence + 1n ||
934
+ !equals(
935
+ authenticated.body.previousPolicyDigest,
936
+ forkEvidence.commonParent.digest,
937
+ )
938
+ ) {
939
+ throw new Error(
940
+ "Stored policy fork observation is not a direct child of the common parent",
941
+ );
942
+ }
943
+ observedEntries.set(hashKey, copyBytes(entryBytes));
944
+ retainCanonicalForkChild(canonicalChildren, {
945
+ digest: authenticated.digest,
946
+ entryBytes,
947
+ });
809
948
  }
810
949
  previousChecksum = copyBytes(record.checksum);
811
950
  }
@@ -832,9 +971,25 @@ export class TrustedNetworkV2DurablePolicyReducer {
832
971
  }
833
972
 
834
973
  if (latestDurableState?.state === "FORKED") {
835
- if (forkEvidence === undefined) {
974
+ if (
975
+ forkEvidence === undefined ||
976
+ expectedForkObservationCount === undefined ||
977
+ expectedForkObservationCommitment === undefined
978
+ ) {
836
979
  throw new Error("Restored forked policy anchor has no fork evidence");
837
980
  }
981
+ if (observedEntries.size !== expectedForkObservationCount) {
982
+ throw new Error(
983
+ `Policy fork evidence is incomplete: expected ${expectedForkObservationCount}, restored ${observedEntries.size}`,
984
+ );
985
+ }
986
+ const restoredCommitment = forkObservationCommitment(
987
+ expectedDescriptorHash,
988
+ canonicalForkObservationEntries(observedEntries.values()),
989
+ );
990
+ if (!equals(restoredCommitment, expectedForkObservationCommitment)) {
991
+ throw new Error("Policy fork evidence commitment mismatch");
992
+ }
838
993
  if (canonicalChildren.length !== 2) {
839
994
  throw new Error(
840
995
  "Stored policy fork evidence has fewer than two distinct children",
@@ -844,7 +999,7 @@ export class TrustedNetworkV2DurablePolicyReducer {
844
999
  PolicyReducerDurableStateV2,
845
1000
  { state: "FORKED" }
846
1001
  > = {
847
- formatVersion: FORMAT_VERSION,
1002
+ formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
848
1003
  state: "FORKED",
849
1004
  commonParentEntryBytes: copyBytes(
850
1005
  latestDurableState.commonParentEntryBytes,
@@ -876,7 +1031,7 @@ export class TrustedNetworkV2DurablePolicyReducer {
876
1031
  }
877
1032
  latestDurableState = normalizedForkState;
878
1033
  durableCoreBytes = serialize(coreRecordFromState(normalizedForkState));
879
- } else if (observedHashes.size !== 0) {
1034
+ } else if (observedEntries.size !== 0) {
880
1035
  throw new Error("Non-forked policy anchor has fork observations");
881
1036
  }
882
1037
 
@@ -888,7 +1043,6 @@ export class TrustedNetworkV2DurablePolicyReducer {
888
1043
  generation: highestGeneration,
889
1044
  previousGenerationChecksum: previousChecksum,
890
1045
  durableCoreBytes,
891
- observedHashes,
892
1046
  });
893
1047
  } catch (error) {
894
1048
  core?.abort();
@@ -897,9 +1051,11 @@ export class TrustedNetworkV2DurablePolicyReducer {
897
1051
  }
898
1052
 
899
1053
  get state(): "EMPTY" | "ACTIVE" | "UNAVAILABLE" | "FORKED" | "HALTED" {
900
- if (this.terminalError !== undefined || this.core.state === "HALTED") {
1054
+ if (this.terminalError !== undefined) {
901
1055
  return "HALTED";
902
1056
  }
1057
+ if (this.published.state === "FORKED") return "FORKED";
1058
+ if (this.core.state === "HALTED") return "HALTED";
903
1059
  return this.published.state;
904
1060
  }
905
1061
 
@@ -923,6 +1079,16 @@ export class TrustedNetworkV2DurablePolicyReducer {
923
1079
  return this.core.pendingDigests.map(copyBytes);
924
1080
  }
925
1081
 
1082
+ /** Internal diagnostics for the fixed pre-publication admission bound. */
1083
+ get bufferedAdmissionCount(): number {
1084
+ return this.bufferedAdmissions;
1085
+ }
1086
+
1087
+ /** Internal diagnostics for copied entry bytes awaiting settlement. */
1088
+ get bufferedAdmissionBytes(): number {
1089
+ return this.bufferedAdmissionEntryBytes;
1090
+ }
1091
+
926
1092
  /** Projection query only; use isAuthorized() for the fail-closed gate. */
927
1093
  rolesFor(subject: PublicSignKey): number {
928
1094
  return this.published.roles.get(keyId(subject)) ?? 0;
@@ -946,27 +1112,148 @@ export class TrustedNetworkV2DurablePolicyReducer {
946
1112
  }
947
1113
 
948
1114
  ingest(entryBytes: Uint8Array): Promise<PolicyAdmissionResultV2> {
949
- const captured =
950
- entryBytes instanceof Uint8Array &&
951
- entryBytes.byteLength <= TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES
952
- ? copyBytes(entryBytes)
953
- : entryBytes;
1115
+ if (this.terminalError !== undefined) {
1116
+ return Promise.reject(this.terminalError);
1117
+ }
1118
+ if (this.published.state === "FORKED") {
1119
+ return Promise.resolve(this.forkFailStopResult());
1120
+ }
1121
+ if (this.core.state === "HALTED") {
1122
+ return Promise.resolve(this.lifecycleHaltedResult());
1123
+ }
1124
+ let reservedEntryBytes: number;
1125
+ try {
1126
+ reservedEntryBytes = exactUint8ArrayByteLength(entryBytes);
1127
+ } catch {
1128
+ return Promise.resolve(
1129
+ this.immediateAdmissionResult(
1130
+ "rejected",
1131
+ "Policy snapshot entry must be a Uint8Array",
1132
+ ),
1133
+ );
1134
+ }
1135
+ if (reservedEntryBytes > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES) {
1136
+ return Promise.resolve(
1137
+ this.immediateAdmissionResult(
1138
+ "rejected",
1139
+ `Policy snapshot entry exceeds ${TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES} bytes`,
1140
+ ),
1141
+ );
1142
+ }
1143
+ if (!this.reserveAdmission(reservedEntryBytes)) {
1144
+ return Promise.resolve(
1145
+ this.immediateAdmissionResult(
1146
+ "capacity",
1147
+ "Durable policy admission queue is at its fixed capacity",
1148
+ this.core.pendingCount,
1149
+ this.core.pendingBytes,
1150
+ ),
1151
+ );
1152
+ }
1153
+ let captured: Uint8Array;
1154
+ try {
1155
+ captured = copyBytesWithLength(entryBytes, reservedEntryBytes);
1156
+ } catch (error) {
1157
+ this.releaseAdmission(reservedEntryBytes);
1158
+ return Promise.reject(error);
1159
+ }
954
1160
  return this.enqueue(async () => {
1161
+ if (this.published.state === "FORKED") {
1162
+ return this.forkFailStopResult();
1163
+ }
1164
+ if (this.core.state === "HALTED") {
1165
+ return this.lifecycleHaltedResult();
1166
+ }
955
1167
  const result = await this.core.ingest(captured);
956
1168
  await this.persistCorePublication(result);
957
1169
  return result;
958
- });
1170
+ }, reservedEntryBytes);
959
1171
  }
960
1172
 
961
1173
  retryUnavailable(): Promise<PolicyAdmissionResultV2> {
1174
+ if (this.terminalError !== undefined) {
1175
+ return Promise.reject(this.terminalError);
1176
+ }
1177
+ if (this.published.state === "FORKED") {
1178
+ return Promise.resolve(this.forkFailStopResult());
1179
+ }
1180
+ if (this.core.state === "HALTED") {
1181
+ return Promise.resolve(this.lifecycleHaltedResult());
1182
+ }
1183
+ if (!this.reserveAdmission(0)) {
1184
+ return Promise.resolve(
1185
+ this.immediateAdmissionResult(
1186
+ "capacity",
1187
+ "Durable policy admission queue is at its fixed capacity",
1188
+ this.core.pendingCount,
1189
+ this.core.pendingBytes,
1190
+ ),
1191
+ );
1192
+ }
962
1193
  return this.enqueue(async () => {
1194
+ if (this.published.state === "FORKED") {
1195
+ return this.forkFailStopResult();
1196
+ }
1197
+ if (this.core.state === "HALTED") {
1198
+ return this.lifecycleHaltedResult();
1199
+ }
963
1200
  const result = await this.core.retryUnavailable();
964
1201
  await this.persistCorePublication(result);
965
1202
  return result;
966
- });
1203
+ }, 0);
1204
+ }
1205
+
1206
+ private forkFailStopResult(): PolicyAdmissionResultV2 {
1207
+ return {
1208
+ status: "halted",
1209
+ reason: "Policy reducer is halted by authority equivocation",
1210
+ fetchHints: [],
1211
+ pendingCount: 0,
1212
+ pendingBytes: 0,
1213
+ };
1214
+ }
1215
+
1216
+ private lifecycleHaltedResult(): PolicyAdmissionResultV2 {
1217
+ return {
1218
+ status: "halted",
1219
+ reason: "Policy reducer lifecycle is aborted",
1220
+ fetchHints: [],
1221
+ pendingCount: this.core.pendingCount,
1222
+ pendingBytes: this.core.pendingBytes,
1223
+ };
1224
+ }
1225
+
1226
+ private immediateAdmissionResult(
1227
+ status: "capacity" | "rejected",
1228
+ reason: string,
1229
+ pendingCount = 0,
1230
+ pendingBytes = 0,
1231
+ ): PolicyAdmissionResultV2 {
1232
+ return { status, reason, fetchHints: [], pendingCount, pendingBytes };
1233
+ }
1234
+
1235
+ private reserveAdmission(entryBytes: number): boolean {
1236
+ if (
1237
+ this.bufferedAdmissions >= TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES ||
1238
+ entryBytes >
1239
+ MAX_QUEUED_POLICY_ENTRY_BYTES_V2 - this.bufferedAdmissionEntryBytes
1240
+ ) {
1241
+ return false;
1242
+ }
1243
+ this.bufferedAdmissions += 1;
1244
+ this.bufferedAdmissionEntryBytes += entryBytes;
1245
+ return true;
1246
+ }
1247
+
1248
+ private releaseAdmission(entryBytes: number): void {
1249
+ this.bufferedAdmissions -= 1;
1250
+ this.bufferedAdmissionEntryBytes -= entryBytes;
967
1251
  }
968
1252
 
969
- private enqueue<T>(operation: () => Promise<T>): Promise<T> {
1253
+ private enqueue<T>(
1254
+ operation: () => Promise<T>,
1255
+ bufferedEntryBytes: number,
1256
+ ): Promise<T> {
970
1257
  this.authorizationFences++;
971
1258
  const result = this.operationTail.then(async () => {
972
1259
  if (this.terminalError !== undefined) throw this.terminalError;
@@ -982,6 +1269,7 @@ export class TrustedNetworkV2DurablePolicyReducer {
982
1269
  );
983
1270
  return result.finally(() => {
984
1271
  this.authorizationFences--;
1272
+ this.releaseAdmission(bufferedEntryBytes);
985
1273
  });
986
1274
  }
987
1275
 
@@ -1010,13 +1298,9 @@ export class TrustedNetworkV2DurablePolicyReducer {
1010
1298
 
1011
1299
  const coreRecord = coreRecordFromState(durableState);
1012
1300
  const coreBytes = serialize(coreRecord);
1013
- // FORKED is terminal. Once its common-parent anchor exists, later child
1014
- // proofs are observation deltas even when they change the live canonical
1015
- // pair; reopen derives that pair from the complete delta history.
1016
1301
  const stateChanged =
1017
- !(this.published.state === "FORKED" && durableState.state === "FORKED") &&
1018
- (this.durableCoreBytes === undefined ||
1019
- !equals(this.durableCoreBytes, coreBytes));
1302
+ this.durableCoreBytes === undefined ||
1303
+ !equals(this.durableCoreBytes, coreBytes);
1020
1304
  const suppliedObservations = new Map<string, Uint8Array>();
1021
1305
  for (const proof of result.forkObservations ?? []) {
1022
1306
  const contentHash = observationContentHash(proof.entryBytes);
@@ -1027,31 +1311,45 @@ export class TrustedNetworkV2DurablePolicyReducer {
1027
1311
  }
1028
1312
  suppliedObservations.set(hashKey, copyBytes(proof.entryBytes));
1029
1313
  }
1314
+ let committedForkObservationCount = 0;
1315
+ let committedForkObservationDigest = copyBytes(ZERO_DIGEST);
1030
1316
  if (durableState.state !== "FORKED") {
1031
- if (suppliedObservations.size !== 0 || this.observedHashes.size !== 0) {
1317
+ if (suppliedObservations.size !== 0) {
1032
1318
  throw this.halt("Only a forked policy anchor may contain observations");
1033
1319
  }
1034
1320
  } else {
1321
+ if (this.published.state === "FORKED" || !stateChanged) {
1322
+ throw this.halt("A durable fork transition may be published only once");
1323
+ }
1324
+ if (
1325
+ suppliedObservations.size < 2 ||
1326
+ suppliedObservations.size > MAX_FORK_OBSERVATIONS_V2
1327
+ ) {
1328
+ throw this.halt(
1329
+ `A durable fork transition must contain 2-${MAX_FORK_OBSERVATIONS_V2} distinct observations`,
1330
+ );
1331
+ }
1332
+ const completeObservationEntries = canonicalForkObservationEntries(
1333
+ suppliedObservations.values(),
1334
+ );
1335
+ committedForkObservationCount = completeObservationEntries.length;
1336
+ committedForkObservationDigest = forkObservationCommitment(
1337
+ this.descriptorHash,
1338
+ completeObservationEntries,
1339
+ );
1035
1340
  for (const entryBytes of durableState.childEntryBytes) {
1036
1341
  const hashKey = bytesKey(observationContentHash(entryBytes));
1037
- if (stateChanged) suppliedObservations.delete(hashKey);
1038
- else if (
1039
- !this.observedHashes.has(hashKey) &&
1040
- !suppliedObservations.has(hashKey)
1041
- ) {
1342
+ if (!suppliedObservations.delete(hashKey)) {
1042
1343
  throw this.halt(
1043
1344
  "Published fork state is missing a canonical observation",
1044
1345
  );
1045
1346
  }
1046
1347
  }
1047
1348
  }
1048
- for (const hashKey of this.observedHashes) {
1049
- suppliedObservations.delete(hashKey);
1050
- }
1051
1349
 
1052
- const observationEntries = [...suppliedObservations.entries()]
1053
- .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
1054
- .map(([hashKey, entryBytes]) => ({ hashKey, entryBytes }));
1350
+ const observationEntries = canonicalForkObservationEntries(
1351
+ suppliedObservations.values(),
1352
+ );
1055
1353
  const recordCount = (stateChanged ? 1 : 0) + observationEntries.length;
1056
1354
  if (recordCount === 0) return;
1057
1355
  if (BigInt(recordCount) > MAX_U64 - this.generation) {
@@ -1060,22 +1358,24 @@ export class TrustedNetworkV2DurablePolicyReducer {
1060
1358
 
1061
1359
  let nextGeneration = this.generation;
1062
1360
  let previousChecksum = copyBytes(this.previousGenerationChecksum);
1063
- const stagedObservedHashes = new Set(this.observedHashes);
1064
1361
  const drafts: Array<{
1065
1362
  kind: number;
1066
1363
  payloadBytes: Uint8Array;
1067
- applyObservationHash?: string;
1068
1364
  }> = [];
1069
1365
  if (stateChanged) {
1070
1366
  const payloadBytes = serialize(
1071
- new PolicyAnchorStateGenerationPayloadV2({ coreState: coreRecord }),
1367
+ new PolicyAnchorStateGenerationPayloadV2({
1368
+ coreState: coreRecord,
1369
+ forkObservationCount: committedForkObservationCount,
1370
+ forkObservationCommitment: committedForkObservationDigest,
1371
+ }),
1072
1372
  );
1073
1373
  if (payloadBytes.byteLength > MAX_STATE_GENERATION_PAYLOAD_BYTES) {
1074
1374
  throw this.halt("Policy-anchor state payload exceeds its byte ceiling");
1075
1375
  }
1076
1376
  drafts.push({ kind: GENERATION_KIND.STATE, payloadBytes });
1077
1377
  }
1078
- for (const { hashKey, entryBytes } of observationEntries) {
1378
+ for (const entryBytes of observationEntries) {
1079
1379
  const payloadBytes = serialize(
1080
1380
  new PolicyAnchorObservationGenerationPayloadV2({
1081
1381
  entryBytes: copyBytes(entryBytes),
@@ -1089,25 +1389,13 @@ export class TrustedNetworkV2DurablePolicyReducer {
1089
1389
  drafts.push({
1090
1390
  kind: GENERATION_KIND.FORK_OBSERVATION,
1091
1391
  payloadBytes,
1092
- applyObservationHash: hashKey,
1093
1392
  });
1094
1393
  }
1095
1394
 
1096
1395
  for (const draft of drafts) {
1097
1396
  nextGeneration += 1n;
1098
- if (draft.kind === GENERATION_KIND.STATE) {
1099
- if (durableState.state === "FORKED") {
1100
- for (const entryBytes of durableState.childEntryBytes) {
1101
- stagedObservedHashes.add(
1102
- bytesKey(observationContentHash(entryBytes)),
1103
- );
1104
- }
1105
- }
1106
- } else {
1107
- stagedObservedHashes.add(draft.applyObservationHash!);
1108
- }
1109
1397
  const body = new PolicyAnchorGenerationBodyV2({
1110
- formatVersion: FORMAT_VERSION,
1398
+ formatVersion: ANCHOR_FORMAT_VERSION,
1111
1399
  generation: nextGeneration,
1112
1400
  descriptorDigest: copyBytes(this.descriptorHash),
1113
1401
  previousGenerationChecksum: copyBytes(previousChecksum),
@@ -1142,7 +1430,6 @@ export class TrustedNetworkV2DurablePolicyReducer {
1142
1430
  this.generation = nextGeneration;
1143
1431
  this.previousGenerationChecksum = previousChecksum;
1144
1432
  this.durableCoreBytes = copyBytes(coreBytes);
1145
- this.observedHashes = stagedObservedHashes;
1146
1433
  try {
1147
1434
  this.published = publishedFromReducer(this.core);
1148
1435
  } catch (error) {