@peerbit/trusted-network 6.0.109 → 6.0.110
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/src/v2-policy-anchor.ts
CHANGED
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
fixedArray,
|
|
5
5
|
serialize,
|
|
6
6
|
variant,
|
|
7
|
+
vec,
|
|
7
8
|
} from "@dao-xyz/borsh";
|
|
9
|
+
import type { CrashSafeAtomicReplaceStore } from "@peerbit/any-store-interface";
|
|
10
|
+
import { CrashSafeTwoSlotCheckpoint } from "@peerbit/any-store/checkpoint";
|
|
8
11
|
import { PublicSignKey, sha256Sync } from "@peerbit/crypto";
|
|
9
12
|
import { compare, concat, equals } from "uint8arrays";
|
|
10
13
|
import type {
|
|
@@ -31,63 +34,42 @@ import {
|
|
|
31
34
|
* Internal crash-safe policy-anchor storage format.
|
|
32
35
|
*
|
|
33
36
|
* This module is intentionally absent from the package entry point. Its store
|
|
34
|
-
* must already be open and
|
|
35
|
-
* one wrapper may write that scope:
|
|
36
|
-
* not pretend that
|
|
37
|
+
* must already be open and dedicated to this anchor and descriptor. Exactly
|
|
38
|
+
* one wrapper may write that scope: the two-slot checkpoint deliberately does
|
|
39
|
+
* not pretend that atomic replacement is a multi-process compare-and-swap.
|
|
37
40
|
*/
|
|
38
41
|
|
|
42
|
+
/** The namespace owned by the superseded append-only anchor format. */
|
|
39
43
|
export const TRUSTED_NETWORK_V2_POLICY_ANCHOR_STORE_OWNER =
|
|
40
44
|
"peerbit/trusted-network/v2/policy-anchor/v1";
|
|
41
|
-
export const TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES =
|
|
42
|
-
TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 4;
|
|
43
45
|
|
|
44
|
-
const GENERATION_KEY_PREFIX = `${TRUSTED_NETWORK_V2_POLICY_ANCHOR_STORE_OWNER}/generation/`;
|
|
45
|
-
const ANCHOR_FORMAT_VERSION = 2;
|
|
46
46
|
const REDUCER_DURABLE_FORMAT_VERSION = 1;
|
|
47
|
-
const MAX_U64 = 0xffffffffffffffffn;
|
|
48
47
|
const MAX_UNAVAILABLE_REASON_LENGTH = 512;
|
|
49
48
|
// The core admits at most 64 pending policies. A fork transition can surface
|
|
50
49
|
// that complete set plus its accepted and triggering children. Once the
|
|
51
50
|
// transition is durable the wrapper stops policy admission entirely.
|
|
52
51
|
const MAX_FORK_OBSERVATIONS_V2 = TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES + 2;
|
|
52
|
+
// Variant/state and length framing for the parent, empty candidate, digest,
|
|
53
|
+
// empty reason, u8 vector count, and every u32-framed observation.
|
|
54
|
+
const MAX_CHECKPOINT_PAYLOAD_FRAMING_BYTES_V2 = 313;
|
|
55
|
+
/** Exact maximum encoded application payload accepted by the checkpoint. */
|
|
56
|
+
export const TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_CHECKPOINT_PAYLOAD_BYTES =
|
|
57
|
+
(MAX_FORK_OBSERVATIONS_V2 + 1) * TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES +
|
|
58
|
+
MAX_CHECKPOINT_PAYLOAD_FRAMING_BYTES_V2;
|
|
53
59
|
const MAX_QUEUED_POLICY_ENTRY_BYTES_V2 =
|
|
54
60
|
TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 2;
|
|
55
61
|
const ZERO_DIGEST = new Uint8Array(32);
|
|
56
62
|
const textEncoder = new TextEncoder();
|
|
57
|
-
const
|
|
58
|
-
"peerbit/trusted-network/v2/policy-anchor/
|
|
59
|
-
);
|
|
60
|
-
const GENERATION_CHECKSUM_DOMAIN = textEncoder.encode(
|
|
61
|
-
"peerbit/trusted-network/v2/policy-anchor/generation/v1",
|
|
62
|
-
);
|
|
63
|
-
const FORK_OBSERVATION_COMMITMENT_DOMAIN = textEncoder.encode(
|
|
64
|
-
"peerbit/trusted-network/v2/policy-anchor/fork-observations/v1",
|
|
63
|
+
const CHECKPOINT_SCOPE_DOMAIN = textEncoder.encode(
|
|
64
|
+
"peerbit/trusted-network/v2/policy-anchor/checkpoint/v1\0",
|
|
65
65
|
);
|
|
66
66
|
const ANCHOR_STATE = Object.freeze({
|
|
67
67
|
ACTIVE: 1,
|
|
68
68
|
UNAVAILABLE: 2,
|
|
69
69
|
FORKED: 3,
|
|
70
70
|
} as const);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
FORK_OBSERVATION: 2,
|
|
74
|
-
} as const);
|
|
75
|
-
const MAX_STATE_GENERATION_PAYLOAD_BYTES =
|
|
76
|
-
TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 4;
|
|
77
|
-
const MAX_OBSERVATION_GENERATION_PAYLOAD_BYTES =
|
|
78
|
-
TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES + 1024;
|
|
79
|
-
|
|
80
|
-
type MaybePromiseV2<T> = T | Promise<T>;
|
|
81
|
-
|
|
82
|
-
export type CrashSafePolicyAnchorStoreV2 = {
|
|
83
|
-
get(key: string): MaybePromiseV2<Uint8Array | undefined>;
|
|
84
|
-
put(key: string, value: Uint8Array): MaybePromiseV2<void>;
|
|
85
|
-
iterator(): AsyncIterable<[string, Uint8Array]>;
|
|
86
|
-
readonly crashSafeDurability: {
|
|
87
|
-
readonly crashSafe: true;
|
|
88
|
-
barrier(): MaybePromiseV2<void>;
|
|
89
|
-
};
|
|
90
|
-
};
|
|
71
|
+
|
|
72
|
+
export type CrashSafePolicyAnchorStoreV2 = CrashSafeAtomicReplaceStore;
|
|
91
73
|
|
|
92
74
|
type DurableReducerOptionsV2 = {
|
|
93
75
|
descriptor: NetworkDescriptorV2;
|
|
@@ -103,8 +85,12 @@ export type TrustedNetworkV2DurablePolicyReducerOptions =
|
|
|
103
85
|
store: CrashSafePolicyAnchorStoreV2;
|
|
104
86
|
};
|
|
105
87
|
|
|
106
|
-
|
|
107
|
-
|
|
88
|
+
/**
|
|
89
|
+
* One self-contained application snapshot. The enclosing generic checkpoint
|
|
90
|
+
* supplies generation, scope, predecessor, and checksum authentication.
|
|
91
|
+
*/
|
|
92
|
+
@variant([2, 16, 5])
|
|
93
|
+
class PolicyAnchorCheckpointPayloadV2 {
|
|
108
94
|
@field({ type: "u8" })
|
|
109
95
|
state: number;
|
|
110
96
|
|
|
@@ -120,11 +106,8 @@ class PolicyAnchorCoreStateRecordV2 {
|
|
|
120
106
|
@field({ type: "string" })
|
|
121
107
|
unavailableReason: string;
|
|
122
108
|
|
|
123
|
-
@field({ type: Uint8Array })
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@field({ type: Uint8Array })
|
|
127
|
-
forkChildEntryBytes1: Uint8Array;
|
|
109
|
+
@field({ type: vec(Uint8Array, "u8") })
|
|
110
|
+
forkObservationEntryBytes: Uint8Array[];
|
|
128
111
|
|
|
129
112
|
constructor(properties?: {
|
|
130
113
|
state: number;
|
|
@@ -132,86 +115,7 @@ class PolicyAnchorCoreStateRecordV2 {
|
|
|
132
115
|
comparisonCandidateEntryBytes: Uint8Array;
|
|
133
116
|
acceptedAncestorDigest: Uint8Array;
|
|
134
117
|
unavailableReason: string;
|
|
135
|
-
|
|
136
|
-
forkChildEntryBytes1: Uint8Array;
|
|
137
|
-
}) {
|
|
138
|
-
if (properties) Object.assign(this, properties);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
@variant([2, 16, 3])
|
|
143
|
-
class PolicyAnchorStateGenerationPayloadV2 {
|
|
144
|
-
@field({ type: PolicyAnchorCoreStateRecordV2 })
|
|
145
|
-
coreState: PolicyAnchorCoreStateRecordV2;
|
|
146
|
-
|
|
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
|
-
}) {
|
|
158
|
-
if (properties) Object.assign(this, properties);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
@variant([2, 16, 4])
|
|
163
|
-
class PolicyAnchorObservationGenerationPayloadV2 {
|
|
164
|
-
@field({ type: Uint8Array })
|
|
165
|
-
entryBytes: Uint8Array;
|
|
166
|
-
|
|
167
|
-
constructor(properties?: { entryBytes: Uint8Array }) {
|
|
168
|
-
if (properties) Object.assign(this, properties);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
@variant([2, 16, 1])
|
|
173
|
-
class PolicyAnchorGenerationBodyV2 {
|
|
174
|
-
@field({ type: "u8" })
|
|
175
|
-
formatVersion: number;
|
|
176
|
-
|
|
177
|
-
@field({ type: "u64" })
|
|
178
|
-
generation: bigint;
|
|
179
|
-
|
|
180
|
-
@field({ type: fixedArray("u8", 32) })
|
|
181
|
-
descriptorDigest: Uint8Array;
|
|
182
|
-
|
|
183
|
-
@field({ type: fixedArray("u8", 32) })
|
|
184
|
-
previousGenerationChecksum: Uint8Array;
|
|
185
|
-
|
|
186
|
-
@field({ type: "u8" })
|
|
187
|
-
kind: number;
|
|
188
|
-
|
|
189
|
-
@field({ type: Uint8Array })
|
|
190
|
-
payloadBytes: Uint8Array;
|
|
191
|
-
|
|
192
|
-
constructor(properties?: {
|
|
193
|
-
formatVersion: number;
|
|
194
|
-
generation: bigint;
|
|
195
|
-
descriptorDigest: Uint8Array;
|
|
196
|
-
previousGenerationChecksum: Uint8Array;
|
|
197
|
-
kind: number;
|
|
198
|
-
payloadBytes: Uint8Array;
|
|
199
|
-
}) {
|
|
200
|
-
if (properties) Object.assign(this, properties);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
@variant([2, 16, 2])
|
|
205
|
-
class PolicyAnchorGenerationRecordV2 {
|
|
206
|
-
@field({ type: PolicyAnchorGenerationBodyV2 })
|
|
207
|
-
body: PolicyAnchorGenerationBodyV2;
|
|
208
|
-
|
|
209
|
-
@field({ type: fixedArray("u8", 32) })
|
|
210
|
-
checksum: Uint8Array;
|
|
211
|
-
|
|
212
|
-
constructor(properties?: {
|
|
213
|
-
body: PolicyAnchorGenerationBodyV2;
|
|
214
|
-
checksum: Uint8Array;
|
|
118
|
+
forkObservationEntryBytes: Uint8Array[];
|
|
215
119
|
}) {
|
|
216
120
|
if (properties) Object.assign(this, properties);
|
|
217
121
|
}
|
|
@@ -250,6 +154,8 @@ const exactUint8ArrayByteLength = (input: unknown): number => {
|
|
|
250
154
|
}
|
|
251
155
|
const byteLength = TYPED_ARRAY_BYTE_LENGTH.call(input) as number;
|
|
252
156
|
if (byteLength === 0) {
|
|
157
|
+
// The intrinsic getter reports zero for both empty and detached views. The
|
|
158
|
+
// intrinsic set distinguishes them without consulting caller properties.
|
|
253
159
|
UINT8_ARRAY_SET.call(new Uint8Array(0), input as Uint8Array);
|
|
254
160
|
}
|
|
255
161
|
return byteLength;
|
|
@@ -323,12 +229,6 @@ const copyForkEvidence = (
|
|
|
323
229
|
|
|
324
230
|
const keyId = (key: PublicSignKey): string => bytesKey(serialize(key));
|
|
325
231
|
|
|
326
|
-
const descriptorDigest = (descriptor: NetworkDescriptorV2): Uint8Array =>
|
|
327
|
-
sha256Sync(concat([DESCRIPTOR_DIGEST_DOMAIN, serialize(descriptor)]));
|
|
328
|
-
|
|
329
|
-
const generationChecksum = (body: PolicyAnchorGenerationBodyV2): Uint8Array =>
|
|
330
|
-
sha256Sync(concat([GENERATION_CHECKSUM_DOMAIN, serialize(body)]));
|
|
331
|
-
|
|
332
232
|
const observationContentHash = (entryBytes: Uint8Array): Uint8Array =>
|
|
333
233
|
sha256Sync(entryBytes);
|
|
334
234
|
|
|
@@ -338,40 +238,14 @@ const u32Bytes = (value: number): Uint8Array => {
|
|
|
338
238
|
return bytes;
|
|
339
239
|
};
|
|
340
240
|
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
373
|
-
const generationKey = (generation: bigint): string =>
|
|
374
|
-
`${GENERATION_KEY_PREFIX}${generation.toString().padStart(20, "0")}`;
|
|
241
|
+
const checkpointScope = (descriptor: NetworkDescriptorV2): Uint8Array => {
|
|
242
|
+
const descriptorBytes = serialize(descriptor);
|
|
243
|
+
return concat([
|
|
244
|
+
CHECKPOINT_SCOPE_DOMAIN,
|
|
245
|
+
u32Bytes(descriptorBytes.byteLength),
|
|
246
|
+
descriptorBytes,
|
|
247
|
+
]);
|
|
248
|
+
};
|
|
375
249
|
|
|
376
250
|
const assertOpenNotAborted = (signal: AbortSignal | undefined): void => {
|
|
377
251
|
if (signal?.aborted) {
|
|
@@ -396,6 +270,41 @@ const assertEntryBytes = (bytes: Uint8Array, label: string): void => {
|
|
|
396
270
|
}
|
|
397
271
|
};
|
|
398
272
|
|
|
273
|
+
const canonicalForkObservationEntries = (
|
|
274
|
+
entries: Iterable<Uint8Array>,
|
|
275
|
+
): Uint8Array[] => {
|
|
276
|
+
const observed = new Map<string, Uint8Array>();
|
|
277
|
+
const hashed: Array<{ entryBytes: Uint8Array; hash: Uint8Array }> = [];
|
|
278
|
+
for (const entryBytes of entries) {
|
|
279
|
+
assertEntryBytes(entryBytes, "fork observation entry");
|
|
280
|
+
const hash = observationContentHash(entryBytes);
|
|
281
|
+
const hashKey = bytesKey(hash);
|
|
282
|
+
const existing = observed.get(hashKey);
|
|
283
|
+
if (existing !== undefined) {
|
|
284
|
+
if (equals(existing, entryBytes)) {
|
|
285
|
+
throw new Error("Duplicate policy fork observation");
|
|
286
|
+
}
|
|
287
|
+
throw new Error("Policy fork-observation content hash collision");
|
|
288
|
+
}
|
|
289
|
+
observed.set(hashKey, entryBytes);
|
|
290
|
+
hashed.push({ entryBytes, hash });
|
|
291
|
+
}
|
|
292
|
+
return hashed
|
|
293
|
+
.sort((left, right) => {
|
|
294
|
+
const hashOrder = compare(left.hash, right.hash);
|
|
295
|
+
return hashOrder === 0
|
|
296
|
+
? compare(left.entryBytes, right.entryBytes)
|
|
297
|
+
: hashOrder;
|
|
298
|
+
})
|
|
299
|
+
.map(({ entryBytes }) => entryBytes);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const assertEmptyBytes = (bytes: Uint8Array, label: string): void => {
|
|
303
|
+
if (!hasExactUint8ArrayByteLength(bytes, 0)) {
|
|
304
|
+
throw new Error(`${label} must be empty`);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
399
308
|
const retainCanonicalForkChild = (
|
|
400
309
|
children: CanonicalForkChildV2[],
|
|
401
310
|
candidate: CanonicalForkChildV2,
|
|
@@ -424,28 +333,34 @@ const retainCanonicalForkChild = (
|
|
|
424
333
|
if (children.length > 2) children.length = 2;
|
|
425
334
|
};
|
|
426
335
|
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
336
|
+
const assertCanonicalObservationOrder = (
|
|
337
|
+
entries: readonly Uint8Array[],
|
|
338
|
+
): void => {
|
|
339
|
+
const canonical = canonicalForkObservationEntries(entries);
|
|
340
|
+
if (
|
|
341
|
+
canonical.length !== entries.length ||
|
|
342
|
+
canonical.some((entryBytes, index) => !equals(entryBytes, entries[index]!))
|
|
343
|
+
) {
|
|
344
|
+
throw new Error("Policy fork observations are not in canonical order");
|
|
430
345
|
}
|
|
431
346
|
};
|
|
432
347
|
|
|
433
|
-
const
|
|
348
|
+
const checkpointPayloadFromState = (
|
|
434
349
|
state: Exclude<PolicyReducerDurableStateV2, { state: "EMPTY" }>,
|
|
435
|
-
|
|
350
|
+
forkObservationEntryBytes: readonly Uint8Array[] = [],
|
|
351
|
+
): PolicyAnchorCheckpointPayloadV2 => {
|
|
436
352
|
if (state.state === "ACTIVE") {
|
|
437
|
-
return new
|
|
353
|
+
return new PolicyAnchorCheckpointPayloadV2({
|
|
438
354
|
state: ANCHOR_STATE.ACTIVE,
|
|
439
355
|
acceptedHeadEntryBytes: copyBytes(state.acceptedHeadEntryBytes),
|
|
440
356
|
comparisonCandidateEntryBytes: new Uint8Array(0),
|
|
441
357
|
acceptedAncestorDigest: copyBytes(ZERO_DIGEST),
|
|
442
358
|
unavailableReason: "",
|
|
443
|
-
|
|
444
|
-
forkChildEntryBytes1: new Uint8Array(0),
|
|
359
|
+
forkObservationEntryBytes: [],
|
|
445
360
|
});
|
|
446
361
|
}
|
|
447
362
|
if (state.state === "UNAVAILABLE") {
|
|
448
|
-
return new
|
|
363
|
+
return new PolicyAnchorCheckpointPayloadV2({
|
|
449
364
|
state: ANCHOR_STATE.UNAVAILABLE,
|
|
450
365
|
acceptedHeadEntryBytes: copyBytes(state.acceptedHeadEntryBytes),
|
|
451
366
|
comparisonCandidateEntryBytes: copyBytes(
|
|
@@ -453,210 +368,191 @@ const coreRecordFromState = (
|
|
|
453
368
|
),
|
|
454
369
|
acceptedAncestorDigest: copyBytes(state.acceptedAncestorDigest),
|
|
455
370
|
unavailableReason: state.reason,
|
|
456
|
-
|
|
457
|
-
forkChildEntryBytes1: new Uint8Array(0),
|
|
371
|
+
forkObservationEntryBytes: [],
|
|
458
372
|
});
|
|
459
373
|
}
|
|
460
|
-
return new
|
|
374
|
+
return new PolicyAnchorCheckpointPayloadV2({
|
|
461
375
|
state: ANCHOR_STATE.FORKED,
|
|
462
376
|
acceptedHeadEntryBytes: copyBytes(state.commonParentEntryBytes),
|
|
463
377
|
comparisonCandidateEntryBytes: new Uint8Array(0),
|
|
464
378
|
acceptedAncestorDigest: copyBytes(ZERO_DIGEST),
|
|
465
379
|
unavailableReason: "",
|
|
466
|
-
|
|
467
|
-
forkChildEntryBytes1: copyBytes(state.childEntryBytes[1]),
|
|
380
|
+
forkObservationEntryBytes: forkObservationEntryBytes.map(copyBytes),
|
|
468
381
|
});
|
|
469
382
|
};
|
|
470
383
|
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
):
|
|
474
|
-
|
|
475
|
-
|
|
384
|
+
const coreIdentityBytesFromState = (
|
|
385
|
+
state: Exclude<PolicyReducerDurableStateV2, { state: "EMPTY" }>,
|
|
386
|
+
): Uint8Array =>
|
|
387
|
+
// Fork observations are deliberately excluded from this transient identity.
|
|
388
|
+
// A FORKED identity is only compared with the preceding non-forked snapshot;
|
|
389
|
+
// after publication, the wrapper admits no more work and retains no copy.
|
|
390
|
+
serialize(checkpointPayloadFromState(state));
|
|
391
|
+
|
|
392
|
+
const decodeCanonicalCheckpointPayload = (
|
|
393
|
+
input: Uint8Array,
|
|
394
|
+
): PolicyAnchorCheckpointPayloadV2 => {
|
|
395
|
+
let byteLength: number;
|
|
396
|
+
try {
|
|
397
|
+
byteLength = exactUint8ArrayByteLength(input);
|
|
398
|
+
} catch {
|
|
399
|
+
byteLength = -1;
|
|
400
|
+
}
|
|
401
|
+
if (
|
|
402
|
+
byteLength < 1 ||
|
|
403
|
+
byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_CHECKPOINT_PAYLOAD_BYTES
|
|
404
|
+
) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
"Policy-anchor checkpoint payload exceeds its byte ceiling",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
// CrashSafeTwoSlotCheckpoint.current already returns a fresh genuine exact
|
|
410
|
+
// copy, so retaining that input while deserializing avoids another full-size
|
|
411
|
+
// restore copy without exposing caller-owned bytes.
|
|
412
|
+
const payload = deserialize(input, PolicyAnchorCheckpointPayloadV2);
|
|
413
|
+
if (!equals(input, serialize(payload))) {
|
|
414
|
+
throw new Error("Policy-anchor checkpoint payload is not canonical");
|
|
415
|
+
}
|
|
416
|
+
return payload;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
type DecodedCheckpointPayloadV2 = {
|
|
420
|
+
durableState: Exclude<PolicyReducerDurableStateV2, { state: "EMPTY" }>;
|
|
421
|
+
coreIdentityBytes?: Uint8Array;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const durableStateFromCheckpointPayload = async (
|
|
425
|
+
payload: PolicyAnchorCheckpointPayloadV2,
|
|
426
|
+
descriptor: NetworkDescriptorV2,
|
|
427
|
+
signal: AbortSignal | undefined,
|
|
428
|
+
): Promise<DecodedCheckpointPayloadV2> => {
|
|
429
|
+
assertEntryBytes(payload.acceptedHeadEntryBytes, "accepted head entry");
|
|
430
|
+
if (payload.state === ANCHOR_STATE.ACTIVE) {
|
|
476
431
|
assertEmptyBytes(
|
|
477
|
-
|
|
432
|
+
payload.comparisonCandidateEntryBytes,
|
|
478
433
|
"active comparison candidate",
|
|
479
434
|
);
|
|
480
|
-
if (!equals(
|
|
435
|
+
if (!equals(payload.acceptedAncestorDigest, ZERO_DIGEST)) {
|
|
481
436
|
throw new Error("Active accepted-ancestor digest must be zero");
|
|
482
437
|
}
|
|
483
|
-
if (
|
|
438
|
+
if (payload.unavailableReason !== "") {
|
|
484
439
|
throw new Error("Active unavailable reason must be empty");
|
|
485
440
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
441
|
+
if (payload.forkObservationEntryBytes.length !== 0) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
"Active policy-anchor state must not contain fork evidence",
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const durableState = {
|
|
489
447
|
formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
|
|
490
448
|
state: "ACTIVE",
|
|
491
|
-
acceptedHeadEntryBytes: copyBytes(
|
|
449
|
+
acceptedHeadEntryBytes: copyBytes(payload.acceptedHeadEntryBytes),
|
|
450
|
+
} as const;
|
|
451
|
+
return {
|
|
452
|
+
durableState,
|
|
453
|
+
coreIdentityBytes: coreIdentityBytesFromState(durableState),
|
|
492
454
|
};
|
|
493
455
|
}
|
|
494
|
-
if (
|
|
456
|
+
if (payload.state === ANCHOR_STATE.UNAVAILABLE) {
|
|
495
457
|
assertEntryBytes(
|
|
496
|
-
|
|
458
|
+
payload.comparisonCandidateEntryBytes,
|
|
497
459
|
"unavailable comparison candidate",
|
|
498
460
|
);
|
|
499
|
-
if (!hasExactUint8ArrayByteLength(
|
|
461
|
+
if (!hasExactUint8ArrayByteLength(payload.acceptedAncestorDigest, 32)) {
|
|
500
462
|
throw new Error("Unavailable accepted-ancestor digest must be 32 bytes");
|
|
501
463
|
}
|
|
502
464
|
if (
|
|
503
|
-
|
|
504
|
-
|
|
465
|
+
payload.unavailableReason.length === 0 ||
|
|
466
|
+
payload.unavailableReason.length > MAX_UNAVAILABLE_REASON_LENGTH
|
|
505
467
|
) {
|
|
506
468
|
throw new Error(
|
|
507
469
|
"Unavailable reason is empty or exceeds its character ceiling",
|
|
508
470
|
);
|
|
509
471
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
472
|
+
if (payload.forkObservationEntryBytes.length !== 0) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
"Unavailable policy-anchor state must not contain fork evidence",
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
const durableState = {
|
|
513
478
|
formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
|
|
514
479
|
state: "UNAVAILABLE",
|
|
515
|
-
acceptedHeadEntryBytes: copyBytes(
|
|
480
|
+
acceptedHeadEntryBytes: copyBytes(payload.acceptedHeadEntryBytes),
|
|
516
481
|
comparisonCandidateEntryBytes: copyBytes(
|
|
517
|
-
|
|
482
|
+
payload.comparisonCandidateEntryBytes,
|
|
518
483
|
),
|
|
519
|
-
acceptedAncestorDigest: copyBytes(
|
|
520
|
-
reason:
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
if (record.state === ANCHOR_STATE.FORKED) {
|
|
524
|
-
assertEmptyBytes(
|
|
525
|
-
record.comparisonCandidateEntryBytes,
|
|
526
|
-
"forked comparison candidate",
|
|
527
|
-
);
|
|
528
|
-
if (!equals(record.acceptedAncestorDigest, ZERO_DIGEST)) {
|
|
529
|
-
throw new Error("Forked accepted-ancestor digest must be zero");
|
|
530
|
-
}
|
|
531
|
-
if (record.unavailableReason !== "") {
|
|
532
|
-
throw new Error("Forked unavailable reason must be empty");
|
|
533
|
-
}
|
|
534
|
-
assertEntryBytes(record.forkChildEntryBytes0, "fork child 0");
|
|
535
|
-
assertEntryBytes(record.forkChildEntryBytes1, "fork child 1");
|
|
484
|
+
acceptedAncestorDigest: copyBytes(payload.acceptedAncestorDigest),
|
|
485
|
+
reason: payload.unavailableReason,
|
|
486
|
+
} as const;
|
|
536
487
|
return {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
commonParentEntryBytes: copyBytes(record.acceptedHeadEntryBytes),
|
|
540
|
-
childEntryBytes: [
|
|
541
|
-
copyBytes(record.forkChildEntryBytes0),
|
|
542
|
-
copyBytes(record.forkChildEntryBytes1),
|
|
543
|
-
],
|
|
488
|
+
durableState,
|
|
489
|
+
coreIdentityBytes: coreIdentityBytesFromState(durableState),
|
|
544
490
|
};
|
|
545
491
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
type DecodedGenerationPayloadV2 =
|
|
550
|
-
| {
|
|
551
|
-
kind: "state";
|
|
552
|
-
payload: PolicyAnchorStateGenerationPayloadV2;
|
|
553
|
-
durableState: Exclude<PolicyReducerDurableStateV2, { state: "EMPTY" }>;
|
|
554
|
-
}
|
|
555
|
-
| {
|
|
556
|
-
kind: "fork-observation";
|
|
557
|
-
payload: PolicyAnchorObservationGenerationPayloadV2;
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
const decodeCanonicalPayload = <T>(
|
|
561
|
-
input: Uint8Array,
|
|
562
|
-
type: new (...args: any[]) => T,
|
|
563
|
-
maxBytes: number,
|
|
564
|
-
label: string,
|
|
565
|
-
): T => {
|
|
566
|
-
let byteLength: number;
|
|
567
|
-
try {
|
|
568
|
-
byteLength = exactUint8ArrayByteLength(input);
|
|
569
|
-
} catch {
|
|
570
|
-
byteLength = -1;
|
|
492
|
+
if (payload.state !== ANCHOR_STATE.FORKED) {
|
|
493
|
+
throw new Error("Unknown durable policy-anchor state");
|
|
571
494
|
}
|
|
572
|
-
|
|
573
|
-
|
|
495
|
+
assertEmptyBytes(
|
|
496
|
+
payload.comparisonCandidateEntryBytes,
|
|
497
|
+
"forked comparison candidate",
|
|
498
|
+
);
|
|
499
|
+
if (!equals(payload.acceptedAncestorDigest, ZERO_DIGEST)) {
|
|
500
|
+
throw new Error("Forked accepted-ancestor digest must be zero");
|
|
574
501
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (!equals(bytes, serialize(payload))) {
|
|
578
|
-
throw new Error(`${label} is not canonical`);
|
|
502
|
+
if (payload.unavailableReason !== "") {
|
|
503
|
+
throw new Error("Forked unavailable reason must be empty");
|
|
579
504
|
}
|
|
580
|
-
|
|
581
|
-
|
|
505
|
+
if (
|
|
506
|
+
payload.forkObservationEntryBytes.length < 2 ||
|
|
507
|
+
payload.forkObservationEntryBytes.length > MAX_FORK_OBSERVATIONS_V2
|
|
508
|
+
) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
`Forked policy-anchor state must contain 2-${MAX_FORK_OBSERVATIONS_V2} observations`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
assertCanonicalObservationOrder(payload.forkObservationEntryBytes);
|
|
582
514
|
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
515
|
+
const commonParent = await authenticatePolicySnapshotEntryV2(
|
|
516
|
+
payload.acceptedHeadEntryBytes,
|
|
517
|
+
descriptor,
|
|
518
|
+
);
|
|
519
|
+
assertOpenNotAborted(signal);
|
|
520
|
+
const canonicalChildren: CanonicalForkChildV2[] = [];
|
|
521
|
+
for (const entryBytes of payload.forkObservationEntryBytes) {
|
|
522
|
+
const authenticated = await authenticatePolicySnapshotEntryV2(
|
|
523
|
+
entryBytes,
|
|
524
|
+
descriptor,
|
|
592
525
|
);
|
|
593
|
-
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
|
|
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)
|
|
526
|
+
assertOpenNotAborted(signal);
|
|
527
|
+
if (
|
|
528
|
+
authenticated.body.sequence !== commonParent.body.sequence + 1n ||
|
|
529
|
+
!equals(authenticated.body.previousPolicyDigest, commonParent.digest)
|
|
606
530
|
) {
|
|
607
531
|
throw new Error(
|
|
608
|
-
"
|
|
532
|
+
"Stored policy fork observation is not a direct child of the common parent",
|
|
609
533
|
);
|
|
610
534
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
};
|
|
535
|
+
retainCanonicalForkChild(canonicalChildren, {
|
|
536
|
+
digest: authenticated.digest,
|
|
537
|
+
entryBytes,
|
|
538
|
+
});
|
|
616
539
|
}
|
|
617
|
-
if (
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
PolicyAnchorObservationGenerationPayloadV2,
|
|
621
|
-
MAX_OBSERVATION_GENERATION_PAYLOAD_BYTES,
|
|
622
|
-
"Policy fork-observation payload",
|
|
540
|
+
if (canonicalChildren.length !== 2) {
|
|
541
|
+
throw new Error(
|
|
542
|
+
"Stored policy fork evidence has fewer than two distinct children",
|
|
623
543
|
);
|
|
624
|
-
assertEntryBytes(payload.entryBytes, "fork observation entry");
|
|
625
|
-
return { kind: "fork-observation", payload };
|
|
626
544
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
byteLength = exactUint8ArrayByteLength(input);
|
|
639
|
-
} catch {
|
|
640
|
-
byteLength = -1;
|
|
641
|
-
}
|
|
642
|
-
if (
|
|
643
|
-
byteLength < 1 ||
|
|
644
|
-
byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES
|
|
645
|
-
) {
|
|
646
|
-
throw new Error("Policy-anchor generation record exceeds its byte ceiling");
|
|
647
|
-
}
|
|
648
|
-
const bytes = copyBytesWithLength(input, byteLength);
|
|
649
|
-
const record = deserialize(bytes, PolicyAnchorGenerationRecordV2);
|
|
650
|
-
if (!equals(bytes, serialize(record))) {
|
|
651
|
-
throw new Error("Policy-anchor generation record is not canonical");
|
|
652
|
-
}
|
|
653
|
-
if (record.body.formatVersion !== ANCHOR_FORMAT_VERSION) {
|
|
654
|
-
throw new Error("Unsupported policy-anchor generation format");
|
|
655
|
-
}
|
|
656
|
-
if (!equals(record.checksum, generationChecksum(record.body))) {
|
|
657
|
-
throw new Error("Policy-anchor generation checksum mismatch");
|
|
658
|
-
}
|
|
659
|
-
return { record, payload: decodeGenerationPayload(record.body) };
|
|
545
|
+
return {
|
|
546
|
+
durableState: {
|
|
547
|
+
formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
|
|
548
|
+
state: "FORKED",
|
|
549
|
+
commonParentEntryBytes: copyBytes(payload.acceptedHeadEntryBytes),
|
|
550
|
+
childEntryBytes: [
|
|
551
|
+
copyBytes(canonicalChildren[0]!.entryBytes),
|
|
552
|
+
copyBytes(canonicalChildren[1]!.entryBytes),
|
|
553
|
+
],
|
|
554
|
+
},
|
|
555
|
+
};
|
|
660
556
|
};
|
|
661
557
|
|
|
662
558
|
const publishedFromReducer = (
|
|
@@ -680,35 +576,129 @@ const publishedFromReducer = (
|
|
|
680
576
|
|
|
681
577
|
const assertCanonicalCoreRestore = (
|
|
682
578
|
reducer: TrustedNetworkV2PolicyReducer,
|
|
683
|
-
|
|
579
|
+
expectedState: Exclude<PolicyReducerDurableStateV2, { state: "EMPTY" }>,
|
|
684
580
|
): void => {
|
|
685
581
|
const restoredState = reducer.exportDurableState();
|
|
686
|
-
if (
|
|
582
|
+
if (
|
|
583
|
+
restoredState.state === "EMPTY" ||
|
|
584
|
+
restoredState.state !== expectedState.state
|
|
585
|
+
) {
|
|
687
586
|
throw new Error("Restored policy-anchor state is unexpectedly empty");
|
|
688
587
|
}
|
|
689
|
-
|
|
690
|
-
if (
|
|
588
|
+
let exact = false;
|
|
589
|
+
if (restoredState.state === "ACTIVE" && expectedState.state === "ACTIVE") {
|
|
590
|
+
exact = equals(
|
|
591
|
+
restoredState.acceptedHeadEntryBytes,
|
|
592
|
+
expectedState.acceptedHeadEntryBytes,
|
|
593
|
+
);
|
|
594
|
+
} else if (
|
|
595
|
+
restoredState.state === "UNAVAILABLE" &&
|
|
596
|
+
expectedState.state === "UNAVAILABLE"
|
|
597
|
+
) {
|
|
598
|
+
exact =
|
|
599
|
+
equals(
|
|
600
|
+
restoredState.acceptedHeadEntryBytes,
|
|
601
|
+
expectedState.acceptedHeadEntryBytes,
|
|
602
|
+
) &&
|
|
603
|
+
equals(
|
|
604
|
+
restoredState.comparisonCandidateEntryBytes,
|
|
605
|
+
expectedState.comparisonCandidateEntryBytes,
|
|
606
|
+
) &&
|
|
607
|
+
equals(
|
|
608
|
+
restoredState.acceptedAncestorDigest,
|
|
609
|
+
expectedState.acceptedAncestorDigest,
|
|
610
|
+
) &&
|
|
611
|
+
restoredState.reason === expectedState.reason;
|
|
612
|
+
} else if (
|
|
613
|
+
restoredState.state === "FORKED" &&
|
|
614
|
+
expectedState.state === "FORKED"
|
|
615
|
+
) {
|
|
616
|
+
exact =
|
|
617
|
+
equals(
|
|
618
|
+
restoredState.commonParentEntryBytes,
|
|
619
|
+
expectedState.commonParentEntryBytes,
|
|
620
|
+
) &&
|
|
621
|
+
equals(
|
|
622
|
+
restoredState.childEntryBytes[0],
|
|
623
|
+
expectedState.childEntryBytes[0],
|
|
624
|
+
) &&
|
|
625
|
+
equals(
|
|
626
|
+
restoredState.childEntryBytes[1],
|
|
627
|
+
expectedState.childEntryBytes[1],
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
if (!exact) {
|
|
691
631
|
throw new Error("Restored policy-anchor state is not canonical");
|
|
692
632
|
}
|
|
693
633
|
};
|
|
694
634
|
|
|
635
|
+
const canonicalForkEntriesForCommit = (
|
|
636
|
+
result: PolicyAdmissionResultV2,
|
|
637
|
+
durableState: Extract<PolicyReducerDurableStateV2, { state: "FORKED" }>,
|
|
638
|
+
forkEvidence: PolicyForkEvidenceV2 | undefined,
|
|
639
|
+
): Uint8Array[] => {
|
|
640
|
+
if (forkEvidence === undefined) {
|
|
641
|
+
throw new Error("Forked policy reducer has no fork evidence");
|
|
642
|
+
}
|
|
643
|
+
const observations = result.forkObservations ?? [];
|
|
644
|
+
if (
|
|
645
|
+
observations.length < 2 ||
|
|
646
|
+
observations.length > MAX_FORK_OBSERVATIONS_V2
|
|
647
|
+
) {
|
|
648
|
+
throw new Error(
|
|
649
|
+
`A durable fork transition must contain 2-${MAX_FORK_OBSERVATIONS_V2} observations`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const observationEntries: Uint8Array[] = [];
|
|
653
|
+
const canonicalChildren: CanonicalForkChildV2[] = [];
|
|
654
|
+
for (const proof of observations) {
|
|
655
|
+
assertEntryBytes(proof.entryBytes, "fork observation entry");
|
|
656
|
+
if (!hasExactUint8ArrayByteLength(proof.digest, 32)) {
|
|
657
|
+
throw new Error("Policy fork-observation digest must be 32 bytes");
|
|
658
|
+
}
|
|
659
|
+
if (proof.sequence !== forkEvidence.commonParent.sequence + 1n) {
|
|
660
|
+
throw new Error(
|
|
661
|
+
"Policy fork observation is not a direct child of the common parent",
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
// The reducer result owns this genuine exact copy and is not exposed until
|
|
665
|
+
// publication settles, so retaining it through this synchronous validation
|
|
666
|
+
// avoids another full fork-evidence copy.
|
|
667
|
+
observationEntries.push(proof.entryBytes);
|
|
668
|
+
retainCanonicalForkChild(canonicalChildren, proof);
|
|
669
|
+
}
|
|
670
|
+
if (canonicalChildren.length !== 2) {
|
|
671
|
+
throw new Error(
|
|
672
|
+
"A durable fork transition requires two distinct policy children",
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
for (let index = 0; index < canonicalChildren.length; index++) {
|
|
676
|
+
if (
|
|
677
|
+
!equals(
|
|
678
|
+
canonicalChildren[index]!.entryBytes,
|
|
679
|
+
durableState.childEntryBytes[index]!,
|
|
680
|
+
)
|
|
681
|
+
) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
"Published fork state does not contain the lowest canonical children",
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return canonicalForkObservationEntries(observationEntries);
|
|
688
|
+
};
|
|
689
|
+
|
|
695
690
|
/**
|
|
696
691
|
* Crash-safe publication wrapper for the internal v2 reducer.
|
|
697
692
|
*
|
|
698
693
|
* The mutable reducer is never exposed. Authorization reads use only the last
|
|
699
|
-
* projection published after
|
|
700
|
-
* fail-closed fence around both reducer mutation and
|
|
694
|
+
* projection published after one atomic checkpoint replacement. An admission
|
|
695
|
+
* places a fail-closed fence around both reducer mutation and publication.
|
|
701
696
|
*/
|
|
702
697
|
export class TrustedNetworkV2DurablePolicyReducer {
|
|
703
|
-
private
|
|
704
|
-
private readonly durability: CrashSafePolicyAnchorStoreV2["crashSafeDurability"];
|
|
705
|
-
private readonly descriptor: NetworkDescriptorV2;
|
|
706
|
-
private readonly descriptorHash: Uint8Array;
|
|
698
|
+
private checkpoint?: CrashSafeTwoSlotCheckpoint;
|
|
707
699
|
private core: TrustedNetworkV2PolicyReducer;
|
|
708
700
|
private published: PublishedProjectionV2;
|
|
709
|
-
private
|
|
710
|
-
private previousGenerationChecksum = copyBytes(ZERO_DIGEST);
|
|
711
|
-
private durableCoreBytes?: Uint8Array;
|
|
701
|
+
private durableCoreIdentityBytes?: Uint8Array;
|
|
712
702
|
private operationTail: Promise<void> = Promise.resolve();
|
|
713
703
|
private authorizationFences = 0;
|
|
714
704
|
private bufferedAdmissions = 0;
|
|
@@ -716,31 +706,17 @@ export class TrustedNetworkV2DurablePolicyReducer {
|
|
|
716
706
|
private terminalError?: Error;
|
|
717
707
|
|
|
718
708
|
private constructor(properties: {
|
|
719
|
-
|
|
720
|
-
durability: CrashSafePolicyAnchorStoreV2["crashSafeDurability"];
|
|
721
|
-
descriptor: NetworkDescriptorV2;
|
|
709
|
+
checkpoint?: CrashSafeTwoSlotCheckpoint;
|
|
722
710
|
core: TrustedNetworkV2PolicyReducer;
|
|
723
|
-
|
|
724
|
-
previousGenerationChecksum?: Uint8Array;
|
|
725
|
-
durableCoreBytes?: Uint8Array;
|
|
711
|
+
durableCoreIdentityBytes?: Uint8Array;
|
|
726
712
|
}) {
|
|
727
|
-
this.
|
|
728
|
-
this.durability = properties.durability;
|
|
729
|
-
this.descriptor = deserialize(
|
|
730
|
-
serialize(properties.descriptor),
|
|
731
|
-
NetworkDescriptorV2,
|
|
732
|
-
);
|
|
733
|
-
this.descriptorHash = descriptorDigest(this.descriptor);
|
|
713
|
+
this.checkpoint = properties.checkpoint;
|
|
734
714
|
this.core = properties.core;
|
|
735
715
|
this.published = publishedFromReducer(this.core);
|
|
736
|
-
this.
|
|
737
|
-
|
|
738
|
-
properties.previousGenerationChecksum ?? ZERO_DIGEST,
|
|
739
|
-
);
|
|
740
|
-
this.durableCoreBytes =
|
|
741
|
-
properties.durableCoreBytes === undefined
|
|
716
|
+
this.durableCoreIdentityBytes =
|
|
717
|
+
properties.durableCoreIdentityBytes === undefined
|
|
742
718
|
? undefined
|
|
743
|
-
: copyBytes(properties.
|
|
719
|
+
: copyBytes(properties.durableCoreIdentityBytes);
|
|
744
720
|
}
|
|
745
721
|
|
|
746
722
|
static async open(
|
|
@@ -762,287 +738,61 @@ export class TrustedNetworkV2DurablePolicyReducer {
|
|
|
762
738
|
maxPendingPolicyBytes: options.maxPendingPolicyBytes,
|
|
763
739
|
};
|
|
764
740
|
assertOpenNotAborted(signal);
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
"TrustedNetwork v2 durable policy requires a crash-safe store barrier",
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// A successful fresh barrier is required on every open, including an empty
|
|
776
|
-
// store; a generic flush acknowledgement is not this physical fence.
|
|
777
|
-
await durability.barrier();
|
|
741
|
+
const checkpoint = await CrashSafeTwoSlotCheckpoint.open({
|
|
742
|
+
store,
|
|
743
|
+
scope: checkpointScope(descriptor),
|
|
744
|
+
maxPayloadBytes:
|
|
745
|
+
TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_CHECKPOINT_PAYLOAD_BYTES,
|
|
746
|
+
});
|
|
778
747
|
assertOpenNotAborted(signal);
|
|
779
748
|
|
|
780
|
-
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
const generationKeys: string[] = [];
|
|
785
|
-
for await (const [key, input] of store.iterator()) {
|
|
749
|
+
// Never let the old append-only format look like an empty or newer
|
|
750
|
+
// checkpoint. It was internal and never activated, so migration or fallback
|
|
751
|
+
// would add rollback surface without preserving a public compatibility need.
|
|
752
|
+
for await (const [key] of store.iterator()) {
|
|
786
753
|
assertOpenNotAborted(signal);
|
|
787
|
-
if (key.startsWith(GENERATION_KEY_PREFIX)) {
|
|
788
|
-
const suffix = key.slice(GENERATION_KEY_PREFIX.length);
|
|
789
|
-
if (!/^\d{20}$/.test(suffix)) {
|
|
790
|
-
throw new Error("Malformed policy-anchor generation key");
|
|
791
|
-
}
|
|
792
|
-
let byteLength: number;
|
|
793
|
-
try {
|
|
794
|
-
byteLength = exactUint8ArrayByteLength(input);
|
|
795
|
-
} catch {
|
|
796
|
-
byteLength = -1;
|
|
797
|
-
}
|
|
798
|
-
if (
|
|
799
|
-
byteLength < 1 ||
|
|
800
|
-
byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES
|
|
801
|
-
) {
|
|
802
|
-
throw new Error(
|
|
803
|
-
"Policy-anchor generation record exceeds its byte ceiling",
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
|
-
const generation = BigInt(suffix);
|
|
807
|
-
if (generation === 0n || generation > MAX_U64) {
|
|
808
|
-
throw new Error("Policy-anchor generation key is outside u64");
|
|
809
|
-
}
|
|
810
|
-
generationKeys.push(key);
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
754
|
if (
|
|
814
755
|
key === TRUSTED_NETWORK_V2_POLICY_ANCHOR_STORE_OWNER ||
|
|
815
756
|
key.startsWith(`${TRUSTED_NETWORK_V2_POLICY_ANCHOR_STORE_OWNER}/`)
|
|
816
757
|
) {
|
|
817
|
-
throw new Error(
|
|
758
|
+
throw new Error(
|
|
759
|
+
"Legacy append-only policy-anchor records are not supported; reset the dedicated store",
|
|
760
|
+
);
|
|
818
761
|
}
|
|
819
762
|
}
|
|
820
763
|
assertOpenNotAborted(signal);
|
|
821
764
|
|
|
822
|
-
|
|
823
|
-
let previousChecksum = copyBytes(ZERO_DIGEST);
|
|
824
|
-
let latestCoreBytes: Uint8Array | undefined;
|
|
825
|
-
let latestDurableState:
|
|
826
|
-
| Exclude<PolicyReducerDurableStateV2, { state: "EMPTY" }>
|
|
827
|
-
| undefined;
|
|
765
|
+
const current = checkpoint.current;
|
|
828
766
|
let core: TrustedNetworkV2PolicyReducer | undefined;
|
|
829
|
-
let durableCoreBytes: Uint8Array | undefined;
|
|
830
|
-
let forkEvidence: PolicyForkEvidenceV2 | undefined;
|
|
831
|
-
let expectedForkObservationCount: number | undefined;
|
|
832
|
-
let expectedForkObservationCommitment: Uint8Array | undefined;
|
|
833
|
-
const observedEntries = new Map<string, Uint8Array>();
|
|
834
|
-
const canonicalChildren: CanonicalForkChildV2[] = [];
|
|
835
767
|
try {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
throw new Error("Policy-anchor generation history is gapped");
|
|
843
|
-
}
|
|
844
|
-
const input = await store.get(key);
|
|
845
|
-
assertOpenNotAborted(signal);
|
|
846
|
-
if (input === undefined) {
|
|
847
|
-
throw new Error(
|
|
848
|
-
"Policy-anchor generation disappeared during restore",
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
const { record, payload } = decodeGenerationRecord(input);
|
|
852
|
-
if (
|
|
853
|
-
record.body.generation !== keyedGeneration ||
|
|
854
|
-
generationKey(record.body.generation) !== key
|
|
855
|
-
) {
|
|
856
|
-
throw new Error("Policy-anchor generation key does not match record");
|
|
857
|
-
}
|
|
858
|
-
if (!equals(record.body.descriptorDigest, expectedDescriptorHash)) {
|
|
859
|
-
throw new Error(
|
|
860
|
-
"Policy-anchor generation belongs to another descriptor",
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
if (!equals(record.body.previousGenerationChecksum, previousChecksum)) {
|
|
864
|
-
throw new Error("Policy-anchor generation checksum chain is broken");
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
if (payload.kind === "state") {
|
|
868
|
-
if (latestDurableState?.state === "FORKED") {
|
|
869
|
-
throw new Error(
|
|
870
|
-
"A FORKED policy anchor may only append observation deltas",
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
latestCoreBytes = serialize(payload.payload.coreState);
|
|
874
|
-
latestDurableState = payload.durableState;
|
|
875
|
-
if (latestDurableState.state === "FORKED") {
|
|
876
|
-
expectedForkObservationCount = payload.payload.forkObservationCount;
|
|
877
|
-
expectedForkObservationCommitment = copyBytes(
|
|
878
|
-
payload.payload.forkObservationCommitment,
|
|
879
|
-
);
|
|
880
|
-
core = await TrustedNetworkV2PolicyReducer.restore({
|
|
881
|
-
...coreProperties,
|
|
882
|
-
durableState: latestDurableState,
|
|
883
|
-
});
|
|
884
|
-
assertOpenNotAborted(signal);
|
|
885
|
-
assertCanonicalCoreRestore(core, latestCoreBytes);
|
|
886
|
-
durableCoreBytes = copyBytes(latestCoreBytes);
|
|
887
|
-
forkEvidence = core.forkEvidence;
|
|
888
|
-
if (forkEvidence === undefined) {
|
|
889
|
-
throw new Error(
|
|
890
|
-
"Restored forked policy anchor has no fork evidence",
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
for (const child of forkEvidence.children) {
|
|
894
|
-
const hashKey = bytesKey(
|
|
895
|
-
observationContentHash(child.entryBytes),
|
|
896
|
-
);
|
|
897
|
-
if (observedEntries.has(hashKey)) {
|
|
898
|
-
throw new Error(
|
|
899
|
-
"Duplicate policy fork observation in durable state",
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
observedEntries.set(hashKey, copyBytes(child.entryBytes));
|
|
903
|
-
retainCanonicalForkChild(canonicalChildren, child);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
} else {
|
|
907
|
-
if (
|
|
908
|
-
latestDurableState?.state !== "FORKED" ||
|
|
909
|
-
core === undefined ||
|
|
910
|
-
forkEvidence === undefined
|
|
911
|
-
) {
|
|
912
|
-
throw new Error(
|
|
913
|
-
"Policy fork-observation delta requires a preceding FORKED state",
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
const entryBytes = payload.payload.entryBytes;
|
|
917
|
-
const hashKey = bytesKey(observationContentHash(entryBytes));
|
|
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`,
|
|
924
|
-
);
|
|
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
|
-
});
|
|
948
|
-
}
|
|
949
|
-
previousChecksum = copyBytes(record.checksum);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
const highestGeneration =
|
|
953
|
-
generationKeys.length === 0 ? undefined : BigInt(generationKeys.length);
|
|
954
|
-
if (highestGeneration !== undefined && latestCoreBytes === undefined) {
|
|
955
|
-
throw new Error(
|
|
956
|
-
"Policy-anchor history has no durable state generation",
|
|
957
|
-
);
|
|
958
|
-
}
|
|
959
|
-
if (core === undefined) {
|
|
960
|
-
if (highestGeneration === undefined) {
|
|
961
|
-
core = new TrustedNetworkV2PolicyReducer(coreProperties);
|
|
962
|
-
} else {
|
|
963
|
-
core = await TrustedNetworkV2PolicyReducer.restore({
|
|
964
|
-
...coreProperties,
|
|
965
|
-
durableState: latestDurableState!,
|
|
966
|
-
});
|
|
967
|
-
assertOpenNotAborted(signal);
|
|
968
|
-
assertCanonicalCoreRestore(core, latestCoreBytes!);
|
|
969
|
-
durableCoreBytes = copyBytes(latestCoreBytes!);
|
|
970
|
-
}
|
|
768
|
+
if (current === undefined) {
|
|
769
|
+
core = new TrustedNetworkV2PolicyReducer(coreProperties);
|
|
770
|
+
return new TrustedNetworkV2DurablePolicyReducer({
|
|
771
|
+
checkpoint,
|
|
772
|
+
core,
|
|
773
|
+
});
|
|
971
774
|
}
|
|
972
775
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
expectedForkObservationCount === undefined ||
|
|
977
|
-
expectedForkObservationCommitment === undefined
|
|
978
|
-
) {
|
|
979
|
-
throw new Error("Restored forked policy anchor has no fork evidence");
|
|
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
|
-
}
|
|
993
|
-
if (canonicalChildren.length !== 2) {
|
|
994
|
-
throw new Error(
|
|
995
|
-
"Stored policy fork evidence has fewer than two distinct children",
|
|
996
|
-
);
|
|
997
|
-
}
|
|
998
|
-
const normalizedForkState: Extract<
|
|
999
|
-
PolicyReducerDurableStateV2,
|
|
1000
|
-
{ state: "FORKED" }
|
|
1001
|
-
> = {
|
|
1002
|
-
formatVersion: REDUCER_DURABLE_FORMAT_VERSION,
|
|
1003
|
-
state: "FORKED",
|
|
1004
|
-
commonParentEntryBytes: copyBytes(
|
|
1005
|
-
latestDurableState.commonParentEntryBytes,
|
|
1006
|
-
),
|
|
1007
|
-
childEntryBytes: [
|
|
1008
|
-
copyBytes(canonicalChildren[0]!.entryBytes),
|
|
1009
|
-
copyBytes(canonicalChildren[1]!.entryBytes),
|
|
1010
|
-
],
|
|
1011
|
-
};
|
|
1012
|
-
const pairChanged = canonicalChildren.some(
|
|
1013
|
-
(child, index) =>
|
|
1014
|
-
!equals(child.digest, forkEvidence!.children[index]!.digest) ||
|
|
1015
|
-
!equals(
|
|
1016
|
-
child.entryBytes,
|
|
1017
|
-
forkEvidence!.children[index]!.entryBytes,
|
|
1018
|
-
),
|
|
1019
|
-
);
|
|
1020
|
-
if (pairChanged) {
|
|
1021
|
-
core.abort();
|
|
1022
|
-
core = await TrustedNetworkV2PolicyReducer.restore({
|
|
1023
|
-
...coreProperties,
|
|
1024
|
-
durableState: normalizedForkState,
|
|
1025
|
-
});
|
|
1026
|
-
assertOpenNotAborted(signal);
|
|
1027
|
-
assertCanonicalCoreRestore(
|
|
1028
|
-
core,
|
|
1029
|
-
serialize(coreRecordFromState(normalizedForkState)),
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
latestDurableState = normalizedForkState;
|
|
1033
|
-
durableCoreBytes = serialize(coreRecordFromState(normalizedForkState));
|
|
1034
|
-
} else if (observedEntries.size !== 0) {
|
|
1035
|
-
throw new Error("Non-forked policy anchor has fork observations");
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
return new TrustedNetworkV2DurablePolicyReducer({
|
|
1039
|
-
store,
|
|
1040
|
-
durability,
|
|
776
|
+
const payload = decodeCanonicalCheckpointPayload(current.payload);
|
|
777
|
+
const decoded = await durableStateFromCheckpointPayload(
|
|
778
|
+
payload,
|
|
1041
779
|
descriptor,
|
|
780
|
+
signal,
|
|
781
|
+
);
|
|
782
|
+
assertOpenNotAborted(signal);
|
|
783
|
+
core = await TrustedNetworkV2PolicyReducer.restore({
|
|
784
|
+
...coreProperties,
|
|
785
|
+
durableState: decoded.durableState,
|
|
786
|
+
});
|
|
787
|
+
assertOpenNotAborted(signal);
|
|
788
|
+
assertCanonicalCoreRestore(core, decoded.durableState);
|
|
789
|
+
const forked = decoded.durableState.state === "FORKED";
|
|
790
|
+
return new TrustedNetworkV2DurablePolicyReducer({
|
|
791
|
+
// The helper retains its latest payload. A terminal fork no longer needs
|
|
792
|
+
// storage, so release that potentially 8.78 MiB snapshot immediately.
|
|
793
|
+
checkpoint: forked ? undefined : checkpoint,
|
|
1042
794
|
core,
|
|
1043
|
-
|
|
1044
|
-
previousGenerationChecksum: previousChecksum,
|
|
1045
|
-
durableCoreBytes,
|
|
795
|
+
durableCoreIdentityBytes: decoded.coreIdentityBytes,
|
|
1046
796
|
});
|
|
1047
797
|
} catch (error) {
|
|
1048
798
|
core?.abort();
|
|
@@ -1068,15 +818,17 @@ export class TrustedNetworkV2DurablePolicyReducer {
|
|
|
1068
818
|
}
|
|
1069
819
|
|
|
1070
820
|
get pendingCount(): number {
|
|
1071
|
-
return this.core.pendingCount;
|
|
821
|
+
return this.published.state === "FORKED" ? 0 : this.core.pendingCount;
|
|
1072
822
|
}
|
|
1073
823
|
|
|
1074
824
|
get pendingBytes(): number {
|
|
1075
|
-
return this.core.pendingBytes;
|
|
825
|
+
return this.published.state === "FORKED" ? 0 : this.core.pendingBytes;
|
|
1076
826
|
}
|
|
1077
827
|
|
|
1078
828
|
get pendingDigests(): Uint8Array[] {
|
|
1079
|
-
return this.
|
|
829
|
+
return this.published.state === "FORKED"
|
|
830
|
+
? []
|
|
831
|
+
: this.core.pendingDigests.map(copyBytes);
|
|
1080
832
|
}
|
|
1081
833
|
|
|
1082
834
|
/** Internal diagnostics for the fixed pre-publication admission bound. */
|
|
@@ -1290,150 +1042,67 @@ export class TrustedNetworkV2DurablePolicyReducer {
|
|
|
1290
1042
|
): Promise<void> {
|
|
1291
1043
|
const durableState = this.core.exportDurableState();
|
|
1292
1044
|
if (durableState.state === "EMPTY") {
|
|
1293
|
-
if (this.
|
|
1294
|
-
throw
|
|
1045
|
+
if (this.durableCoreIdentityBytes !== undefined) {
|
|
1046
|
+
throw new Error("Durable policy state cannot return to EMPTY");
|
|
1047
|
+
}
|
|
1048
|
+
if ((result.forkObservations?.length ?? 0) !== 0) {
|
|
1049
|
+
throw new Error("An empty policy anchor cannot contain fork evidence");
|
|
1295
1050
|
}
|
|
1296
1051
|
return;
|
|
1297
1052
|
}
|
|
1298
1053
|
|
|
1299
|
-
const
|
|
1300
|
-
const coreBytes = serialize(coreRecord);
|
|
1054
|
+
const coreIdentityBytes = coreIdentityBytesFromState(durableState);
|
|
1301
1055
|
const stateChanged =
|
|
1302
|
-
this.
|
|
1303
|
-
!equals(this.
|
|
1304
|
-
const suppliedObservations = new Map<string, Uint8Array>();
|
|
1305
|
-
for (const proof of result.forkObservations ?? []) {
|
|
1306
|
-
const contentHash = observationContentHash(proof.entryBytes);
|
|
1307
|
-
const hashKey = bytesKey(contentHash);
|
|
1308
|
-
const retained = suppliedObservations.get(hashKey);
|
|
1309
|
-
if (retained !== undefined && !equals(retained, proof.entryBytes)) {
|
|
1310
|
-
throw this.halt("Policy fork-observation content hash collision");
|
|
1311
|
-
}
|
|
1312
|
-
suppliedObservations.set(hashKey, copyBytes(proof.entryBytes));
|
|
1313
|
-
}
|
|
1314
|
-
let committedForkObservationCount = 0;
|
|
1315
|
-
let committedForkObservationDigest = copyBytes(ZERO_DIGEST);
|
|
1056
|
+
this.durableCoreIdentityBytes === undefined ||
|
|
1057
|
+
!equals(this.durableCoreIdentityBytes, coreIdentityBytes);
|
|
1316
1058
|
if (durableState.state !== "FORKED") {
|
|
1317
|
-
if (
|
|
1318
|
-
throw
|
|
1319
|
-
}
|
|
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
|
-
);
|
|
1340
|
-
for (const entryBytes of durableState.childEntryBytes) {
|
|
1341
|
-
const hashKey = bytesKey(observationContentHash(entryBytes));
|
|
1342
|
-
if (!suppliedObservations.delete(hashKey)) {
|
|
1343
|
-
throw this.halt(
|
|
1344
|
-
"Published fork state is missing a canonical observation",
|
|
1345
|
-
);
|
|
1346
|
-
}
|
|
1059
|
+
if ((result.forkObservations?.length ?? 0) !== 0) {
|
|
1060
|
+
throw new Error("Only a forked policy anchor may contain observations");
|
|
1347
1061
|
}
|
|
1062
|
+
if (!stateChanged) return;
|
|
1063
|
+
} else if (this.published.state === "FORKED" || !stateChanged) {
|
|
1064
|
+
throw new Error("A durable fork transition may be published only once");
|
|
1348
1065
|
}
|
|
1349
1066
|
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1067
|
+
const nextPublished = publishedFromReducer(this.core);
|
|
1068
|
+
const observationEntries =
|
|
1069
|
+
durableState.state === "FORKED"
|
|
1070
|
+
? canonicalForkEntriesForCommit(
|
|
1071
|
+
result,
|
|
1072
|
+
durableState,
|
|
1073
|
+
nextPublished.forkEvidence,
|
|
1074
|
+
)
|
|
1075
|
+
: [];
|
|
1076
|
+
const payloadBytes = serialize(
|
|
1077
|
+
checkpointPayloadFromState(durableState, observationEntries),
|
|
1352
1078
|
);
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
let nextGeneration = this.generation;
|
|
1360
|
-
let previousChecksum = copyBytes(this.previousGenerationChecksum);
|
|
1361
|
-
const drafts: Array<{
|
|
1362
|
-
kind: number;
|
|
1363
|
-
payloadBytes: Uint8Array;
|
|
1364
|
-
}> = [];
|
|
1365
|
-
if (stateChanged) {
|
|
1366
|
-
const payloadBytes = serialize(
|
|
1367
|
-
new PolicyAnchorStateGenerationPayloadV2({
|
|
1368
|
-
coreState: coreRecord,
|
|
1369
|
-
forkObservationCount: committedForkObservationCount,
|
|
1370
|
-
forkObservationCommitment: committedForkObservationDigest,
|
|
1371
|
-
}),
|
|
1372
|
-
);
|
|
1373
|
-
if (payloadBytes.byteLength > MAX_STATE_GENERATION_PAYLOAD_BYTES) {
|
|
1374
|
-
throw this.halt("Policy-anchor state payload exceeds its byte ceiling");
|
|
1375
|
-
}
|
|
1376
|
-
drafts.push({ kind: GENERATION_KIND.STATE, payloadBytes });
|
|
1377
|
-
}
|
|
1378
|
-
for (const entryBytes of observationEntries) {
|
|
1379
|
-
const payloadBytes = serialize(
|
|
1380
|
-
new PolicyAnchorObservationGenerationPayloadV2({
|
|
1381
|
-
entryBytes: copyBytes(entryBytes),
|
|
1382
|
-
}),
|
|
1079
|
+
if (
|
|
1080
|
+
payloadBytes.byteLength >
|
|
1081
|
+
TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_CHECKPOINT_PAYLOAD_BYTES
|
|
1082
|
+
) {
|
|
1083
|
+
throw new Error(
|
|
1084
|
+
"Policy-anchor checkpoint payload exceeds its byte ceiling",
|
|
1383
1085
|
);
|
|
1384
|
-
if (payloadBytes.byteLength > MAX_OBSERVATION_GENERATION_PAYLOAD_BYTES) {
|
|
1385
|
-
throw this.halt(
|
|
1386
|
-
"Policy fork-observation payload exceeds its byte ceiling",
|
|
1387
|
-
);
|
|
1388
|
-
}
|
|
1389
|
-
drafts.push({
|
|
1390
|
-
kind: GENERATION_KIND.FORK_OBSERVATION,
|
|
1391
|
-
payloadBytes,
|
|
1392
|
-
});
|
|
1393
1086
|
}
|
|
1394
1087
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
});
|
|
1405
|
-
const record = new PolicyAnchorGenerationRecordV2({
|
|
1406
|
-
body,
|
|
1407
|
-
checksum: generationChecksum(body),
|
|
1408
|
-
});
|
|
1409
|
-
const recordBytes = serialize(record);
|
|
1410
|
-
if (
|
|
1411
|
-
recordBytes.byteLength >
|
|
1412
|
-
TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_GENERATION_BYTES
|
|
1413
|
-
) {
|
|
1414
|
-
throw this.halt(
|
|
1415
|
-
"Policy-anchor generation record exceeds its byte ceiling",
|
|
1416
|
-
);
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
const key = generationKey(nextGeneration);
|
|
1420
|
-
if ((await this.store.get(key)) !== undefined) {
|
|
1421
|
-
throw this.halt("Policy-anchor generation must never be overwritten");
|
|
1422
|
-
}
|
|
1423
|
-
await this.store.put(key, copyBytes(recordBytes));
|
|
1424
|
-
// Each immutable generation gets one physical fence. The projection is
|
|
1425
|
-
// published only after every state/observation delta is fenced.
|
|
1426
|
-
await this.durability.barrier();
|
|
1427
|
-
previousChecksum = copyBytes(record.checksum);
|
|
1088
|
+
// Allocate every post-commit publication value before durable replacement.
|
|
1089
|
+
// Once commit resolves, the remaining operations are non-throwing assignments.
|
|
1090
|
+
const nextDurableCoreIdentityBytes =
|
|
1091
|
+
durableState.state === "FORKED"
|
|
1092
|
+
? undefined
|
|
1093
|
+
: copyBytes(coreIdentityBytes);
|
|
1094
|
+
const checkpoint = this.checkpoint;
|
|
1095
|
+
if (checkpoint === undefined) {
|
|
1096
|
+
throw new Error("Policy-anchor checkpoint is unavailable");
|
|
1428
1097
|
}
|
|
1098
|
+
await checkpoint.commit(payloadBytes);
|
|
1429
1099
|
|
|
1430
|
-
this.
|
|
1431
|
-
this.
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
throw this.halt(error);
|
|
1100
|
+
this.durableCoreIdentityBytes = nextDurableCoreIdentityBytes;
|
|
1101
|
+
this.published = nextPublished;
|
|
1102
|
+
if (durableState.state === "FORKED") {
|
|
1103
|
+
// The terminal projection is self-contained; drop the helper's retained
|
|
1104
|
+
// copy of the potentially maximum-sized checkpoint payload.
|
|
1105
|
+
this.checkpoint = undefined;
|
|
1437
1106
|
}
|
|
1438
1107
|
}
|
|
1439
1108
|
}
|