@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.
- package/dist/src/v2-policy-anchor.d.ts +11 -1
- package/dist/src/v2-policy-anchor.d.ts.map +1 -1
- package/dist/src/v2-policy-anchor.js +304 -100
- package/dist/src/v2-policy-anchor.js.map +1 -1
- package/dist/src/v2-policy-engine.d.ts +10 -0
- package/dist/src/v2-policy-engine.d.ts.map +1 -1
- package/dist/src/v2-policy-engine.js +57 -12
- package/dist/src/v2-policy-engine.js.map +1 -1
- package/package.json +7 -7
- package/src/v2-policy-anchor.ts +406 -119
- package/src/v2-policy-engine.ts +76 -12
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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 =
|
|
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 !==
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
621
|
-
|
|
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
|
-
|
|
669
|
-
|
|
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
|
-
|
|
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 (
|
|
688
|
-
|
|
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
|
-
|
|
695
|
-
|
|
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:
|
|
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 (
|
|
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
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
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
|
-
|
|
872
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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({
|
|
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
|
|
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:
|
|
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
|
}
|