@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.
@@ -35,7 +35,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
35
35
  import { deserialize, field, fixedArray, serialize, variant, } from "@dao-xyz/borsh";
36
36
  import { PublicSignKey, sha256Sync } from "@peerbit/crypto";
37
37
  import { compare, concat, equals } from "uint8arrays";
38
- import { TrustedNetworkV2PolicyReducer, authenticatePolicySnapshotEntryV2, } from "./v2-policy-engine.js";
38
+ import { TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES, TrustedNetworkV2PolicyReducer, authenticatePolicySnapshotEntryV2, } from "./v2-policy-engine.js";
39
39
  import { NetworkDescriptorV2, PolicySubjectBindingV2, TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS, TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES, assertNetworkDescriptorV2, } from "./v2.js";
40
40
  /**
41
41
  * Internal crash-safe policy-anchor storage format.
@@ -48,13 +48,20 @@ import { NetworkDescriptorV2, PolicySubjectBindingV2, TRUSTED_NETWORK_V2_KNOWN_R
48
48
  export const TRUSTED_NETWORK_V2_POLICY_ANCHOR_STORE_OWNER = "peerbit/trusted-network/v2/policy-anchor/v1";
49
49
  export const TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES = TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 4;
50
50
  const GENERATION_KEY_PREFIX = `${TRUSTED_NETWORK_V2_POLICY_ANCHOR_STORE_OWNER}/generation/`;
51
- const FORMAT_VERSION = 1;
51
+ const ANCHOR_FORMAT_VERSION = 2;
52
+ const REDUCER_DURABLE_FORMAT_VERSION = 1;
52
53
  const MAX_U64 = 0xffffffffffffffffn;
53
54
  const MAX_UNAVAILABLE_REASON_LENGTH = 512;
55
+ // The core admits at most 64 pending policies. A fork transition can surface
56
+ // that complete set plus its accepted and triggering children. Once the
57
+ // transition is durable the wrapper stops policy admission entirely.
58
+ const MAX_FORK_OBSERVATIONS_V2 = TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES + 2;
59
+ const MAX_QUEUED_POLICY_ENTRY_BYTES_V2 = TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 2;
54
60
  const ZERO_DIGEST = new Uint8Array(32);
55
61
  const textEncoder = new TextEncoder();
56
62
  const DESCRIPTOR_DIGEST_DOMAIN = textEncoder.encode("peerbit/trusted-network/v2/policy-anchor/descriptor/v1");
57
63
  const GENERATION_CHECKSUM_DOMAIN = textEncoder.encode("peerbit/trusted-network/v2/policy-anchor/generation/v1");
64
+ const FORK_OBSERVATION_COMMITMENT_DOMAIN = textEncoder.encode("peerbit/trusted-network/v2/policy-anchor/fork-observations/v1");
58
65
  const ANCHOR_STATE = Object.freeze({
59
66
  ACTIVE: 1,
60
67
  UNAVAILABLE: 2,
@@ -138,20 +145,32 @@ let PolicyAnchorStateGenerationPayloadV2 = (() => {
138
145
  let _coreState_decorators;
139
146
  let _coreState_initializers = [];
140
147
  let _coreState_extraInitializers = [];
148
+ let _forkObservationCount_decorators;
149
+ let _forkObservationCount_initializers = [];
150
+ let _forkObservationCount_extraInitializers = [];
151
+ let _forkObservationCommitment_decorators;
152
+ let _forkObservationCommitment_initializers = [];
153
+ let _forkObservationCommitment_extraInitializers = [];
141
154
  var PolicyAnchorStateGenerationPayloadV2 = class {
142
155
  static { _classThis = this; }
143
156
  static {
144
157
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
145
158
  _coreState_decorators = [field({ type: PolicyAnchorCoreStateRecordV2 })];
159
+ _forkObservationCount_decorators = [field({ type: "u8" })];
160
+ _forkObservationCommitment_decorators = [field({ type: fixedArray("u8", 32) })];
146
161
  __esDecorate(null, null, _coreState_decorators, { kind: "field", name: "coreState", static: false, private: false, access: { has: obj => "coreState" in obj, get: obj => obj.coreState, set: (obj, value) => { obj.coreState = value; } }, metadata: _metadata }, _coreState_initializers, _coreState_extraInitializers);
162
+ __esDecorate(null, null, _forkObservationCount_decorators, { kind: "field", name: "forkObservationCount", static: false, private: false, access: { has: obj => "forkObservationCount" in obj, get: obj => obj.forkObservationCount, set: (obj, value) => { obj.forkObservationCount = value; } }, metadata: _metadata }, _forkObservationCount_initializers, _forkObservationCount_extraInitializers);
163
+ __esDecorate(null, null, _forkObservationCommitment_decorators, { kind: "field", name: "forkObservationCommitment", static: false, private: false, access: { has: obj => "forkObservationCommitment" in obj, get: obj => obj.forkObservationCommitment, set: (obj, value) => { obj.forkObservationCommitment = value; } }, metadata: _metadata }, _forkObservationCommitment_initializers, _forkObservationCommitment_extraInitializers);
147
164
  __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
148
165
  PolicyAnchorStateGenerationPayloadV2 = _classThis = _classDescriptor.value;
149
166
  if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
150
167
  __runInitializers(_classThis, _classExtraInitializers);
151
168
  }
152
169
  coreState = __runInitializers(this, _coreState_initializers, void 0);
170
+ forkObservationCount = (__runInitializers(this, _coreState_extraInitializers), __runInitializers(this, _forkObservationCount_initializers, void 0));
171
+ forkObservationCommitment = (__runInitializers(this, _forkObservationCount_extraInitializers), __runInitializers(this, _forkObservationCommitment_initializers, void 0));
153
172
  constructor(properties) {
154
- __runInitializers(this, _coreState_extraInitializers);
173
+ __runInitializers(this, _forkObservationCommitment_extraInitializers);
155
174
  if (properties)
156
175
  Object.assign(this, properties);
157
176
  }
@@ -278,7 +297,39 @@ let PolicyAnchorGenerationRecordV2 = (() => {
278
297
  };
279
298
  return PolicyAnchorGenerationRecordV2 = _classThis;
280
299
  })();
281
- const copyBytes = (bytes) => Uint8Array.from(bytes);
300
+ const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype);
301
+ const TYPED_ARRAY_BYTE_LENGTH = Object.getOwnPropertyDescriptor(TYPED_ARRAY_PROTOTYPE, "byteLength").get;
302
+ const TYPED_ARRAY_TAG = Object.getOwnPropertyDescriptor(TYPED_ARRAY_PROTOTYPE, Symbol.toStringTag).get;
303
+ const ARRAY_BUFFER_IS_VIEW = ArrayBuffer.isView;
304
+ const UINT8_ARRAY_SET = Uint8Array.prototype.set;
305
+ const exactUint8ArrayByteLength = (input) => {
306
+ if (!ARRAY_BUFFER_IS_VIEW(input) ||
307
+ TYPED_ARRAY_TAG.call(input) !== "Uint8Array") {
308
+ throw new TypeError("Expected a genuine Uint8Array");
309
+ }
310
+ const byteLength = TYPED_ARRAY_BYTE_LENGTH.call(input);
311
+ if (byteLength === 0) {
312
+ UINT8_ARRAY_SET.call(new Uint8Array(0), input);
313
+ }
314
+ return byteLength;
315
+ };
316
+ const copyBytesWithLength = (bytes, byteLength) => {
317
+ const copy = new Uint8Array(byteLength);
318
+ UINT8_ARRAY_SET.call(copy, bytes);
319
+ return copy;
320
+ };
321
+ const copyBytes = (bytes) => {
322
+ const byteLength = exactUint8ArrayByteLength(bytes);
323
+ return copyBytesWithLength(bytes, byteLength);
324
+ };
325
+ const hasExactUint8ArrayByteLength = (input, expected) => {
326
+ try {
327
+ return exactUint8ArrayByteLength(input) === expected;
328
+ }
329
+ catch {
330
+ return false;
331
+ }
332
+ };
282
333
  const bytesKey = (bytes) => {
283
334
  let key = "";
284
335
  for (const byte of bytes)
@@ -314,6 +365,32 @@ const keyId = (key) => bytesKey(serialize(key));
314
365
  const descriptorDigest = (descriptor) => sha256Sync(concat([DESCRIPTOR_DIGEST_DOMAIN, serialize(descriptor)]));
315
366
  const generationChecksum = (body) => sha256Sync(concat([GENERATION_CHECKSUM_DOMAIN, serialize(body)]));
316
367
  const observationContentHash = (entryBytes) => sha256Sync(entryBytes);
368
+ const u32Bytes = (value) => {
369
+ const bytes = new Uint8Array(4);
370
+ new DataView(bytes.buffer).setUint32(0, value, false);
371
+ return bytes;
372
+ };
373
+ const canonicalForkObservationEntries = (entries) => [...entries]
374
+ .map((entryBytes) => ({
375
+ entryBytes: copyBytes(entryBytes),
376
+ hash: observationContentHash(entryBytes),
377
+ }))
378
+ .sort((left, right) => {
379
+ const hashOrder = compare(left.hash, right.hash);
380
+ return hashOrder === 0
381
+ ? compare(left.entryBytes, right.entryBytes)
382
+ : hashOrder;
383
+ })
384
+ .map(({ entryBytes }) => entryBytes);
385
+ const forkObservationCommitment = (descriptorHash, canonicalEntries) => sha256Sync(concat([
386
+ FORK_OBSERVATION_COMMITMENT_DOMAIN,
387
+ descriptorHash,
388
+ Uint8Array.of(canonicalEntries.length),
389
+ ...canonicalEntries.flatMap((entryBytes) => [
390
+ u32Bytes(entryBytes.byteLength),
391
+ entryBytes,
392
+ ]),
393
+ ]));
317
394
  const generationKey = (generation) => `${GENERATION_KEY_PREFIX}${generation.toString().padStart(20, "0")}`;
318
395
  const assertOpenNotAborted = (signal) => {
319
396
  if (signal?.aborted) {
@@ -321,9 +398,15 @@ const assertOpenNotAborted = (signal) => {
321
398
  }
322
399
  };
323
400
  const assertEntryBytes = (bytes, label) => {
324
- if (!(bytes instanceof Uint8Array) ||
325
- bytes.byteLength === 0 ||
326
- bytes.byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES) {
401
+ let byteLength;
402
+ try {
403
+ byteLength = exactUint8ArrayByteLength(bytes);
404
+ }
405
+ catch {
406
+ byteLength = -1;
407
+ }
408
+ if (byteLength < 1 ||
409
+ byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES) {
327
410
  throw new Error(`${label} must contain 1-${TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES} bytes`);
328
411
  }
329
412
  };
@@ -349,7 +432,7 @@ const retainCanonicalForkChild = (children, candidate) => {
349
432
  children.length = 2;
350
433
  };
351
434
  const assertEmptyBytes = (bytes, label) => {
352
- if (!(bytes instanceof Uint8Array) || bytes.byteLength !== 0) {
435
+ if (!hasExactUint8ArrayByteLength(bytes, 0)) {
353
436
  throw new Error(`${label} must be empty`);
354
437
  }
355
438
  };
@@ -399,14 +482,14 @@ const durableStateFromCoreRecord = (record) => {
399
482
  assertEmptyBytes(record.forkChildEntryBytes0, "active fork child 0");
400
483
  assertEmptyBytes(record.forkChildEntryBytes1, "active fork child 1");
401
484
  return {
402
- formatVersion: FORMAT_VERSION,
485
+ formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
403
486
  state: "ACTIVE",
404
487
  acceptedHeadEntryBytes: copyBytes(record.acceptedHeadEntryBytes),
405
488
  };
406
489
  }
407
490
  if (record.state === ANCHOR_STATE.UNAVAILABLE) {
408
491
  assertEntryBytes(record.comparisonCandidateEntryBytes, "unavailable comparison candidate");
409
- if (record.acceptedAncestorDigest.byteLength !== 32) {
492
+ if (!hasExactUint8ArrayByteLength(record.acceptedAncestorDigest, 32)) {
410
493
  throw new Error("Unavailable accepted-ancestor digest must be 32 bytes");
411
494
  }
412
495
  if (record.unavailableReason.length === 0 ||
@@ -416,7 +499,7 @@ const durableStateFromCoreRecord = (record) => {
416
499
  assertEmptyBytes(record.forkChildEntryBytes0, "unavailable fork child 0");
417
500
  assertEmptyBytes(record.forkChildEntryBytes1, "unavailable fork child 1");
418
501
  return {
419
- formatVersion: FORMAT_VERSION,
502
+ formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
420
503
  state: "UNAVAILABLE",
421
504
  acceptedHeadEntryBytes: copyBytes(record.acceptedHeadEntryBytes),
422
505
  comparisonCandidateEntryBytes: copyBytes(record.comparisonCandidateEntryBytes),
@@ -435,7 +518,7 @@ const durableStateFromCoreRecord = (record) => {
435
518
  assertEntryBytes(record.forkChildEntryBytes0, "fork child 0");
436
519
  assertEntryBytes(record.forkChildEntryBytes1, "fork child 1");
437
520
  return {
438
- formatVersion: FORMAT_VERSION,
521
+ formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
439
522
  state: "FORKED",
440
523
  commonParentEntryBytes: copyBytes(record.acceptedHeadEntryBytes),
441
524
  childEntryBytes: [
@@ -447,12 +530,17 @@ const durableStateFromCoreRecord = (record) => {
447
530
  throw new Error("Unknown durable policy-anchor state");
448
531
  };
449
532
  const decodeCanonicalPayload = (input, type, maxBytes, label) => {
450
- if (!(input instanceof Uint8Array) ||
451
- input.byteLength === 0 ||
452
- input.byteLength > maxBytes) {
533
+ let byteLength;
534
+ try {
535
+ byteLength = exactUint8ArrayByteLength(input);
536
+ }
537
+ catch {
538
+ byteLength = -1;
539
+ }
540
+ if (byteLength < 1 || byteLength > maxBytes) {
453
541
  throw new Error(`${label} exceeds its byte ceiling`);
454
542
  }
455
- const bytes = copyBytes(input);
543
+ const bytes = copyBytesWithLength(input, byteLength);
456
544
  const payload = deserialize(bytes, type);
457
545
  if (!equals(bytes, serialize(payload))) {
458
546
  throw new Error(`${label} is not canonical`);
@@ -462,10 +550,21 @@ const decodeCanonicalPayload = (input, type, maxBytes, label) => {
462
550
  const decodeGenerationPayload = (body) => {
463
551
  if (body.kind === GENERATION_KIND.STATE) {
464
552
  const payload = decodeCanonicalPayload(body.payloadBytes, PolicyAnchorStateGenerationPayloadV2, MAX_STATE_GENERATION_PAYLOAD_BYTES, "Policy-anchor state payload");
553
+ const durableState = durableStateFromCoreRecord(payload.coreState);
554
+ if (durableState.state === "FORKED") {
555
+ if (payload.forkObservationCount < 2 ||
556
+ payload.forkObservationCount > MAX_FORK_OBSERVATIONS_V2) {
557
+ throw new Error(`Forked policy-anchor state must commit to 2-${MAX_FORK_OBSERVATIONS_V2} observations`);
558
+ }
559
+ }
560
+ else if (payload.forkObservationCount !== 0 ||
561
+ !equals(payload.forkObservationCommitment, ZERO_DIGEST)) {
562
+ throw new Error("Non-forked policy-anchor state must not contain a fork commitment");
563
+ }
465
564
  return {
466
565
  kind: "state",
467
566
  payload,
468
- durableState: durableStateFromCoreRecord(payload.coreState),
567
+ durableState,
469
568
  };
470
569
  }
471
570
  if (body.kind === GENERATION_KIND.FORK_OBSERVATION) {
@@ -476,17 +575,23 @@ const decodeGenerationPayload = (body) => {
476
575
  throw new Error("Unknown policy-anchor generation kind");
477
576
  };
478
577
  const decodeGenerationRecord = (input) => {
479
- if (!(input instanceof Uint8Array) ||
480
- input.byteLength === 0 ||
481
- input.byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES) {
578
+ let byteLength;
579
+ try {
580
+ byteLength = exactUint8ArrayByteLength(input);
581
+ }
582
+ catch {
583
+ byteLength = -1;
584
+ }
585
+ if (byteLength < 1 ||
586
+ byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES) {
482
587
  throw new Error("Policy-anchor generation record exceeds its byte ceiling");
483
588
  }
484
- const bytes = copyBytes(input);
589
+ const bytes = copyBytesWithLength(input, byteLength);
485
590
  const record = deserialize(bytes, PolicyAnchorGenerationRecordV2);
486
591
  if (!equals(bytes, serialize(record))) {
487
592
  throw new Error("Policy-anchor generation record is not canonical");
488
593
  }
489
- if (record.body.formatVersion !== FORMAT_VERSION) {
594
+ if (record.body.formatVersion !== ANCHOR_FORMAT_VERSION) {
490
595
  throw new Error("Unsupported policy-anchor generation format");
491
596
  }
492
597
  if (!equals(record.checksum, generationChecksum(record.body))) {
@@ -537,9 +642,10 @@ export class TrustedNetworkV2DurablePolicyReducer {
537
642
  generation = 0n;
538
643
  previousGenerationChecksum = copyBytes(ZERO_DIGEST);
539
644
  durableCoreBytes;
540
- observedHashes = new Set();
541
645
  operationTail = Promise.resolve();
542
646
  authorizationFences = 0;
647
+ bufferedAdmissions = 0;
648
+ bufferedAdmissionEntryBytes = 0;
543
649
  terminalError;
544
650
  constructor(properties) {
545
651
  this.store = properties.store;
@@ -554,7 +660,6 @@ export class TrustedNetworkV2DurablePolicyReducer {
554
660
  properties.durableCoreBytes === undefined
555
661
  ? undefined
556
662
  : copyBytes(properties.durableCoreBytes);
557
- this.observedHashes = new Set(properties.observedHashes);
558
663
  }
559
664
  static async open(options) {
560
665
  assertNetworkDescriptorV2(options.descriptor);
@@ -591,10 +696,15 @@ export class TrustedNetworkV2DurablePolicyReducer {
591
696
  if (!/^\d{20}$/.test(suffix)) {
592
697
  throw new Error("Malformed policy-anchor generation key");
593
698
  }
594
- if (!(input instanceof Uint8Array) ||
595
- input.byteLength === 0 ||
596
- input.byteLength >
597
- TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES) {
699
+ let byteLength;
700
+ try {
701
+ byteLength = exactUint8ArrayByteLength(input);
702
+ }
703
+ catch {
704
+ byteLength = -1;
705
+ }
706
+ if (byteLength < 1 ||
707
+ byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES) {
598
708
  throw new Error("Policy-anchor generation record exceeds its byte ceiling");
599
709
  }
600
710
  const generation = BigInt(suffix);
@@ -617,8 +727,9 @@ export class TrustedNetworkV2DurablePolicyReducer {
617
727
  let core;
618
728
  let durableCoreBytes;
619
729
  let forkEvidence;
620
- const observedHashes = new Set();
621
- const authenticatedEvidenceEntryByHash = new Map();
730
+ let expectedForkObservationCount;
731
+ let expectedForkObservationCommitment;
732
+ const observedEntries = new Map();
622
733
  const canonicalChildren = [];
623
734
  try {
624
735
  for (let index = 0; index < generationKeys.length; index++) {
@@ -652,6 +763,8 @@ export class TrustedNetworkV2DurablePolicyReducer {
652
763
  latestCoreBytes = serialize(payload.payload.coreState);
653
764
  latestDurableState = payload.durableState;
654
765
  if (latestDurableState.state === "FORKED") {
766
+ expectedForkObservationCount = payload.payload.forkObservationCount;
767
+ expectedForkObservationCommitment = copyBytes(payload.payload.forkObservationCommitment);
655
768
  core = await TrustedNetworkV2PolicyReducer.restore({
656
769
  ...coreProperties,
657
770
  durableState: latestDurableState,
@@ -665,13 +778,10 @@ export class TrustedNetworkV2DurablePolicyReducer {
665
778
  }
666
779
  for (const child of forkEvidence.children) {
667
780
  const hashKey = bytesKey(observationContentHash(child.entryBytes));
668
- const retained = authenticatedEvidenceEntryByHash.get(hashKey);
669
- if (retained !== undefined &&
670
- !equals(retained, child.entryBytes)) {
671
- throw new Error("Policy fork-observation content hash collision");
781
+ if (observedEntries.has(hashKey)) {
782
+ throw new Error("Duplicate policy fork observation in durable state");
672
783
  }
673
- authenticatedEvidenceEntryByHash.set(hashKey, child.entryBytes);
674
- observedHashes.add(hashKey);
784
+ observedEntries.set(hashKey, copyBytes(child.entryBytes));
675
785
  retainCanonicalForkChild(canonicalChildren, child);
676
786
  }
677
787
  }
@@ -684,27 +794,24 @@ export class TrustedNetworkV2DurablePolicyReducer {
684
794
  }
685
795
  const entryBytes = payload.payload.entryBytes;
686
796
  const hashKey = bytesKey(observationContentHash(entryBytes));
687
- if (observedHashes.has(hashKey)) {
688
- const evidenceEntry = authenticatedEvidenceEntryByHash.get(hashKey);
689
- if (evidenceEntry !== undefined &&
690
- !equals(evidenceEntry, entryBytes)) {
691
- throw new Error("Policy fork-observation content hash collision");
692
- }
797
+ if (observedEntries.has(hashKey)) {
798
+ throw new Error("Duplicate policy fork-observation generation");
693
799
  }
694
- else {
695
- const authenticated = await authenticatePolicySnapshotEntryV2(entryBytes, descriptor);
696
- assertOpenNotAborted(signal);
697
- if (authenticated.body.sequence !==
698
- forkEvidence.commonParent.sequence + 1n ||
699
- !equals(authenticated.body.previousPolicyDigest, forkEvidence.commonParent.digest)) {
700
- throw new Error("Stored policy fork observation is not a direct child of the common parent");
701
- }
702
- observedHashes.add(hashKey);
703
- retainCanonicalForkChild(canonicalChildren, {
704
- digest: authenticated.digest,
705
- entryBytes,
706
- });
800
+ if (observedEntries.size >= MAX_FORK_OBSERVATIONS_V2) {
801
+ throw new Error(`Policy fork evidence exceeds ${MAX_FORK_OBSERVATIONS_V2} children`);
707
802
  }
803
+ const authenticated = await authenticatePolicySnapshotEntryV2(entryBytes, descriptor);
804
+ assertOpenNotAborted(signal);
805
+ if (authenticated.body.sequence !==
806
+ forkEvidence.commonParent.sequence + 1n ||
807
+ !equals(authenticated.body.previousPolicyDigest, forkEvidence.commonParent.digest)) {
808
+ throw new Error("Stored policy fork observation is not a direct child of the common parent");
809
+ }
810
+ observedEntries.set(hashKey, copyBytes(entryBytes));
811
+ retainCanonicalForkChild(canonicalChildren, {
812
+ digest: authenticated.digest,
813
+ entryBytes,
814
+ });
708
815
  }
709
816
  previousChecksum = copyBytes(record.checksum);
710
817
  }
@@ -727,14 +834,23 @@ export class TrustedNetworkV2DurablePolicyReducer {
727
834
  }
728
835
  }
729
836
  if (latestDurableState?.state === "FORKED") {
730
- if (forkEvidence === undefined) {
837
+ if (forkEvidence === undefined ||
838
+ expectedForkObservationCount === undefined ||
839
+ expectedForkObservationCommitment === undefined) {
731
840
  throw new Error("Restored forked policy anchor has no fork evidence");
732
841
  }
842
+ if (observedEntries.size !== expectedForkObservationCount) {
843
+ throw new Error(`Policy fork evidence is incomplete: expected ${expectedForkObservationCount}, restored ${observedEntries.size}`);
844
+ }
845
+ const restoredCommitment = forkObservationCommitment(expectedDescriptorHash, canonicalForkObservationEntries(observedEntries.values()));
846
+ if (!equals(restoredCommitment, expectedForkObservationCommitment)) {
847
+ throw new Error("Policy fork evidence commitment mismatch");
848
+ }
733
849
  if (canonicalChildren.length !== 2) {
734
850
  throw new Error("Stored policy fork evidence has fewer than two distinct children");
735
851
  }
736
852
  const normalizedForkState = {
737
- formatVersion: FORMAT_VERSION,
853
+ formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
738
854
  state: "FORKED",
739
855
  commonParentEntryBytes: copyBytes(latestDurableState.commonParentEntryBytes),
740
856
  childEntryBytes: [
@@ -756,7 +872,7 @@ export class TrustedNetworkV2DurablePolicyReducer {
756
872
  latestDurableState = normalizedForkState;
757
873
  durableCoreBytes = serialize(coreRecordFromState(normalizedForkState));
758
874
  }
759
- else if (observedHashes.size !== 0) {
875
+ else if (observedEntries.size !== 0) {
760
876
  throw new Error("Non-forked policy anchor has fork observations");
761
877
  }
762
878
  return new TrustedNetworkV2DurablePolicyReducer({
@@ -767,7 +883,6 @@ export class TrustedNetworkV2DurablePolicyReducer {
767
883
  generation: highestGeneration,
768
884
  previousGenerationChecksum: previousChecksum,
769
885
  durableCoreBytes,
770
- observedHashes,
771
886
  });
772
887
  }
773
888
  catch (error) {
@@ -776,9 +891,13 @@ export class TrustedNetworkV2DurablePolicyReducer {
776
891
  }
777
892
  }
778
893
  get state() {
779
- if (this.terminalError !== undefined || this.core.state === "HALTED") {
894
+ if (this.terminalError !== undefined) {
780
895
  return "HALTED";
781
896
  }
897
+ if (this.published.state === "FORKED")
898
+ return "FORKED";
899
+ if (this.core.state === "HALTED")
900
+ return "HALTED";
782
901
  return this.published.state;
783
902
  }
784
903
  get head() {
@@ -796,6 +915,14 @@ export class TrustedNetworkV2DurablePolicyReducer {
796
915
  get pendingDigests() {
797
916
  return this.core.pendingDigests.map(copyBytes);
798
917
  }
918
+ /** Internal diagnostics for the fixed pre-publication admission bound. */
919
+ get bufferedAdmissionCount() {
920
+ return this.bufferedAdmissions;
921
+ }
922
+ /** Internal diagnostics for copied entry bytes awaiting settlement. */
923
+ get bufferedAdmissionBytes() {
924
+ return this.bufferedAdmissionEntryBytes;
925
+ }
799
926
  /** Projection query only; use isAuthorized() for the fail-closed gate. */
800
927
  rolesFor(subject) {
801
928
  return this.published.roles.get(keyId(subject)) ?? 0;
@@ -814,24 +941,109 @@ export class TrustedNetworkV2DurablePolicyReducer {
814
941
  this.core.abort();
815
942
  }
816
943
  ingest(entryBytes) {
817
- const captured = entryBytes instanceof Uint8Array &&
818
- entryBytes.byteLength <= TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES
819
- ? copyBytes(entryBytes)
820
- : entryBytes;
944
+ if (this.terminalError !== undefined) {
945
+ return Promise.reject(this.terminalError);
946
+ }
947
+ if (this.published.state === "FORKED") {
948
+ return Promise.resolve(this.forkFailStopResult());
949
+ }
950
+ if (this.core.state === "HALTED") {
951
+ return Promise.resolve(this.lifecycleHaltedResult());
952
+ }
953
+ let reservedEntryBytes;
954
+ try {
955
+ reservedEntryBytes = exactUint8ArrayByteLength(entryBytes);
956
+ }
957
+ catch {
958
+ return Promise.resolve(this.immediateAdmissionResult("rejected", "Policy snapshot entry must be a Uint8Array"));
959
+ }
960
+ if (reservedEntryBytes > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES) {
961
+ return Promise.resolve(this.immediateAdmissionResult("rejected", `Policy snapshot entry exceeds ${TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES} bytes`));
962
+ }
963
+ if (!this.reserveAdmission(reservedEntryBytes)) {
964
+ return Promise.resolve(this.immediateAdmissionResult("capacity", "Durable policy admission queue is at its fixed capacity", this.core.pendingCount, this.core.pendingBytes));
965
+ }
966
+ let captured;
967
+ try {
968
+ captured = copyBytesWithLength(entryBytes, reservedEntryBytes);
969
+ }
970
+ catch (error) {
971
+ this.releaseAdmission(reservedEntryBytes);
972
+ return Promise.reject(error);
973
+ }
821
974
  return this.enqueue(async () => {
975
+ if (this.published.state === "FORKED") {
976
+ return this.forkFailStopResult();
977
+ }
978
+ if (this.core.state === "HALTED") {
979
+ return this.lifecycleHaltedResult();
980
+ }
822
981
  const result = await this.core.ingest(captured);
823
982
  await this.persistCorePublication(result);
824
983
  return result;
825
- });
984
+ }, reservedEntryBytes);
826
985
  }
827
986
  retryUnavailable() {
987
+ if (this.terminalError !== undefined) {
988
+ return Promise.reject(this.terminalError);
989
+ }
990
+ if (this.published.state === "FORKED") {
991
+ return Promise.resolve(this.forkFailStopResult());
992
+ }
993
+ if (this.core.state === "HALTED") {
994
+ return Promise.resolve(this.lifecycleHaltedResult());
995
+ }
996
+ if (!this.reserveAdmission(0)) {
997
+ return Promise.resolve(this.immediateAdmissionResult("capacity", "Durable policy admission queue is at its fixed capacity", this.core.pendingCount, this.core.pendingBytes));
998
+ }
828
999
  return this.enqueue(async () => {
1000
+ if (this.published.state === "FORKED") {
1001
+ return this.forkFailStopResult();
1002
+ }
1003
+ if (this.core.state === "HALTED") {
1004
+ return this.lifecycleHaltedResult();
1005
+ }
829
1006
  const result = await this.core.retryUnavailable();
830
1007
  await this.persistCorePublication(result);
831
1008
  return result;
832
- });
1009
+ }, 0);
1010
+ }
1011
+ forkFailStopResult() {
1012
+ return {
1013
+ status: "halted",
1014
+ reason: "Policy reducer is halted by authority equivocation",
1015
+ fetchHints: [],
1016
+ pendingCount: 0,
1017
+ pendingBytes: 0,
1018
+ };
1019
+ }
1020
+ lifecycleHaltedResult() {
1021
+ return {
1022
+ status: "halted",
1023
+ reason: "Policy reducer lifecycle is aborted",
1024
+ fetchHints: [],
1025
+ pendingCount: this.core.pendingCount,
1026
+ pendingBytes: this.core.pendingBytes,
1027
+ };
1028
+ }
1029
+ immediateAdmissionResult(status, reason, pendingCount = 0, pendingBytes = 0) {
1030
+ return { status, reason, fetchHints: [], pendingCount, pendingBytes };
1031
+ }
1032
+ reserveAdmission(entryBytes) {
1033
+ if (this.bufferedAdmissions >= TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES ||
1034
+ entryBytes >
1035
+ MAX_QUEUED_POLICY_ENTRY_BYTES_V2 - this.bufferedAdmissionEntryBytes) {
1036
+ return false;
1037
+ }
1038
+ this.bufferedAdmissions += 1;
1039
+ this.bufferedAdmissionEntryBytes += entryBytes;
1040
+ return true;
833
1041
  }
834
- enqueue(operation) {
1042
+ releaseAdmission(entryBytes) {
1043
+ this.bufferedAdmissions -= 1;
1044
+ this.bufferedAdmissionEntryBytes -= entryBytes;
1045
+ }
1046
+ enqueue(operation, bufferedEntryBytes) {
835
1047
  this.authorizationFences++;
836
1048
  const result = this.operationTail.then(async () => {
837
1049
  if (this.terminalError !== undefined)
@@ -846,6 +1058,7 @@ export class TrustedNetworkV2DurablePolicyReducer {
846
1058
  this.operationTail = result.then(() => { }, () => { });
847
1059
  return result.finally(() => {
848
1060
  this.authorizationFences--;
1061
+ this.releaseAdmission(bufferedEntryBytes);
849
1062
  });
850
1063
  }
851
1064
  halt(error) {
@@ -868,12 +1081,8 @@ export class TrustedNetworkV2DurablePolicyReducer {
868
1081
  }
869
1082
  const coreRecord = coreRecordFromState(durableState);
870
1083
  const coreBytes = serialize(coreRecord);
871
- // FORKED is terminal. Once its common-parent anchor exists, later child
872
- // proofs are observation deltas even when they change the live canonical
873
- // pair; reopen derives that pair from the complete delta history.
874
- const stateChanged = !(this.published.state === "FORKED" && durableState.state === "FORKED") &&
875
- (this.durableCoreBytes === undefined ||
876
- !equals(this.durableCoreBytes, coreBytes));
1084
+ const stateChanged = this.durableCoreBytes === undefined ||
1085
+ !equals(this.durableCoreBytes, coreBytes);
877
1086
  const suppliedObservations = new Map();
878
1087
  for (const proof of result.forkObservations ?? []) {
879
1088
  const contentHash = observationContentHash(proof.entryBytes);
@@ -884,28 +1093,32 @@ export class TrustedNetworkV2DurablePolicyReducer {
884
1093
  }
885
1094
  suppliedObservations.set(hashKey, copyBytes(proof.entryBytes));
886
1095
  }
1096
+ let committedForkObservationCount = 0;
1097
+ let committedForkObservationDigest = copyBytes(ZERO_DIGEST);
887
1098
  if (durableState.state !== "FORKED") {
888
- if (suppliedObservations.size !== 0 || this.observedHashes.size !== 0) {
1099
+ if (suppliedObservations.size !== 0) {
889
1100
  throw this.halt("Only a forked policy anchor may contain observations");
890
1101
  }
891
1102
  }
892
1103
  else {
1104
+ if (this.published.state === "FORKED" || !stateChanged) {
1105
+ throw this.halt("A durable fork transition may be published only once");
1106
+ }
1107
+ if (suppliedObservations.size < 2 ||
1108
+ suppliedObservations.size > MAX_FORK_OBSERVATIONS_V2) {
1109
+ throw this.halt(`A durable fork transition must contain 2-${MAX_FORK_OBSERVATIONS_V2} distinct observations`);
1110
+ }
1111
+ const completeObservationEntries = canonicalForkObservationEntries(suppliedObservations.values());
1112
+ committedForkObservationCount = completeObservationEntries.length;
1113
+ committedForkObservationDigest = forkObservationCommitment(this.descriptorHash, completeObservationEntries);
893
1114
  for (const entryBytes of durableState.childEntryBytes) {
894
1115
  const hashKey = bytesKey(observationContentHash(entryBytes));
895
- if (stateChanged)
896
- suppliedObservations.delete(hashKey);
897
- else if (!this.observedHashes.has(hashKey) &&
898
- !suppliedObservations.has(hashKey)) {
1116
+ if (!suppliedObservations.delete(hashKey)) {
899
1117
  throw this.halt("Published fork state is missing a canonical observation");
900
1118
  }
901
1119
  }
902
1120
  }
903
- for (const hashKey of this.observedHashes) {
904
- suppliedObservations.delete(hashKey);
905
- }
906
- const observationEntries = [...suppliedObservations.entries()]
907
- .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
908
- .map(([hashKey, entryBytes]) => ({ hashKey, entryBytes }));
1121
+ const observationEntries = canonicalForkObservationEntries(suppliedObservations.values());
909
1122
  const recordCount = (stateChanged ? 1 : 0) + observationEntries.length;
910
1123
  if (recordCount === 0)
911
1124
  return;
@@ -914,16 +1127,19 @@ export class TrustedNetworkV2DurablePolicyReducer {
914
1127
  }
915
1128
  let nextGeneration = this.generation;
916
1129
  let previousChecksum = copyBytes(this.previousGenerationChecksum);
917
- const stagedObservedHashes = new Set(this.observedHashes);
918
1130
  const drafts = [];
919
1131
  if (stateChanged) {
920
- const payloadBytes = serialize(new PolicyAnchorStateGenerationPayloadV2({ coreState: coreRecord }));
1132
+ const payloadBytes = serialize(new PolicyAnchorStateGenerationPayloadV2({
1133
+ coreState: coreRecord,
1134
+ forkObservationCount: committedForkObservationCount,
1135
+ forkObservationCommitment: committedForkObservationDigest,
1136
+ }));
921
1137
  if (payloadBytes.byteLength > MAX_STATE_GENERATION_PAYLOAD_BYTES) {
922
1138
  throw this.halt("Policy-anchor state payload exceeds its byte ceiling");
923
1139
  }
924
1140
  drafts.push({ kind: GENERATION_KIND.STATE, payloadBytes });
925
1141
  }
926
- for (const { hashKey, entryBytes } of observationEntries) {
1142
+ for (const entryBytes of observationEntries) {
927
1143
  const payloadBytes = serialize(new PolicyAnchorObservationGenerationPayloadV2({
928
1144
  entryBytes: copyBytes(entryBytes),
929
1145
  }));
@@ -933,23 +1149,12 @@ export class TrustedNetworkV2DurablePolicyReducer {
933
1149
  drafts.push({
934
1150
  kind: GENERATION_KIND.FORK_OBSERVATION,
935
1151
  payloadBytes,
936
- applyObservationHash: hashKey,
937
1152
  });
938
1153
  }
939
1154
  for (const draft of drafts) {
940
1155
  nextGeneration += 1n;
941
- if (draft.kind === GENERATION_KIND.STATE) {
942
- if (durableState.state === "FORKED") {
943
- for (const entryBytes of durableState.childEntryBytes) {
944
- stagedObservedHashes.add(bytesKey(observationContentHash(entryBytes)));
945
- }
946
- }
947
- }
948
- else {
949
- stagedObservedHashes.add(draft.applyObservationHash);
950
- }
951
1156
  const body = new PolicyAnchorGenerationBodyV2({
952
- formatVersion: FORMAT_VERSION,
1157
+ formatVersion: ANCHOR_FORMAT_VERSION,
953
1158
  generation: nextGeneration,
954
1159
  descriptorDigest: copyBytes(this.descriptorHash),
955
1160
  previousGenerationChecksum: copyBytes(previousChecksum),
@@ -978,7 +1183,6 @@ export class TrustedNetworkV2DurablePolicyReducer {
978
1183
  this.generation = nextGeneration;
979
1184
  this.previousGenerationChecksum = previousChecksum;
980
1185
  this.durableCoreBytes = copyBytes(coreBytes);
981
- this.observedHashes = stagedObservedHashes;
982
1186
  try {
983
1187
  this.published = publishedFromReducer(this.core);
984
1188
  }