@agentplat/mesh 0.3.0-beta.2 → 0.3.0-beta.3
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/README.md +133 -4
- package/dist/adaptive-overlay-contracts.d.ts +149 -0
- package/dist/adaptive-overlay-contracts.d.ts.map +1 -0
- package/dist/adaptive-overlay-contracts.js +3 -0
- package/dist/adaptive-overlay-contracts.js.map +1 -0
- package/dist/adaptive-overlay-runtime.d.ts +67 -0
- package/dist/adaptive-overlay-runtime.d.ts.map +1 -0
- package/dist/adaptive-overlay-runtime.js +427 -0
- package/dist/adaptive-overlay-runtime.js.map +1 -0
- package/dist/adaptive-overlay-validation.d.ts +19 -0
- package/dist/adaptive-overlay-validation.d.ts.map +1 -0
- package/dist/adaptive-overlay-validation.js +326 -0
- package/dist/adaptive-overlay-validation.js.map +1 -0
- package/dist/adaptive-overlay.d.ts +4 -0
- package/dist/adaptive-overlay.d.ts.map +1 -0
- package/dist/adaptive-overlay.js +4 -0
- package/dist/adaptive-overlay.js.map +1 -0
- package/dist/authenticated-overlay-transport.d.ts +227 -0
- package/dist/authenticated-overlay-transport.d.ts.map +1 -0
- package/dist/authenticated-overlay-transport.js +702 -0
- package/dist/authenticated-overlay-transport.js.map +1 -0
- package/dist/authority-continuity-contracts.d.ts +249 -0
- package/dist/authority-continuity-contracts.d.ts.map +1 -0
- package/dist/authority-continuity-contracts.js +11 -0
- package/dist/authority-continuity-contracts.js.map +1 -0
- package/dist/authority-continuity.d.ts +43 -0
- package/dist/authority-continuity.d.ts.map +1 -0
- package/dist/authority-continuity.js +1338 -0
- package/dist/authority-continuity.js.map +1 -0
- package/dist/continuity.d.ts +3 -0
- package/dist/continuity.d.ts.map +1 -0
- package/dist/continuity.js +3 -0
- package/dist/continuity.js.map +1 -0
- package/dist/coordination-allocation-contracts.d.ts +6 -0
- package/dist/coordination-allocation-contracts.d.ts.map +1 -1
- package/dist/coordination-allocation-state.d.ts.map +1 -1
- package/dist/coordination-allocation-state.js +37 -7
- package/dist/coordination-allocation-state.js.map +1 -1
- package/dist/coordination-allocation.d.ts +1 -1
- package/dist/coordination-allocation.d.ts.map +1 -1
- package/dist/coordination-allocation.js +92 -11
- package/dist/coordination-allocation.js.map +1 -1
- package/dist/coordination-discovery-state.d.ts.map +1 -1
- package/dist/coordination-discovery-state.js +5 -3
- package/dist/coordination-discovery-state.js.map +1 -1
- package/dist/coordination-recovery.js +5 -1
- package/dist/coordination-recovery.js.map +1 -1
- package/dist/durability.d.ts +22 -0
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +22 -0
- package/dist/durability.js.map +1 -1
- package/dist/overlay-transport.d.ts +7 -0
- package/dist/overlay-transport.d.ts.map +1 -0
- package/dist/overlay-transport.js +7 -0
- package/dist/overlay-transport.js.map +1 -0
- package/dist/overlay.d.ts +9 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +9 -0
- package/dist/overlay.js.map +1 -0
- package/dist/sparse-overlay-contracts.d.ts +91 -0
- package/dist/sparse-overlay-contracts.d.ts.map +1 -0
- package/dist/sparse-overlay-contracts.js +2 -0
- package/dist/sparse-overlay-contracts.js.map +1 -0
- package/dist/sparse-overlay-durability.d.ts +23 -0
- package/dist/sparse-overlay-durability.d.ts.map +1 -0
- package/dist/sparse-overlay-durability.js +97 -0
- package/dist/sparse-overlay-durability.js.map +1 -0
- package/dist/sparse-overlay-runtime-contracts.d.ts +158 -0
- package/dist/sparse-overlay-runtime-contracts.d.ts.map +1 -0
- package/dist/sparse-overlay-runtime-contracts.js +2 -0
- package/dist/sparse-overlay-runtime-contracts.js.map +1 -0
- package/dist/sparse-overlay-runtime.d.ts +48 -0
- package/dist/sparse-overlay-runtime.d.ts.map +1 -0
- package/dist/sparse-overlay-runtime.js +763 -0
- package/dist/sparse-overlay-runtime.js.map +1 -0
- package/dist/sparse-overlay-transport-contracts.d.ts +183 -0
- package/dist/sparse-overlay-transport-contracts.d.ts.map +1 -0
- package/dist/sparse-overlay-transport-contracts.js +2 -0
- package/dist/sparse-overlay-transport-contracts.js.map +1 -0
- package/dist/sparse-overlay-transport-durability.d.ts +19 -0
- package/dist/sparse-overlay-transport-durability.d.ts.map +1 -0
- package/dist/sparse-overlay-transport-durability.js +91 -0
- package/dist/sparse-overlay-transport-durability.js.map +1 -0
- package/dist/sparse-overlay-transport.d.ts +97 -0
- package/dist/sparse-overlay-transport.d.ts.map +1 -0
- package/dist/sparse-overlay-transport.js +888 -0
- package/dist/sparse-overlay-transport.js.map +1 -0
- package/dist/sparse-overlay.d.ts +74 -0
- package/dist/sparse-overlay.d.ts.map +1 -0
- package/dist/sparse-overlay.js +817 -0
- package/dist/sparse-overlay.js.map +1 -0
- package/package.json +20 -4
|
@@ -0,0 +1,1338 @@
|
|
|
1
|
+
import { sha256Base64Url } from "./sha256.js";
|
|
2
|
+
import { MeshAuthorityContinuityErrorV1, } from "./authority-continuity-contracts.js";
|
|
3
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:@-]*$/u;
|
|
4
|
+
const DIGEST = /^sha256:[A-Za-z0-9_-]{43}$/u;
|
|
5
|
+
const DEFAULT_MAXIMUM_TRANSITIONS = 256;
|
|
6
|
+
const DEFAULT_MAXIMUM_PROOF_BYTES = 16_384;
|
|
7
|
+
const MAXIMUM_ARRAY = 4_096;
|
|
8
|
+
const utf8 = new TextEncoder();
|
|
9
|
+
/** Stable key for one independently fenced authority scope. */
|
|
10
|
+
export function meshAuthorityScopeKeyV1(scope) {
|
|
11
|
+
const normalized = normalizeScope(scope);
|
|
12
|
+
return `authority:${sha256Base64Url(utf8.encode(stableJson(normalized)))}`;
|
|
13
|
+
}
|
|
14
|
+
/** Canonical signer-and-statement bytes signed by an evidence provider. */
|
|
15
|
+
export function meshAuthorityEvidenceBytesV1(input) {
|
|
16
|
+
if (!input || typeof input !== "object" || input.schemaVersion !== 1) {
|
|
17
|
+
invalid("authority evidence input is invalid");
|
|
18
|
+
}
|
|
19
|
+
return utf8.encode(stableJson({
|
|
20
|
+
schemaVersion: 1,
|
|
21
|
+
statement: normalizeStatement(input.statement),
|
|
22
|
+
signer: normalizeIdentity(input.signer, "evidence.signer"),
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
export function createMeshAuthorityContinuityPolicyV1(input) {
|
|
26
|
+
const body = {
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
policyId: identifier(input.policyId, "policyId"),
|
|
29
|
+
policyRevision: positive(input.policyRevision, "policyRevision"),
|
|
30
|
+
witnessPeerIds: identifiers(input.witnessPeerIds, "witnessPeerIds", true),
|
|
31
|
+
witnessThreshold: positive(input.witnessThreshold, "witnessThreshold"),
|
|
32
|
+
recoveryDelayMs: safeInteger(input.recoveryDelayMs, "recoveryDelayMs", 0),
|
|
33
|
+
maximumProposalLifetimeMs: positive(input.maximumProposalLifetimeMs, "maximumProposalLifetimeMs"),
|
|
34
|
+
validUntilLogicalMs: positive(input.validUntilLogicalMs, "validUntilLogicalMs"),
|
|
35
|
+
};
|
|
36
|
+
if (body.witnessThreshold > body.witnessPeerIds.length ||
|
|
37
|
+
body.witnessThreshold <= Math.floor(body.witnessPeerIds.length / 2)) {
|
|
38
|
+
invalid("witnessThreshold must be a strict majority of the witness set");
|
|
39
|
+
}
|
|
40
|
+
return freeze({
|
|
41
|
+
...body,
|
|
42
|
+
policyDigest: digestOf("authority-policy", body),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function createMeshAuthorityHeadV1(input) {
|
|
46
|
+
const scope = normalizeScope(input.scope);
|
|
47
|
+
const policy = normalizePolicy(input.policy);
|
|
48
|
+
const body = {
|
|
49
|
+
schemaVersion: 1,
|
|
50
|
+
scope,
|
|
51
|
+
scopeKey: meshAuthorityScopeKeyV1(scope),
|
|
52
|
+
generation: positive(input.generation, "generation"),
|
|
53
|
+
holder: normalizeIdentity(input.holder, "holder"),
|
|
54
|
+
activatedBy: oneOf(input.activatedBy, ["bootstrap", "certified_transition"], "activatedBy"),
|
|
55
|
+
activationId: identifier(input.activationId, "activationId"),
|
|
56
|
+
predecessorHeadDigest: input.predecessorHeadDigest === null
|
|
57
|
+
? null
|
|
58
|
+
: digest(input.predecessorHeadDigest, "predecessorHeadDigest"),
|
|
59
|
+
fencingToken: identifier(input.fencingToken, "fencingToken"),
|
|
60
|
+
activatedAtLogicalMs: safeInteger(input.activatedAtLogicalMs, "activatedAtLogicalMs", 0),
|
|
61
|
+
holderValidUntilLogicalMs: positive(input.holderValidUntilLogicalMs, "holderValidUntilLogicalMs"),
|
|
62
|
+
policy,
|
|
63
|
+
};
|
|
64
|
+
if (body.holderValidUntilLogicalMs <= body.activatedAtLogicalMs ||
|
|
65
|
+
body.holderValidUntilLogicalMs > policy.validUntilLogicalMs ||
|
|
66
|
+
policy.witnessPeerIds.includes(body.holder.peerId) ||
|
|
67
|
+
(body.activatedBy === "bootstrap") !== (body.predecessorHeadDigest === null)) {
|
|
68
|
+
invalid("authority head validity or predecessor is invalid");
|
|
69
|
+
}
|
|
70
|
+
return freeze({ ...body, headDigest: digestOf("authority-head", body) });
|
|
71
|
+
}
|
|
72
|
+
export function createMeshAuthorityProposalV1(input) {
|
|
73
|
+
const scope = normalizeScope(input.scope);
|
|
74
|
+
const body = {
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
proposalId: identifier(input.proposalId, "proposalId"),
|
|
77
|
+
scope,
|
|
78
|
+
scopeKey: meshAuthorityScopeKeyV1(scope),
|
|
79
|
+
mode: oneOf(input.mode, ["coordinated_transfer", "witness_recovery"], "mode"),
|
|
80
|
+
previousHeadDigest: digest(input.previousHeadDigest, "previousHeadDigest"),
|
|
81
|
+
previousGeneration: positive(input.previousGeneration, "previousGeneration"),
|
|
82
|
+
proposedGeneration: positive(input.proposedGeneration, "proposedGeneration"),
|
|
83
|
+
previousHolder: normalizeIdentity(input.previousHolder, "previousHolder"),
|
|
84
|
+
successor: normalizeIdentity(input.successor, "successor"),
|
|
85
|
+
successorValidUntilLogicalMs: positive(input.successorValidUntilLogicalMs, "successorValidUntilLogicalMs"),
|
|
86
|
+
successorPolicy: normalizePolicy(input.successorPolicy),
|
|
87
|
+
proposedAtLogicalMs: safeInteger(input.proposedAtLogicalMs, "proposedAtLogicalMs", 0),
|
|
88
|
+
notBeforeLogicalMs: safeInteger(input.notBeforeLogicalMs, "notBeforeLogicalMs", 0),
|
|
89
|
+
expiresAtLogicalMs: positive(input.expiresAtLogicalMs, "expiresAtLogicalMs"),
|
|
90
|
+
};
|
|
91
|
+
if (body.proposedGeneration !== body.previousGeneration + 1 ||
|
|
92
|
+
body.notBeforeLogicalMs < body.proposedAtLogicalMs ||
|
|
93
|
+
body.expiresAtLogicalMs <= body.notBeforeLogicalMs ||
|
|
94
|
+
body.successorValidUntilLogicalMs <= body.notBeforeLogicalMs ||
|
|
95
|
+
body.successorPolicy.witnessPeerIds.includes(body.successor.peerId) ||
|
|
96
|
+
sameIdentity(body.previousHolder, body.successor)) {
|
|
97
|
+
invalid("authority proposal generations, timing or successor is invalid");
|
|
98
|
+
}
|
|
99
|
+
return freeze({
|
|
100
|
+
...body,
|
|
101
|
+
proposalDigest: digestOf("authority-proposal", body),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function createMeshAuthorityEndorsementV1(input) {
|
|
105
|
+
const body = {
|
|
106
|
+
schemaVersion: 1,
|
|
107
|
+
endorsementId: identifier(input.endorsementId, "endorsementId"),
|
|
108
|
+
scopeKey: identifier(input.scopeKey, "scopeKey"),
|
|
109
|
+
proposalId: identifier(input.proposalId, "proposalId"),
|
|
110
|
+
proposalDigest: digest(input.proposalDigest, "proposalDigest"),
|
|
111
|
+
witnessPeerId: identifier(input.witnessPeerId, "witnessPeerId"),
|
|
112
|
+
observedUnavailableSinceLogicalMs: safeInteger(input.observedUnavailableSinceLogicalMs, "observedUnavailableSinceLogicalMs", 0),
|
|
113
|
+
endorsedAtLogicalMs: safeInteger(input.endorsedAtLogicalMs, "endorsedAtLogicalMs", 0),
|
|
114
|
+
};
|
|
115
|
+
if (body.observedUnavailableSinceLogicalMs > body.endorsedAtLogicalMs) {
|
|
116
|
+
invalid("witness observation follows its endorsement");
|
|
117
|
+
}
|
|
118
|
+
return freeze({
|
|
119
|
+
...body,
|
|
120
|
+
endorsementDigest: digestOf("authority-endorsement", body),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export function createMeshAuthorityAcceptanceV1(input) {
|
|
124
|
+
const body = {
|
|
125
|
+
schemaVersion: 1,
|
|
126
|
+
acceptanceId: identifier(input.acceptanceId, "acceptanceId"),
|
|
127
|
+
scopeKey: identifier(input.scopeKey, "scopeKey"),
|
|
128
|
+
proposalId: identifier(input.proposalId, "proposalId"),
|
|
129
|
+
proposalDigest: digest(input.proposalDigest, "proposalDigest"),
|
|
130
|
+
certificateId: identifier(input.certificateId, "certificateId"),
|
|
131
|
+
certificateDigest: digest(input.certificateDigest, "certificateDigest"),
|
|
132
|
+
successor: normalizeIdentity(input.successor, "successor"),
|
|
133
|
+
acceptedAtLogicalMs: safeInteger(input.acceptedAtLogicalMs, "acceptedAtLogicalMs", 0),
|
|
134
|
+
};
|
|
135
|
+
return freeze({
|
|
136
|
+
...body,
|
|
137
|
+
acceptanceDigest: digestOf("authority-acceptance", body),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** CAS-backed productive runtime for certified authority succession. */
|
|
141
|
+
export class MeshAuthorityContinuityRuntimeV1 {
|
|
142
|
+
options;
|
|
143
|
+
maximumTransitions;
|
|
144
|
+
maximumProofBytes;
|
|
145
|
+
constructor(options) {
|
|
146
|
+
this.options = options;
|
|
147
|
+
if (!options ||
|
|
148
|
+
typeof options !== "object" ||
|
|
149
|
+
!options.store ||
|
|
150
|
+
typeof options.store.load !== "function" ||
|
|
151
|
+
typeof options.store.compareAndSwap !== "function") {
|
|
152
|
+
invalid("authority continuity store is required");
|
|
153
|
+
}
|
|
154
|
+
if (!options.verifier || typeof options.verifier.verify !== "function") {
|
|
155
|
+
invalid("authority evidence verifier is required");
|
|
156
|
+
}
|
|
157
|
+
if (!options.eligibility ||
|
|
158
|
+
typeof options.eligibility.check !== "function") {
|
|
159
|
+
invalid("successor eligibility provider is required");
|
|
160
|
+
}
|
|
161
|
+
identifier(options.verifier.verifierId, "verifierId");
|
|
162
|
+
positive(options.verifier.verifierVersion, "verifierVersion");
|
|
163
|
+
identifier(options.verifier.implementationId, "verifier implementationId");
|
|
164
|
+
identifier(options.eligibility.eligibilityId, "eligibilityId");
|
|
165
|
+
positive(options.eligibility.eligibilityVersion, "eligibilityVersion");
|
|
166
|
+
identifier(options.eligibility.implementationId, "eligibility implementationId");
|
|
167
|
+
this.maximumTransitions = boundedLimit(options.maximumTransitions, DEFAULT_MAXIMUM_TRANSITIONS, "maximumTransitions", 1, 16_384);
|
|
168
|
+
this.maximumProofBytes = boundedLimit(options.maximumProofBytes, DEFAULT_MAXIMUM_PROOF_BYTES, "maximumProofBytes", 64, 1_048_576);
|
|
169
|
+
}
|
|
170
|
+
async initialize(input) {
|
|
171
|
+
const head = normalizeHead(input.head);
|
|
172
|
+
const logicalTimeMs = safeInteger(input.logicalTimeMs, "logicalTimeMs", head.activatedAtLogicalMs);
|
|
173
|
+
const snapshot = buildSnapshot({
|
|
174
|
+
revision: 1,
|
|
175
|
+
head,
|
|
176
|
+
activeProposal: null,
|
|
177
|
+
endorsements: [],
|
|
178
|
+
certificate: null,
|
|
179
|
+
transitions: [],
|
|
180
|
+
lastLogicalTimeMs: logicalTimeMs,
|
|
181
|
+
bindings: this.bindings(),
|
|
182
|
+
maximumTransitions: this.maximumTransitions,
|
|
183
|
+
maximumProofBytes: this.maximumProofBytes,
|
|
184
|
+
});
|
|
185
|
+
if (!(await this.options.store.compareAndSwap({
|
|
186
|
+
scopeKey: head.scopeKey,
|
|
187
|
+
expectedRevision: null,
|
|
188
|
+
next: snapshot,
|
|
189
|
+
}))) {
|
|
190
|
+
conflict("authority scope is already initialized");
|
|
191
|
+
}
|
|
192
|
+
return snapshot;
|
|
193
|
+
}
|
|
194
|
+
async getSnapshot(scopeKey) {
|
|
195
|
+
const value = await this.options.store.load(identifier(scopeKey, "scopeKey"));
|
|
196
|
+
if (!value)
|
|
197
|
+
return undefined;
|
|
198
|
+
return this.boundSnapshot(value);
|
|
199
|
+
}
|
|
200
|
+
async recordProposal(input) {
|
|
201
|
+
const current = await this.requireSnapshot(input.scopeKey, input.expectedRevision, input.logicalTimeMs);
|
|
202
|
+
const proposal = normalizeSignedRecord(input.proposal, "proposal", this.maximumProofBytes);
|
|
203
|
+
const statement = proposal.statement;
|
|
204
|
+
assertProposalBinding(current, proposal, input.logicalTimeMs);
|
|
205
|
+
if (current.activeProposal &&
|
|
206
|
+
input.logicalTimeMs < current.activeProposal.statement.expiresAtLogicalMs) {
|
|
207
|
+
conflict("an unexpired authority proposal already exists");
|
|
208
|
+
}
|
|
209
|
+
await this.verify("proposal", current.head.scope, proposal, input.logicalTimeMs);
|
|
210
|
+
await this.checkEligibility(statement, input.logicalTimeMs);
|
|
211
|
+
const next = buildSnapshot({
|
|
212
|
+
...snapshotParts(current),
|
|
213
|
+
revision: current.revision + 1,
|
|
214
|
+
activeProposal: proposal,
|
|
215
|
+
endorsements: [],
|
|
216
|
+
certificate: null,
|
|
217
|
+
lastLogicalTimeMs: input.logicalTimeMs,
|
|
218
|
+
bindings: this.bindings(),
|
|
219
|
+
maximumTransitions: this.maximumTransitions,
|
|
220
|
+
maximumProofBytes: this.maximumProofBytes,
|
|
221
|
+
});
|
|
222
|
+
return this.swap(current, next);
|
|
223
|
+
}
|
|
224
|
+
async recordEndorsement(input) {
|
|
225
|
+
const current = await this.requireSnapshot(input.scopeKey, input.expectedRevision, input.logicalTimeMs);
|
|
226
|
+
const endorsement = normalizeSignedRecord(input.endorsement, "endorsement", this.maximumProofBytes);
|
|
227
|
+
assertEndorsementBinding(current, endorsement, input.logicalTimeMs);
|
|
228
|
+
const existing = current.endorsements.find(({ statement }) => statement.witnessPeerId === endorsement.statement.witnessPeerId ||
|
|
229
|
+
statement.endorsementId === endorsement.statement.endorsementId);
|
|
230
|
+
if (existing) {
|
|
231
|
+
if (stableJson(existing) === stableJson(endorsement))
|
|
232
|
+
return current;
|
|
233
|
+
conflict("witness already endorsed the active authority proposal");
|
|
234
|
+
}
|
|
235
|
+
await this.verify("endorsement", current.head.scope, endorsement, input.logicalTimeMs);
|
|
236
|
+
const endorsements = [...current.endorsements, endorsement].sort((a, b) => compare(a.statement.witnessPeerId, b.statement.witnessPeerId));
|
|
237
|
+
const next = buildSnapshot({
|
|
238
|
+
...snapshotParts(current),
|
|
239
|
+
revision: current.revision + 1,
|
|
240
|
+
endorsements,
|
|
241
|
+
lastLogicalTimeMs: input.logicalTimeMs,
|
|
242
|
+
bindings: this.bindings(),
|
|
243
|
+
maximumTransitions: this.maximumTransitions,
|
|
244
|
+
maximumProofBytes: this.maximumProofBytes,
|
|
245
|
+
});
|
|
246
|
+
return this.swap(current, next);
|
|
247
|
+
}
|
|
248
|
+
async issueCertificate(input) {
|
|
249
|
+
const current = await this.requireSnapshot(input.scopeKey, input.expectedRevision, input.logicalTimeMs);
|
|
250
|
+
const proposalId = identifier(input.proposalId, "proposalId");
|
|
251
|
+
const proposal = current.activeProposal?.statement;
|
|
252
|
+
if (!proposal || proposal.proposalId !== proposalId) {
|
|
253
|
+
notReady("active authority proposal is unavailable");
|
|
254
|
+
}
|
|
255
|
+
if (current.certificate)
|
|
256
|
+
return current;
|
|
257
|
+
if (input.logicalTimeMs < proposal.notBeforeLogicalMs ||
|
|
258
|
+
input.logicalTimeMs >= proposal.expiresAtLogicalMs ||
|
|
259
|
+
input.logicalTimeMs >= current.head.policy.validUntilLogicalMs) {
|
|
260
|
+
notReady("authority proposal is outside its certification window");
|
|
261
|
+
}
|
|
262
|
+
if (proposal.mode === "witness_recovery" &&
|
|
263
|
+
current.endorsements.length < current.head.policy.witnessThreshold) {
|
|
264
|
+
notReady("authority recovery witness threshold is not met");
|
|
265
|
+
}
|
|
266
|
+
const certificate = createCertificate(current, proposal, input.logicalTimeMs);
|
|
267
|
+
const next = buildSnapshot({
|
|
268
|
+
...snapshotParts(current),
|
|
269
|
+
revision: current.revision + 1,
|
|
270
|
+
certificate,
|
|
271
|
+
lastLogicalTimeMs: input.logicalTimeMs,
|
|
272
|
+
bindings: this.bindings(),
|
|
273
|
+
maximumTransitions: this.maximumTransitions,
|
|
274
|
+
maximumProofBytes: this.maximumProofBytes,
|
|
275
|
+
});
|
|
276
|
+
return this.swap(current, next);
|
|
277
|
+
}
|
|
278
|
+
async accept(input) {
|
|
279
|
+
const current = await this.requireSnapshot(input.scopeKey, input.expectedRevision, input.logicalTimeMs);
|
|
280
|
+
const acceptance = normalizeSignedRecord(input.acceptance, "acceptance", this.maximumProofBytes);
|
|
281
|
+
const proposalRecord = current.activeProposal;
|
|
282
|
+
const certificate = current.certificate;
|
|
283
|
+
if (!proposalRecord || !certificate) {
|
|
284
|
+
notReady("certified authority proposal is unavailable");
|
|
285
|
+
}
|
|
286
|
+
assertAcceptanceBinding(current, proposalRecord.statement, certificate, acceptance, input.logicalTimeMs);
|
|
287
|
+
await this.verify("acceptance", current.head.scope, acceptance, input.logicalTimeMs);
|
|
288
|
+
await this.checkEligibility(proposalRecord.statement, input.logicalTimeMs);
|
|
289
|
+
if (current.transitions.length >= this.maximumTransitions) {
|
|
290
|
+
conflict("authority transition evidence capacity is exhausted");
|
|
291
|
+
}
|
|
292
|
+
const newHead = transitionedHead(current.head, proposalRecord.statement, certificate, acceptance.statement);
|
|
293
|
+
const evidence = freeze({
|
|
294
|
+
schemaVersion: 1,
|
|
295
|
+
previousHead: current.head,
|
|
296
|
+
proposal: proposalRecord,
|
|
297
|
+
endorsements: current.endorsements,
|
|
298
|
+
certificate,
|
|
299
|
+
acceptance,
|
|
300
|
+
resultingHeadDigest: newHead.headDigest,
|
|
301
|
+
});
|
|
302
|
+
const next = buildSnapshot({
|
|
303
|
+
revision: current.revision + 1,
|
|
304
|
+
head: newHead,
|
|
305
|
+
activeProposal: null,
|
|
306
|
+
endorsements: [],
|
|
307
|
+
certificate: null,
|
|
308
|
+
transitions: [...current.transitions, evidence],
|
|
309
|
+
lastLogicalTimeMs: input.logicalTimeMs,
|
|
310
|
+
bindings: this.bindings(),
|
|
311
|
+
maximumTransitions: this.maximumTransitions,
|
|
312
|
+
maximumProofBytes: this.maximumProofBytes,
|
|
313
|
+
});
|
|
314
|
+
return this.swap(current, next);
|
|
315
|
+
}
|
|
316
|
+
async checkCurrent(binding) {
|
|
317
|
+
try {
|
|
318
|
+
const normalized = normalizeCurrentBinding(binding);
|
|
319
|
+
const snapshot = await this.getSnapshot(normalized.scopeKey);
|
|
320
|
+
if (!snapshot) {
|
|
321
|
+
return freeze({
|
|
322
|
+
current: false,
|
|
323
|
+
reasonCode: "authority_scope_missing",
|
|
324
|
+
head: null,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const head = snapshot.head;
|
|
328
|
+
const current = normalized.generation === head.generation &&
|
|
329
|
+
sameIdentity(normalized.holder, head.holder) &&
|
|
330
|
+
normalized.headDigest === head.headDigest &&
|
|
331
|
+
normalized.fencingToken === head.fencingToken &&
|
|
332
|
+
normalized.logicalTimeMs >= head.activatedAtLogicalMs &&
|
|
333
|
+
normalized.logicalTimeMs < head.holderValidUntilLogicalMs;
|
|
334
|
+
return current
|
|
335
|
+
? freeze({
|
|
336
|
+
current: true,
|
|
337
|
+
reasonCode: "current",
|
|
338
|
+
head,
|
|
339
|
+
})
|
|
340
|
+
: freeze({
|
|
341
|
+
current: false,
|
|
342
|
+
reasonCode: "authority_not_current",
|
|
343
|
+
head,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return freeze({
|
|
348
|
+
current: false,
|
|
349
|
+
reasonCode: "authority_currentness_unavailable",
|
|
350
|
+
head: null,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
bindings() {
|
|
355
|
+
return {
|
|
356
|
+
verifierId: this.options.verifier.verifierId,
|
|
357
|
+
verifierVersion: this.options.verifier.verifierVersion,
|
|
358
|
+
verifierImplementationId: this.options.verifier.implementationId,
|
|
359
|
+
eligibilityId: this.options.eligibility.eligibilityId,
|
|
360
|
+
eligibilityVersion: this.options.eligibility.eligibilityVersion,
|
|
361
|
+
eligibilityImplementationId: this.options.eligibility.implementationId,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
boundSnapshot(value) {
|
|
365
|
+
const snapshot = validateMeshAuthorityContinuitySnapshotV1(value);
|
|
366
|
+
const bindings = this.bindings();
|
|
367
|
+
if (snapshot.verifierId !== bindings.verifierId ||
|
|
368
|
+
snapshot.verifierVersion !== bindings.verifierVersion ||
|
|
369
|
+
snapshot.verifierImplementationId !== bindings.verifierImplementationId ||
|
|
370
|
+
snapshot.eligibilityId !== bindings.eligibilityId ||
|
|
371
|
+
snapshot.eligibilityVersion !== bindings.eligibilityVersion ||
|
|
372
|
+
snapshot.eligibilityImplementationId !==
|
|
373
|
+
bindings.eligibilityImplementationId ||
|
|
374
|
+
snapshot.maximumTransitions !== this.maximumTransitions ||
|
|
375
|
+
snapshot.maximumProofBytes !== this.maximumProofBytes) {
|
|
376
|
+
conflict("authority continuity implementation binding changed");
|
|
377
|
+
}
|
|
378
|
+
return snapshot;
|
|
379
|
+
}
|
|
380
|
+
async requireSnapshot(scopeKey, expectedRevision, logicalTimeMs) {
|
|
381
|
+
const value = await this.getSnapshot(scopeKey);
|
|
382
|
+
if (!value) {
|
|
383
|
+
throw new MeshAuthorityContinuityErrorV1("NOT_FOUND", "authority scope is not initialized");
|
|
384
|
+
}
|
|
385
|
+
if (value.revision !== positive(expectedRevision, "expectedRevision") ||
|
|
386
|
+
safeInteger(logicalTimeMs, "logicalTimeMs", value.lastLogicalTimeMs) <
|
|
387
|
+
value.lastLogicalTimeMs) {
|
|
388
|
+
conflict("authority snapshot revision or logical time is stale");
|
|
389
|
+
}
|
|
390
|
+
return value;
|
|
391
|
+
}
|
|
392
|
+
async verify(purpose, scope, record, logicalTimeMs) {
|
|
393
|
+
let decision;
|
|
394
|
+
try {
|
|
395
|
+
decision = await this.options.verifier.verify({
|
|
396
|
+
schemaVersion: 1,
|
|
397
|
+
purpose,
|
|
398
|
+
scope,
|
|
399
|
+
record,
|
|
400
|
+
logicalTimeMs,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
rejected("authority evidence verifier failed closed");
|
|
405
|
+
}
|
|
406
|
+
if (!decision ||
|
|
407
|
+
typeof decision !== "object" ||
|
|
408
|
+
!exactDecision(decision, "verified") ||
|
|
409
|
+
decision.verified !== true ||
|
|
410
|
+
decision.reasonCode !== "verified") {
|
|
411
|
+
rejected("authority evidence was not verified");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async checkEligibility(proposal, logicalTimeMs) {
|
|
415
|
+
let decision;
|
|
416
|
+
try {
|
|
417
|
+
decision = await this.options.eligibility.check({
|
|
418
|
+
schemaVersion: 1,
|
|
419
|
+
scope: proposal.scope,
|
|
420
|
+
successor: proposal.successor,
|
|
421
|
+
proposedGeneration: proposal.proposedGeneration,
|
|
422
|
+
logicalTimeMs,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
ineligible("successor eligibility provider failed closed");
|
|
427
|
+
}
|
|
428
|
+
if (!decision ||
|
|
429
|
+
typeof decision !== "object" ||
|
|
430
|
+
!exactDecision(decision, "eligible") ||
|
|
431
|
+
decision.eligible !== true ||
|
|
432
|
+
decision.reasonCode !== "eligible") {
|
|
433
|
+
ineligible("authority successor is not eligible");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async swap(current, next) {
|
|
437
|
+
if (!(await this.options.store.compareAndSwap({
|
|
438
|
+
scopeKey: current.scopeKey,
|
|
439
|
+
expectedRevision: current.revision,
|
|
440
|
+
next,
|
|
441
|
+
}))) {
|
|
442
|
+
conflict("authority snapshot changed concurrently");
|
|
443
|
+
}
|
|
444
|
+
return next;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** Minimal reference store. Production stores implement the same CAS port. */
|
|
448
|
+
export class InMemoryMeshAuthorityContinuityStoreV1 {
|
|
449
|
+
values = new Map();
|
|
450
|
+
async load(scopeKey) {
|
|
451
|
+
return this.values.get(scopeKey);
|
|
452
|
+
}
|
|
453
|
+
async compareAndSwap(input) {
|
|
454
|
+
const current = this.values.get(input.scopeKey);
|
|
455
|
+
if ((input.expectedRevision === null && current !== undefined) ||
|
|
456
|
+
(input.expectedRevision !== null &&
|
|
457
|
+
current?.revision !== input.expectedRevision)) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
this.values.set(input.scopeKey, input.next);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/** Strict validation used by stores when restoring durable snapshots. */
|
|
465
|
+
export function validateMeshAuthorityContinuitySnapshotV1(value) {
|
|
466
|
+
record(value, "snapshot");
|
|
467
|
+
exactKeys(value, [
|
|
468
|
+
"activeProposal",
|
|
469
|
+
"certificate",
|
|
470
|
+
"eligibilityId",
|
|
471
|
+
"eligibilityImplementationId",
|
|
472
|
+
"eligibilityVersion",
|
|
473
|
+
"endorsements",
|
|
474
|
+
"head",
|
|
475
|
+
"lastLogicalTimeMs",
|
|
476
|
+
"maximumProofBytes",
|
|
477
|
+
"maximumTransitions",
|
|
478
|
+
"revision",
|
|
479
|
+
"schemaVersion",
|
|
480
|
+
"scopeKey",
|
|
481
|
+
"snapshotDigest",
|
|
482
|
+
"transitions",
|
|
483
|
+
"verifierId",
|
|
484
|
+
"verifierImplementationId",
|
|
485
|
+
"verifierVersion",
|
|
486
|
+
]);
|
|
487
|
+
if (value.schemaVersion !== 1)
|
|
488
|
+
invalid("snapshot schemaVersion is invalid");
|
|
489
|
+
const maximumTransitions = boundedLimit(value.maximumTransitions, DEFAULT_MAXIMUM_TRANSITIONS, "maximumTransitions", 1, 16_384);
|
|
490
|
+
const maximumProofBytes = boundedLimit(value.maximumProofBytes, DEFAULT_MAXIMUM_PROOF_BYTES, "maximumProofBytes", 64, 1_048_576);
|
|
491
|
+
const head = normalizeHead(value.head);
|
|
492
|
+
const activeProposal = value.activeProposal === null
|
|
493
|
+
? null
|
|
494
|
+
: normalizeSignedRecord(value.activeProposal, "proposal", maximumProofBytes);
|
|
495
|
+
const endorsements = signedRecords(value.endorsements, "endorsement", maximumProofBytes);
|
|
496
|
+
const certificate = value.certificate === null ? null : normalizeCertificate(value.certificate);
|
|
497
|
+
const transitions = transitionEvidence(value.transitions, maximumTransitions, maximumProofBytes);
|
|
498
|
+
const parts = {
|
|
499
|
+
revision: positive(value.revision, "revision"),
|
|
500
|
+
head,
|
|
501
|
+
activeProposal,
|
|
502
|
+
endorsements,
|
|
503
|
+
certificate,
|
|
504
|
+
transitions,
|
|
505
|
+
lastLogicalTimeMs: safeInteger(value.lastLogicalTimeMs, "lastLogicalTimeMs", head.activatedAtLogicalMs),
|
|
506
|
+
bindings: {
|
|
507
|
+
verifierId: identifier(value.verifierId, "verifierId"),
|
|
508
|
+
verifierVersion: positive(value.verifierVersion, "verifierVersion"),
|
|
509
|
+
verifierImplementationId: identifier(value.verifierImplementationId, "verifierImplementationId"),
|
|
510
|
+
eligibilityId: identifier(value.eligibilityId, "eligibilityId"),
|
|
511
|
+
eligibilityVersion: positive(value.eligibilityVersion, "eligibilityVersion"),
|
|
512
|
+
eligibilityImplementationId: identifier(value.eligibilityImplementationId, "eligibilityImplementationId"),
|
|
513
|
+
},
|
|
514
|
+
maximumTransitions,
|
|
515
|
+
maximumProofBytes,
|
|
516
|
+
};
|
|
517
|
+
const rebuilt = buildSnapshot(parts);
|
|
518
|
+
if (identifier(value.scopeKey, "scopeKey") !== rebuilt.scopeKey ||
|
|
519
|
+
digest(value.snapshotDigest, "snapshotDigest") !== rebuilt.snapshotDigest) {
|
|
520
|
+
invalid("authority snapshot digest or scope binding is invalid");
|
|
521
|
+
}
|
|
522
|
+
assertActiveState(rebuilt);
|
|
523
|
+
assertTransitionChain(rebuilt);
|
|
524
|
+
return rebuilt;
|
|
525
|
+
}
|
|
526
|
+
function assertProposalBinding(current, recordValue, logicalTimeMs) {
|
|
527
|
+
const proposal = recordValue.statement;
|
|
528
|
+
const head = current.head;
|
|
529
|
+
if (proposal.scopeKey !== current.scopeKey ||
|
|
530
|
+
stableJson(proposal.scope) !== stableJson(head.scope) ||
|
|
531
|
+
proposal.previousHeadDigest !== head.headDigest ||
|
|
532
|
+
proposal.previousGeneration !== head.generation ||
|
|
533
|
+
proposal.proposedGeneration !== head.generation + 1 ||
|
|
534
|
+
!sameIdentity(proposal.previousHolder, head.holder) ||
|
|
535
|
+
proposal.proposedAtLogicalMs < current.lastLogicalTimeMs ||
|
|
536
|
+
proposal.proposedAtLogicalMs > logicalTimeMs ||
|
|
537
|
+
logicalTimeMs >= proposal.expiresAtLogicalMs ||
|
|
538
|
+
proposal.expiresAtLogicalMs - proposal.proposedAtLogicalMs >
|
|
539
|
+
head.policy.maximumProposalLifetimeMs ||
|
|
540
|
+
proposal.expiresAtLogicalMs > head.policy.validUntilLogicalMs ||
|
|
541
|
+
proposal.successorValidUntilLogicalMs >
|
|
542
|
+
proposal.successorPolicy.validUntilLogicalMs) {
|
|
543
|
+
invalid("authority proposal is not bound to the current head");
|
|
544
|
+
}
|
|
545
|
+
if (proposal.mode === "coordinated_transfer") {
|
|
546
|
+
if (!sameIdentity(recordValue.signer, head.holder) ||
|
|
547
|
+
proposal.proposedAtLogicalMs >= head.holderValidUntilLogicalMs ||
|
|
548
|
+
proposal.notBeforeLogicalMs !== proposal.proposedAtLogicalMs) {
|
|
549
|
+
rejected("coordinated authority transfer lacks current holder authority");
|
|
550
|
+
}
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (!sameIdentity(recordValue.signer, proposal.successor) ||
|
|
554
|
+
proposal.notBeforeLogicalMs <
|
|
555
|
+
proposal.proposedAtLogicalMs + head.policy.recoveryDelayMs ||
|
|
556
|
+
stableJson(proposal.successorPolicy) !== stableJson(head.policy) ||
|
|
557
|
+
proposal.successorValidUntilLogicalMs > head.policy.validUntilLogicalMs) {
|
|
558
|
+
rejected("authority recovery proposal is not witness-policy bound");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function assertEndorsementBinding(current, recordValue, logicalTimeMs) {
|
|
562
|
+
const proposal = current.activeProposal?.statement;
|
|
563
|
+
const endorsement = recordValue.statement;
|
|
564
|
+
if (!proposal ||
|
|
565
|
+
proposal.mode !== "witness_recovery" ||
|
|
566
|
+
current.certificate !== null ||
|
|
567
|
+
endorsement.scopeKey !== current.scopeKey ||
|
|
568
|
+
endorsement.proposalId !== proposal.proposalId ||
|
|
569
|
+
endorsement.proposalDigest !== proposal.proposalDigest ||
|
|
570
|
+
endorsement.witnessPeerId !== recordValue.signer.peerId ||
|
|
571
|
+
!current.head.policy.witnessPeerIds.includes(endorsement.witnessPeerId) ||
|
|
572
|
+
endorsement.endorsedAtLogicalMs < proposal.proposedAtLogicalMs ||
|
|
573
|
+
endorsement.endorsedAtLogicalMs > logicalTimeMs ||
|
|
574
|
+
endorsement.endorsedAtLogicalMs >= proposal.expiresAtLogicalMs ||
|
|
575
|
+
endorsement.observedUnavailableSinceLogicalMs >
|
|
576
|
+
proposal.proposedAtLogicalMs ||
|
|
577
|
+
endorsement.endorsedAtLogicalMs <
|
|
578
|
+
endorsement.observedUnavailableSinceLogicalMs +
|
|
579
|
+
current.head.policy.recoveryDelayMs) {
|
|
580
|
+
rejected("authority recovery endorsement is not current or eligible");
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function assertAcceptanceBinding(current, proposal, certificate, recordValue, logicalTimeMs) {
|
|
584
|
+
const acceptance = recordValue.statement;
|
|
585
|
+
if (acceptance.scopeKey !== current.scopeKey ||
|
|
586
|
+
acceptance.proposalId !== proposal.proposalId ||
|
|
587
|
+
acceptance.proposalDigest !== proposal.proposalDigest ||
|
|
588
|
+
acceptance.certificateId !== certificate.certificateId ||
|
|
589
|
+
acceptance.certificateDigest !== certificate.certificateDigest ||
|
|
590
|
+
!sameIdentity(acceptance.successor, proposal.successor) ||
|
|
591
|
+
!sameIdentity(recordValue.signer, proposal.successor) ||
|
|
592
|
+
acceptance.acceptedAtLogicalMs < certificate.issuedAtLogicalMs ||
|
|
593
|
+
acceptance.acceptedAtLogicalMs > logicalTimeMs ||
|
|
594
|
+
acceptance.acceptedAtLogicalMs >= certificate.expiresAtLogicalMs ||
|
|
595
|
+
acceptance.acceptedAtLogicalMs >= proposal.successorValidUntilLogicalMs ||
|
|
596
|
+
current.head.headDigest !== certificate.previousHeadDigest ||
|
|
597
|
+
current.head.generation + 1 !== certificate.proposedGeneration) {
|
|
598
|
+
rejected("authority acceptance is not bound to the current certificate");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function createCertificate(current, proposal, issuedAtLogicalMs) {
|
|
602
|
+
const endorsements = proposal.mode === "witness_recovery" ? current.endorsements : [];
|
|
603
|
+
const certificateId = certificateIdFor({
|
|
604
|
+
scopeKey: current.scopeKey,
|
|
605
|
+
proposalDigest: proposal.proposalDigest,
|
|
606
|
+
previousHeadDigest: current.head.headDigest,
|
|
607
|
+
proposedGeneration: proposal.proposedGeneration,
|
|
608
|
+
issuedAtLogicalMs,
|
|
609
|
+
});
|
|
610
|
+
const body = {
|
|
611
|
+
schemaVersion: 1,
|
|
612
|
+
certificateId,
|
|
613
|
+
scopeKey: current.scopeKey,
|
|
614
|
+
proposalId: proposal.proposalId,
|
|
615
|
+
proposalDigest: proposal.proposalDigest,
|
|
616
|
+
previousHeadDigest: current.head.headDigest,
|
|
617
|
+
proposedGeneration: proposal.proposedGeneration,
|
|
618
|
+
mode: proposal.mode,
|
|
619
|
+
endorsementDigests: endorsements.map(({ statement }) => statement.endorsementDigest),
|
|
620
|
+
witnessPeerIds: endorsements.map(({ statement }) => statement.witnessPeerId),
|
|
621
|
+
issuedAtLogicalMs,
|
|
622
|
+
expiresAtLogicalMs: proposal.expiresAtLogicalMs,
|
|
623
|
+
};
|
|
624
|
+
return freeze({
|
|
625
|
+
...body,
|
|
626
|
+
certificateDigest: digestOf("authority-certificate", body),
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
function transitionedHead(previous, proposal, certificate, acceptance) {
|
|
630
|
+
const fencingToken = `fence:${sha256Base64Url(utf8.encode(stableJson({
|
|
631
|
+
certificateDigest: certificate.certificateDigest,
|
|
632
|
+
generation: proposal.proposedGeneration,
|
|
633
|
+
successor: proposal.successor,
|
|
634
|
+
})))}`;
|
|
635
|
+
return createMeshAuthorityHeadV1({
|
|
636
|
+
scope: previous.scope,
|
|
637
|
+
generation: proposal.proposedGeneration,
|
|
638
|
+
holder: proposal.successor,
|
|
639
|
+
activatedBy: "certified_transition",
|
|
640
|
+
activationId: acceptance.acceptanceId,
|
|
641
|
+
predecessorHeadDigest: previous.headDigest,
|
|
642
|
+
fencingToken,
|
|
643
|
+
activatedAtLogicalMs: acceptance.acceptedAtLogicalMs,
|
|
644
|
+
holderValidUntilLogicalMs: proposal.successorValidUntilLogicalMs,
|
|
645
|
+
policy: proposal.successorPolicy,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function buildSnapshot(input) {
|
|
649
|
+
const body = {
|
|
650
|
+
schemaVersion: 1,
|
|
651
|
+
revision: input.revision,
|
|
652
|
+
scopeKey: input.head.scopeKey,
|
|
653
|
+
...input.bindings,
|
|
654
|
+
maximumTransitions: input.maximumTransitions,
|
|
655
|
+
maximumProofBytes: input.maximumProofBytes,
|
|
656
|
+
head: input.head,
|
|
657
|
+
activeProposal: input.activeProposal,
|
|
658
|
+
endorsements: input.endorsements,
|
|
659
|
+
certificate: input.certificate,
|
|
660
|
+
transitions: input.transitions,
|
|
661
|
+
lastLogicalTimeMs: input.lastLogicalTimeMs,
|
|
662
|
+
};
|
|
663
|
+
return freeze({
|
|
664
|
+
...body,
|
|
665
|
+
snapshotDigest: digestOf("authority-snapshot", body),
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
function snapshotParts(snapshot) {
|
|
669
|
+
return {
|
|
670
|
+
head: snapshot.head,
|
|
671
|
+
activeProposal: snapshot.activeProposal,
|
|
672
|
+
endorsements: snapshot.endorsements,
|
|
673
|
+
certificate: snapshot.certificate,
|
|
674
|
+
transitions: snapshot.transitions,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function assertActiveState(snapshot) {
|
|
678
|
+
const proposal = snapshot.activeProposal?.statement;
|
|
679
|
+
if (snapshot.scopeKey !== snapshot.head.scopeKey ||
|
|
680
|
+
(proposal === undefined &&
|
|
681
|
+
(snapshot.endorsements.length !== 0 || snapshot.certificate !== null)) ||
|
|
682
|
+
(proposal !== undefined &&
|
|
683
|
+
(proposal.scopeKey !== snapshot.scopeKey ||
|
|
684
|
+
proposal.previousHeadDigest !== snapshot.head.headDigest ||
|
|
685
|
+
proposal.proposedAtLogicalMs > snapshot.lastLogicalTimeMs)) ||
|
|
686
|
+
(snapshot.certificate !== null &&
|
|
687
|
+
(proposal === undefined ||
|
|
688
|
+
snapshot.certificate.proposalDigest !== proposal.proposalDigest)) ||
|
|
689
|
+
new Set(snapshot.endorsements.map(({ statement }) => statement.witnessPeerId)).size !== snapshot.endorsements.length) {
|
|
690
|
+
invalid("authority snapshot active transition is inconsistent");
|
|
691
|
+
}
|
|
692
|
+
if (!snapshot.activeProposal)
|
|
693
|
+
return;
|
|
694
|
+
const atProposal = {
|
|
695
|
+
...snapshot,
|
|
696
|
+
lastLogicalTimeMs: snapshot.activeProposal.statement.proposedAtLogicalMs,
|
|
697
|
+
};
|
|
698
|
+
assertProposalBinding(atProposal, snapshot.activeProposal, snapshot.activeProposal.statement.proposedAtLogicalMs);
|
|
699
|
+
const beforeCertificate = {
|
|
700
|
+
...snapshot,
|
|
701
|
+
certificate: null,
|
|
702
|
+
};
|
|
703
|
+
for (const endorsement of snapshot.endorsements) {
|
|
704
|
+
assertEndorsementBinding(beforeCertificate, endorsement, snapshot.lastLogicalTimeMs);
|
|
705
|
+
}
|
|
706
|
+
if (!snapshot.certificate)
|
|
707
|
+
return;
|
|
708
|
+
const activeProposalStatement = snapshot.activeProposal.statement;
|
|
709
|
+
if (snapshot.certificate.issuedAtLogicalMs > snapshot.lastLogicalTimeMs ||
|
|
710
|
+
(activeProposalStatement.mode === "witness_recovery" &&
|
|
711
|
+
snapshot.endorsements.length < snapshot.head.policy.witnessThreshold)) {
|
|
712
|
+
invalid("active authority certificate is not ready");
|
|
713
|
+
}
|
|
714
|
+
const expectedCertificate = createCertificate(snapshot, activeProposalStatement, snapshot.certificate.issuedAtLogicalMs);
|
|
715
|
+
if (stableJson(expectedCertificate) !== stableJson(snapshot.certificate)) {
|
|
716
|
+
invalid("active authority certificate does not match its evidence");
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function assertTransitionChain(snapshot) {
|
|
720
|
+
let resulting;
|
|
721
|
+
for (const evidence of snapshot.transitions) {
|
|
722
|
+
if (resulting !== undefined &&
|
|
723
|
+
evidence.previousHead.headDigest !== resulting.headDigest) {
|
|
724
|
+
invalid("authority transition history is not contiguous");
|
|
725
|
+
}
|
|
726
|
+
resulting = validateTransitionBinding(evidence);
|
|
727
|
+
}
|
|
728
|
+
if (resulting !== undefined &&
|
|
729
|
+
resulting.headDigest !== snapshot.head.headDigest) {
|
|
730
|
+
invalid("authority transition history does not reach the current head");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function validateTransitionBinding(evidence) {
|
|
734
|
+
const previous = evidence.previousHead;
|
|
735
|
+
const proposalRecord = evidence.proposal;
|
|
736
|
+
const proposal = proposalRecord.statement;
|
|
737
|
+
const certificate = evidence.certificate;
|
|
738
|
+
const acceptanceRecord = evidence.acceptance;
|
|
739
|
+
const acceptance = acceptanceRecord.statement;
|
|
740
|
+
const endorsementDigests = evidence.endorsements.map(({ statement }) => statement.endorsementDigest);
|
|
741
|
+
const witnessPeerIds = evidence.endorsements.map(({ statement }) => statement.witnessPeerId);
|
|
742
|
+
if (proposal.scopeKey !== previous.scopeKey ||
|
|
743
|
+
stableJson(proposal.scope) !== stableJson(previous.scope) ||
|
|
744
|
+
proposal.previousHeadDigest !== previous.headDigest ||
|
|
745
|
+
proposal.previousGeneration !== previous.generation ||
|
|
746
|
+
proposal.proposedGeneration !== previous.generation + 1 ||
|
|
747
|
+
!sameIdentity(proposal.previousHolder, previous.holder) ||
|
|
748
|
+
proposal.proposedAtLogicalMs < previous.activatedAtLogicalMs ||
|
|
749
|
+
proposal.expiresAtLogicalMs - proposal.proposedAtLogicalMs >
|
|
750
|
+
previous.policy.maximumProposalLifetimeMs ||
|
|
751
|
+
proposal.expiresAtLogicalMs > previous.policy.validUntilLogicalMs ||
|
|
752
|
+
proposal.successorValidUntilLogicalMs >
|
|
753
|
+
proposal.successorPolicy.validUntilLogicalMs ||
|
|
754
|
+
certificate.scopeKey !== previous.scopeKey ||
|
|
755
|
+
certificate.proposalId !== proposal.proposalId ||
|
|
756
|
+
certificate.proposalDigest !== proposal.proposalDigest ||
|
|
757
|
+
certificate.previousHeadDigest !== previous.headDigest ||
|
|
758
|
+
certificate.proposedGeneration !== proposal.proposedGeneration ||
|
|
759
|
+
certificate.mode !== proposal.mode ||
|
|
760
|
+
certificate.issuedAtLogicalMs < proposal.notBeforeLogicalMs ||
|
|
761
|
+
certificate.expiresAtLogicalMs !== proposal.expiresAtLogicalMs ||
|
|
762
|
+
stableJson(certificate.endorsementDigests) !==
|
|
763
|
+
stableJson(endorsementDigests) ||
|
|
764
|
+
stableJson(certificate.witnessPeerIds) !== stableJson(witnessPeerIds) ||
|
|
765
|
+
acceptance.scopeKey !== previous.scopeKey ||
|
|
766
|
+
acceptance.proposalId !== proposal.proposalId ||
|
|
767
|
+
acceptance.proposalDigest !== proposal.proposalDigest ||
|
|
768
|
+
acceptance.certificateId !== certificate.certificateId ||
|
|
769
|
+
acceptance.certificateDigest !== certificate.certificateDigest ||
|
|
770
|
+
!sameIdentity(acceptance.successor, proposal.successor) ||
|
|
771
|
+
!sameIdentity(acceptanceRecord.signer, proposal.successor) ||
|
|
772
|
+
acceptance.acceptedAtLogicalMs < certificate.issuedAtLogicalMs ||
|
|
773
|
+
acceptance.acceptedAtLogicalMs >= certificate.expiresAtLogicalMs ||
|
|
774
|
+
acceptance.acceptedAtLogicalMs >= proposal.successorValidUntilLogicalMs) {
|
|
775
|
+
invalid("authority transition evidence is not fully bound");
|
|
776
|
+
}
|
|
777
|
+
if (proposal.mode === "coordinated_transfer") {
|
|
778
|
+
if (!sameIdentity(proposalRecord.signer, previous.holder) ||
|
|
779
|
+
evidence.endorsements.length !== 0 ||
|
|
780
|
+
proposal.proposedAtLogicalMs >= previous.holderValidUntilLogicalMs ||
|
|
781
|
+
proposal.notBeforeLogicalMs !== proposal.proposedAtLogicalMs) {
|
|
782
|
+
invalid("coordinated authority transition evidence is invalid");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
const witnesses = new Set();
|
|
787
|
+
for (const recordValue of evidence.endorsements) {
|
|
788
|
+
const endorsement = recordValue.statement;
|
|
789
|
+
if (endorsement.scopeKey !== previous.scopeKey ||
|
|
790
|
+
endorsement.proposalId !== proposal.proposalId ||
|
|
791
|
+
endorsement.proposalDigest !== proposal.proposalDigest ||
|
|
792
|
+
endorsement.witnessPeerId !== recordValue.signer.peerId ||
|
|
793
|
+
!previous.policy.witnessPeerIds.includes(endorsement.witnessPeerId) ||
|
|
794
|
+
witnesses.has(endorsement.witnessPeerId) ||
|
|
795
|
+
endorsement.endorsedAtLogicalMs < proposal.proposedAtLogicalMs ||
|
|
796
|
+
endorsement.endorsedAtLogicalMs >= proposal.expiresAtLogicalMs ||
|
|
797
|
+
endorsement.observedUnavailableSinceLogicalMs >
|
|
798
|
+
proposal.proposedAtLogicalMs ||
|
|
799
|
+
endorsement.endorsedAtLogicalMs <
|
|
800
|
+
endorsement.observedUnavailableSinceLogicalMs +
|
|
801
|
+
previous.policy.recoveryDelayMs) {
|
|
802
|
+
invalid("authority witness evidence is invalid");
|
|
803
|
+
}
|
|
804
|
+
witnesses.add(endorsement.witnessPeerId);
|
|
805
|
+
}
|
|
806
|
+
if (!sameIdentity(proposalRecord.signer, proposal.successor) ||
|
|
807
|
+
witnesses.size < previous.policy.witnessThreshold ||
|
|
808
|
+
stableJson(proposal.successorPolicy) !== stableJson(previous.policy) ||
|
|
809
|
+
proposal.notBeforeLogicalMs <
|
|
810
|
+
proposal.proposedAtLogicalMs + previous.policy.recoveryDelayMs ||
|
|
811
|
+
proposal.successorValidUntilLogicalMs >
|
|
812
|
+
previous.policy.validUntilLogicalMs) {
|
|
813
|
+
invalid("authority recovery evidence does not meet its policy");
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const resulting = transitionedHead(previous, proposal, certificate, acceptance);
|
|
817
|
+
if (resulting.headDigest !== evidence.resultingHeadDigest) {
|
|
818
|
+
invalid("authority transition resulting head digest is invalid");
|
|
819
|
+
}
|
|
820
|
+
return resulting;
|
|
821
|
+
}
|
|
822
|
+
function normalizeHead(value) {
|
|
823
|
+
record(value, "authority head");
|
|
824
|
+
exactKeys(value, [
|
|
825
|
+
"activatedAtLogicalMs",
|
|
826
|
+
"activatedBy",
|
|
827
|
+
"activationId",
|
|
828
|
+
"fencingToken",
|
|
829
|
+
"generation",
|
|
830
|
+
"headDigest",
|
|
831
|
+
"holder",
|
|
832
|
+
"holderValidUntilLogicalMs",
|
|
833
|
+
"policy",
|
|
834
|
+
"predecessorHeadDigest",
|
|
835
|
+
"schemaVersion",
|
|
836
|
+
"scope",
|
|
837
|
+
"scopeKey",
|
|
838
|
+
]);
|
|
839
|
+
if (value.schemaVersion !== 1)
|
|
840
|
+
invalid("authority head schemaVersion is invalid");
|
|
841
|
+
const rebuilt = createMeshAuthorityHeadV1({
|
|
842
|
+
scope: value.scope,
|
|
843
|
+
generation: value.generation,
|
|
844
|
+
holder: value.holder,
|
|
845
|
+
activatedBy: value.activatedBy,
|
|
846
|
+
activationId: value.activationId,
|
|
847
|
+
predecessorHeadDigest: value.predecessorHeadDigest,
|
|
848
|
+
fencingToken: value.fencingToken,
|
|
849
|
+
activatedAtLogicalMs: value.activatedAtLogicalMs,
|
|
850
|
+
holderValidUntilLogicalMs: value.holderValidUntilLogicalMs,
|
|
851
|
+
policy: value.policy,
|
|
852
|
+
});
|
|
853
|
+
if (value.scopeKey !== rebuilt.scopeKey ||
|
|
854
|
+
value.headDigest !== rebuilt.headDigest) {
|
|
855
|
+
invalid("authority head digest is invalid");
|
|
856
|
+
}
|
|
857
|
+
return rebuilt;
|
|
858
|
+
}
|
|
859
|
+
function normalizePolicy(value) {
|
|
860
|
+
record(value, "continuity policy");
|
|
861
|
+
exactKeys(value, [
|
|
862
|
+
"maximumProposalLifetimeMs",
|
|
863
|
+
"policyDigest",
|
|
864
|
+
"policyId",
|
|
865
|
+
"policyRevision",
|
|
866
|
+
"recoveryDelayMs",
|
|
867
|
+
"schemaVersion",
|
|
868
|
+
"validUntilLogicalMs",
|
|
869
|
+
"witnessPeerIds",
|
|
870
|
+
"witnessThreshold",
|
|
871
|
+
]);
|
|
872
|
+
if (value.schemaVersion !== 1)
|
|
873
|
+
invalid("continuity policy schemaVersion is invalid");
|
|
874
|
+
const rebuilt = createMeshAuthorityContinuityPolicyV1({
|
|
875
|
+
policyId: value.policyId,
|
|
876
|
+
policyRevision: value.policyRevision,
|
|
877
|
+
witnessPeerIds: value.witnessPeerIds,
|
|
878
|
+
witnessThreshold: value.witnessThreshold,
|
|
879
|
+
recoveryDelayMs: value.recoveryDelayMs,
|
|
880
|
+
maximumProposalLifetimeMs: value.maximumProposalLifetimeMs,
|
|
881
|
+
validUntilLogicalMs: value.validUntilLogicalMs,
|
|
882
|
+
});
|
|
883
|
+
if (value.policyDigest !== rebuilt.policyDigest) {
|
|
884
|
+
invalid("continuity policy digest is invalid");
|
|
885
|
+
}
|
|
886
|
+
return rebuilt;
|
|
887
|
+
}
|
|
888
|
+
function normalizeScope(value) {
|
|
889
|
+
record(value, "authority scope");
|
|
890
|
+
exactKeys(value, [
|
|
891
|
+
"kind",
|
|
892
|
+
"meshId",
|
|
893
|
+
"objectiveId",
|
|
894
|
+
"schemaVersion",
|
|
895
|
+
"tenantId",
|
|
896
|
+
"workItemId",
|
|
897
|
+
]);
|
|
898
|
+
if (value.schemaVersion !== 1)
|
|
899
|
+
invalid("authority scope schemaVersion is invalid");
|
|
900
|
+
const kind = oneOf(value.kind, ["objective_issuer", "work_owner"], "scope.kind");
|
|
901
|
+
const workItemId = value.workItemId === null
|
|
902
|
+
? null
|
|
903
|
+
: identifier(value.workItemId, "scope.workItemId");
|
|
904
|
+
if ((kind === "objective_issuer" && workItemId !== null) ||
|
|
905
|
+
(kind === "work_owner" && workItemId === null)) {
|
|
906
|
+
invalid("authority scope workItemId is inconsistent with its kind");
|
|
907
|
+
}
|
|
908
|
+
return freeze({
|
|
909
|
+
schemaVersion: 1,
|
|
910
|
+
kind,
|
|
911
|
+
tenantId: identifier(value.tenantId, "scope.tenantId"),
|
|
912
|
+
meshId: identifier(value.meshId, "scope.meshId"),
|
|
913
|
+
objectiveId: identifier(value.objectiveId, "scope.objectiveId"),
|
|
914
|
+
workItemId,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
function normalizeIdentity(value, label) {
|
|
918
|
+
record(value, label);
|
|
919
|
+
exactKeys(value, ["instanceId", "keyId", "peerId", "schemaVersion"]);
|
|
920
|
+
if (value.schemaVersion !== 1)
|
|
921
|
+
invalid(`${label}.schemaVersion is invalid`);
|
|
922
|
+
return freeze({
|
|
923
|
+
schemaVersion: 1,
|
|
924
|
+
peerId: identifier(value.peerId, `${label}.peerId`),
|
|
925
|
+
instanceId: identifier(value.instanceId, `${label}.instanceId`),
|
|
926
|
+
keyId: identifier(value.keyId, `${label}.keyId`),
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
function normalizeSignedRecord(value, purpose, maximumProofBytes) {
|
|
930
|
+
record(value, `${purpose} record`);
|
|
931
|
+
exactKeys(value, ["proof", "schemaVersion", "signer", "statement"]);
|
|
932
|
+
if (value.schemaVersion !== 1)
|
|
933
|
+
invalid(`${purpose} record schemaVersion is invalid`);
|
|
934
|
+
const statement = normalizeStatementForPurpose(value.statement, purpose);
|
|
935
|
+
const signer = normalizeIdentity(value.signer, `${purpose}.signer`);
|
|
936
|
+
record(value.proof, `${purpose}.proof`);
|
|
937
|
+
exactKeys(value.proof, ["algorithm", "schemaVersion", "value"]);
|
|
938
|
+
if (value.proof.schemaVersion !== 1)
|
|
939
|
+
invalid(`${purpose} proof schemaVersion is invalid`);
|
|
940
|
+
const proof = freeze({
|
|
941
|
+
schemaVersion: 1,
|
|
942
|
+
algorithm: identifier(value.proof.algorithm, `${purpose}.proof.algorithm`),
|
|
943
|
+
value: boundedText(value.proof.value, `${purpose}.proof.value`, maximumProofBytes),
|
|
944
|
+
});
|
|
945
|
+
return freeze({ schemaVersion: 1, statement, signer, proof });
|
|
946
|
+
}
|
|
947
|
+
function normalizeStatement(value) {
|
|
948
|
+
if (isRecord(value) && "proposalDigest" in value) {
|
|
949
|
+
if ("successorPolicy" in value)
|
|
950
|
+
return normalizeProposal(value);
|
|
951
|
+
if ("certificateDigest" in value)
|
|
952
|
+
return normalizeAcceptance(value);
|
|
953
|
+
return normalizeEndorsement(value);
|
|
954
|
+
}
|
|
955
|
+
invalid("authority signed statement kind is invalid");
|
|
956
|
+
}
|
|
957
|
+
function normalizeStatementForPurpose(value, purpose) {
|
|
958
|
+
if (purpose === "proposal")
|
|
959
|
+
return normalizeProposal(value);
|
|
960
|
+
if (purpose === "endorsement")
|
|
961
|
+
return normalizeEndorsement(value);
|
|
962
|
+
return normalizeAcceptance(value);
|
|
963
|
+
}
|
|
964
|
+
function normalizeProposal(value) {
|
|
965
|
+
record(value, "authority proposal");
|
|
966
|
+
exactKeys(value, [
|
|
967
|
+
"expiresAtLogicalMs",
|
|
968
|
+
"mode",
|
|
969
|
+
"notBeforeLogicalMs",
|
|
970
|
+
"previousGeneration",
|
|
971
|
+
"previousHeadDigest",
|
|
972
|
+
"previousHolder",
|
|
973
|
+
"proposalDigest",
|
|
974
|
+
"proposalId",
|
|
975
|
+
"proposedAtLogicalMs",
|
|
976
|
+
"proposedGeneration",
|
|
977
|
+
"schemaVersion",
|
|
978
|
+
"scope",
|
|
979
|
+
"scopeKey",
|
|
980
|
+
"successor",
|
|
981
|
+
"successorPolicy",
|
|
982
|
+
"successorValidUntilLogicalMs",
|
|
983
|
+
]);
|
|
984
|
+
if (value.schemaVersion !== 1)
|
|
985
|
+
invalid("authority proposal schemaVersion is invalid");
|
|
986
|
+
const rebuilt = createMeshAuthorityProposalV1({
|
|
987
|
+
proposalId: value.proposalId,
|
|
988
|
+
scope: value.scope,
|
|
989
|
+
mode: value.mode,
|
|
990
|
+
previousHeadDigest: value.previousHeadDigest,
|
|
991
|
+
previousGeneration: value.previousGeneration,
|
|
992
|
+
proposedGeneration: value.proposedGeneration,
|
|
993
|
+
previousHolder: value.previousHolder,
|
|
994
|
+
successor: value.successor,
|
|
995
|
+
successorValidUntilLogicalMs: value.successorValidUntilLogicalMs,
|
|
996
|
+
successorPolicy: value.successorPolicy,
|
|
997
|
+
proposedAtLogicalMs: value.proposedAtLogicalMs,
|
|
998
|
+
notBeforeLogicalMs: value.notBeforeLogicalMs,
|
|
999
|
+
expiresAtLogicalMs: value.expiresAtLogicalMs,
|
|
1000
|
+
});
|
|
1001
|
+
if (value.scopeKey !== rebuilt.scopeKey ||
|
|
1002
|
+
value.proposalDigest !== rebuilt.proposalDigest) {
|
|
1003
|
+
invalid("authority proposal digest is invalid");
|
|
1004
|
+
}
|
|
1005
|
+
return rebuilt;
|
|
1006
|
+
}
|
|
1007
|
+
function normalizeEndorsement(value) {
|
|
1008
|
+
record(value, "authority endorsement");
|
|
1009
|
+
exactKeys(value, [
|
|
1010
|
+
"endorsedAtLogicalMs",
|
|
1011
|
+
"endorsementDigest",
|
|
1012
|
+
"endorsementId",
|
|
1013
|
+
"observedUnavailableSinceLogicalMs",
|
|
1014
|
+
"proposalDigest",
|
|
1015
|
+
"proposalId",
|
|
1016
|
+
"schemaVersion",
|
|
1017
|
+
"scopeKey",
|
|
1018
|
+
"witnessPeerId",
|
|
1019
|
+
]);
|
|
1020
|
+
if (value.schemaVersion !== 1)
|
|
1021
|
+
invalid("authority endorsement schemaVersion is invalid");
|
|
1022
|
+
const rebuilt = createMeshAuthorityEndorsementV1({
|
|
1023
|
+
endorsementId: value.endorsementId,
|
|
1024
|
+
scopeKey: value.scopeKey,
|
|
1025
|
+
proposalId: value.proposalId,
|
|
1026
|
+
proposalDigest: value.proposalDigest,
|
|
1027
|
+
witnessPeerId: value.witnessPeerId,
|
|
1028
|
+
observedUnavailableSinceLogicalMs: value.observedUnavailableSinceLogicalMs,
|
|
1029
|
+
endorsedAtLogicalMs: value.endorsedAtLogicalMs,
|
|
1030
|
+
});
|
|
1031
|
+
if (value.endorsementDigest !== rebuilt.endorsementDigest) {
|
|
1032
|
+
invalid("authority endorsement digest is invalid");
|
|
1033
|
+
}
|
|
1034
|
+
return rebuilt;
|
|
1035
|
+
}
|
|
1036
|
+
function normalizeAcceptance(value) {
|
|
1037
|
+
record(value, "authority acceptance");
|
|
1038
|
+
exactKeys(value, [
|
|
1039
|
+
"acceptanceDigest",
|
|
1040
|
+
"acceptanceId",
|
|
1041
|
+
"acceptedAtLogicalMs",
|
|
1042
|
+
"certificateDigest",
|
|
1043
|
+
"certificateId",
|
|
1044
|
+
"proposalDigest",
|
|
1045
|
+
"proposalId",
|
|
1046
|
+
"schemaVersion",
|
|
1047
|
+
"scopeKey",
|
|
1048
|
+
"successor",
|
|
1049
|
+
]);
|
|
1050
|
+
if (value.schemaVersion !== 1)
|
|
1051
|
+
invalid("authority acceptance schemaVersion is invalid");
|
|
1052
|
+
const rebuilt = createMeshAuthorityAcceptanceV1({
|
|
1053
|
+
acceptanceId: value.acceptanceId,
|
|
1054
|
+
scopeKey: value.scopeKey,
|
|
1055
|
+
proposalId: value.proposalId,
|
|
1056
|
+
proposalDigest: value.proposalDigest,
|
|
1057
|
+
certificateId: value.certificateId,
|
|
1058
|
+
certificateDigest: value.certificateDigest,
|
|
1059
|
+
successor: value.successor,
|
|
1060
|
+
acceptedAtLogicalMs: value.acceptedAtLogicalMs,
|
|
1061
|
+
});
|
|
1062
|
+
if (value.acceptanceDigest !== rebuilt.acceptanceDigest) {
|
|
1063
|
+
invalid("authority acceptance digest is invalid");
|
|
1064
|
+
}
|
|
1065
|
+
return rebuilt;
|
|
1066
|
+
}
|
|
1067
|
+
function normalizeCertificate(value) {
|
|
1068
|
+
record(value, "authority certificate");
|
|
1069
|
+
exactKeys(value, [
|
|
1070
|
+
"certificateDigest",
|
|
1071
|
+
"certificateId",
|
|
1072
|
+
"endorsementDigests",
|
|
1073
|
+
"expiresAtLogicalMs",
|
|
1074
|
+
"issuedAtLogicalMs",
|
|
1075
|
+
"mode",
|
|
1076
|
+
"previousHeadDigest",
|
|
1077
|
+
"proposalDigest",
|
|
1078
|
+
"proposalId",
|
|
1079
|
+
"proposedGeneration",
|
|
1080
|
+
"schemaVersion",
|
|
1081
|
+
"scopeKey",
|
|
1082
|
+
"witnessPeerIds",
|
|
1083
|
+
]);
|
|
1084
|
+
if (value.schemaVersion !== 1)
|
|
1085
|
+
invalid("authority certificate schemaVersion is invalid");
|
|
1086
|
+
const body = {
|
|
1087
|
+
schemaVersion: 1,
|
|
1088
|
+
certificateId: identifier(value.certificateId, "certificateId"),
|
|
1089
|
+
scopeKey: identifier(value.scopeKey, "certificate.scopeKey"),
|
|
1090
|
+
proposalId: identifier(value.proposalId, "certificate.proposalId"),
|
|
1091
|
+
proposalDigest: digest(value.proposalDigest, "certificate.proposalDigest"),
|
|
1092
|
+
previousHeadDigest: digest(value.previousHeadDigest, "certificate.previousHeadDigest"),
|
|
1093
|
+
proposedGeneration: positive(value.proposedGeneration, "certificate.proposedGeneration"),
|
|
1094
|
+
mode: oneOf(value.mode, ["coordinated_transfer", "witness_recovery"], "certificate.mode"),
|
|
1095
|
+
endorsementDigests: digests(value.endorsementDigests, "certificate.endorsementDigests"),
|
|
1096
|
+
witnessPeerIds: identifiers(value.witnessPeerIds, "certificate.witnessPeerIds", false),
|
|
1097
|
+
issuedAtLogicalMs: safeInteger(value.issuedAtLogicalMs, "certificate.issuedAtLogicalMs", 0),
|
|
1098
|
+
expiresAtLogicalMs: positive(value.expiresAtLogicalMs, "certificate.expiresAtLogicalMs"),
|
|
1099
|
+
};
|
|
1100
|
+
if (body.certificateId !==
|
|
1101
|
+
certificateIdFor({
|
|
1102
|
+
scopeKey: body.scopeKey,
|
|
1103
|
+
proposalDigest: body.proposalDigest,
|
|
1104
|
+
previousHeadDigest: body.previousHeadDigest,
|
|
1105
|
+
proposedGeneration: body.proposedGeneration,
|
|
1106
|
+
issuedAtLogicalMs: body.issuedAtLogicalMs,
|
|
1107
|
+
}) ||
|
|
1108
|
+
body.endorsementDigests.length !== body.witnessPeerIds.length ||
|
|
1109
|
+
body.expiresAtLogicalMs <= body.issuedAtLogicalMs ||
|
|
1110
|
+
(body.mode === "coordinated_transfer" &&
|
|
1111
|
+
body.endorsementDigests.length !== 0)) {
|
|
1112
|
+
invalid("authority certificate evidence or timing is invalid");
|
|
1113
|
+
}
|
|
1114
|
+
const rebuilt = freeze({
|
|
1115
|
+
...body,
|
|
1116
|
+
certificateDigest: digestOf("authority-certificate", body),
|
|
1117
|
+
});
|
|
1118
|
+
if (value.certificateDigest !== rebuilt.certificateDigest) {
|
|
1119
|
+
invalid("authority certificate digest is invalid");
|
|
1120
|
+
}
|
|
1121
|
+
return rebuilt;
|
|
1122
|
+
}
|
|
1123
|
+
function certificateIdFor(input) {
|
|
1124
|
+
return `certificate:${sha256Base64Url(utf8.encode(stableJson(input)))}`;
|
|
1125
|
+
}
|
|
1126
|
+
function signedRecords(value, purpose, maximumProofBytes) {
|
|
1127
|
+
if (!Array.isArray(value) || value.length > MAXIMUM_ARRAY) {
|
|
1128
|
+
invalid(`${purpose} records are invalid`);
|
|
1129
|
+
}
|
|
1130
|
+
return freeze(value.map((item) => normalizeSignedRecord(item, purpose, maximumProofBytes)));
|
|
1131
|
+
}
|
|
1132
|
+
function transitionEvidence(value, maximumTransitions, maximumProofBytes) {
|
|
1133
|
+
if (!Array.isArray(value) || value.length > maximumTransitions) {
|
|
1134
|
+
invalid("authority transition evidence is invalid");
|
|
1135
|
+
}
|
|
1136
|
+
return freeze(value.map((item, index) => normalizeTransition(item, index, maximumProofBytes)));
|
|
1137
|
+
}
|
|
1138
|
+
function normalizeTransition(value, index, maximumProofBytes) {
|
|
1139
|
+
record(value, `transition[${index}]`);
|
|
1140
|
+
exactKeys(value, [
|
|
1141
|
+
"acceptance",
|
|
1142
|
+
"certificate",
|
|
1143
|
+
"endorsements",
|
|
1144
|
+
"previousHead",
|
|
1145
|
+
"proposal",
|
|
1146
|
+
"resultingHeadDigest",
|
|
1147
|
+
"schemaVersion",
|
|
1148
|
+
]);
|
|
1149
|
+
if (value.schemaVersion !== 1)
|
|
1150
|
+
invalid("transition schemaVersion is invalid");
|
|
1151
|
+
const previousHead = normalizeHead(value.previousHead);
|
|
1152
|
+
const proposal = normalizeSignedRecord(value.proposal, "proposal", maximumProofBytes);
|
|
1153
|
+
const endorsements = signedRecords(value.endorsements, "endorsement", maximumProofBytes);
|
|
1154
|
+
const certificate = normalizeCertificate(value.certificate);
|
|
1155
|
+
const acceptance = normalizeSignedRecord(value.acceptance, "acceptance", maximumProofBytes);
|
|
1156
|
+
const resultingHeadDigest = digest(value.resultingHeadDigest, "resultingHeadDigest");
|
|
1157
|
+
if (proposal.statement.previousHeadDigest !== previousHead.headDigest ||
|
|
1158
|
+
certificate.proposalDigest !== proposal.statement.proposalDigest ||
|
|
1159
|
+
acceptance.statement.certificateDigest !== certificate.certificateDigest) {
|
|
1160
|
+
invalid("authority transition evidence bindings are invalid");
|
|
1161
|
+
}
|
|
1162
|
+
return freeze({
|
|
1163
|
+
schemaVersion: 1,
|
|
1164
|
+
previousHead,
|
|
1165
|
+
proposal,
|
|
1166
|
+
endorsements,
|
|
1167
|
+
certificate,
|
|
1168
|
+
acceptance,
|
|
1169
|
+
resultingHeadDigest,
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
function normalizeCurrentBinding(value) {
|
|
1173
|
+
record(value, "authority current binding");
|
|
1174
|
+
exactKeys(value, [
|
|
1175
|
+
"fencingToken",
|
|
1176
|
+
"generation",
|
|
1177
|
+
"headDigest",
|
|
1178
|
+
"holder",
|
|
1179
|
+
"logicalTimeMs",
|
|
1180
|
+
"schemaVersion",
|
|
1181
|
+
"scopeKey",
|
|
1182
|
+
]);
|
|
1183
|
+
if (value.schemaVersion !== 1)
|
|
1184
|
+
invalid("current binding schemaVersion is invalid");
|
|
1185
|
+
return freeze({
|
|
1186
|
+
schemaVersion: 1,
|
|
1187
|
+
scopeKey: identifier(value.scopeKey, "current.scopeKey"),
|
|
1188
|
+
generation: positive(value.generation, "current.generation"),
|
|
1189
|
+
holder: normalizeIdentity(value.holder, "current.holder"),
|
|
1190
|
+
headDigest: digest(value.headDigest, "current.headDigest"),
|
|
1191
|
+
fencingToken: identifier(value.fencingToken, "current.fencingToken"),
|
|
1192
|
+
logicalTimeMs: safeInteger(value.logicalTimeMs, "current.logicalTimeMs", 0),
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
function exactDecision(value, discriminant) {
|
|
1196
|
+
const keys = Object.keys(value).sort(compare);
|
|
1197
|
+
return (keys.length === 2 &&
|
|
1198
|
+
keys.includes("reasonCode") &&
|
|
1199
|
+
keys.includes(discriminant));
|
|
1200
|
+
}
|
|
1201
|
+
function sameIdentity(left, right) {
|
|
1202
|
+
return (left.peerId === right.peerId &&
|
|
1203
|
+
left.instanceId === right.instanceId &&
|
|
1204
|
+
left.keyId === right.keyId);
|
|
1205
|
+
}
|
|
1206
|
+
function digestOf(domain, value) {
|
|
1207
|
+
return `sha256:${sha256Base64Url(utf8.encode(stableJson({ domain, value })))}`;
|
|
1208
|
+
}
|
|
1209
|
+
function stableJson(value) {
|
|
1210
|
+
if (Array.isArray(value))
|
|
1211
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
1212
|
+
if (isRecord(value)) {
|
|
1213
|
+
return `{${Object.keys(value)
|
|
1214
|
+
.sort(compare)
|
|
1215
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
1216
|
+
.join(",")}}`;
|
|
1217
|
+
}
|
|
1218
|
+
const encoded = JSON.stringify(value);
|
|
1219
|
+
if (encoded === undefined)
|
|
1220
|
+
invalid("value is not canonical JSON");
|
|
1221
|
+
return encoded;
|
|
1222
|
+
}
|
|
1223
|
+
function identifiers(value, label, nonEmpty) {
|
|
1224
|
+
if (!Array.isArray(value) ||
|
|
1225
|
+
value.length > MAXIMUM_ARRAY ||
|
|
1226
|
+
(nonEmpty && value.length === 0)) {
|
|
1227
|
+
invalid(`${label} is invalid`);
|
|
1228
|
+
}
|
|
1229
|
+
const result = value
|
|
1230
|
+
.map((item) => identifier(item, label))
|
|
1231
|
+
.sort(compare);
|
|
1232
|
+
if (new Set(result).size !== result.length)
|
|
1233
|
+
invalid(`${label} has duplicates`);
|
|
1234
|
+
return freeze(result);
|
|
1235
|
+
}
|
|
1236
|
+
function digests(value, label) {
|
|
1237
|
+
if (!Array.isArray(value) || value.length > MAXIMUM_ARRAY) {
|
|
1238
|
+
invalid(`${label} is invalid`);
|
|
1239
|
+
}
|
|
1240
|
+
const result = value.map((item) => digest(item, label));
|
|
1241
|
+
if (new Set(result).size !== result.length)
|
|
1242
|
+
invalid(`${label} has duplicates`);
|
|
1243
|
+
return freeze(result);
|
|
1244
|
+
}
|
|
1245
|
+
function identifier(value, label) {
|
|
1246
|
+
if (typeof value !== "string" ||
|
|
1247
|
+
value.length === 0 ||
|
|
1248
|
+
value.length > 256 ||
|
|
1249
|
+
!IDENTIFIER.test(value)) {
|
|
1250
|
+
invalid(`${label} is invalid`);
|
|
1251
|
+
}
|
|
1252
|
+
return value;
|
|
1253
|
+
}
|
|
1254
|
+
function digest(value, label) {
|
|
1255
|
+
if (typeof value !== "string" || !DIGEST.test(value)) {
|
|
1256
|
+
invalid(`${label} is invalid`);
|
|
1257
|
+
}
|
|
1258
|
+
return value;
|
|
1259
|
+
}
|
|
1260
|
+
function boundedText(value, label, maximum) {
|
|
1261
|
+
if (typeof value !== "string" ||
|
|
1262
|
+
value.length === 0 ||
|
|
1263
|
+
utf8.encode(value).byteLength > maximum ||
|
|
1264
|
+
value.includes("\u0000")) {
|
|
1265
|
+
invalid(`${label} is invalid`);
|
|
1266
|
+
}
|
|
1267
|
+
return value;
|
|
1268
|
+
}
|
|
1269
|
+
function positive(value, label) {
|
|
1270
|
+
return safeInteger(value, label, 1);
|
|
1271
|
+
}
|
|
1272
|
+
function safeInteger(value, label, minimum) {
|
|
1273
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
1274
|
+
invalid(`${label} is invalid`);
|
|
1275
|
+
}
|
|
1276
|
+
return value;
|
|
1277
|
+
}
|
|
1278
|
+
function boundedLimit(value, fallback, label, minimum, maximum) {
|
|
1279
|
+
const result = value ?? fallback;
|
|
1280
|
+
if (!Number.isSafeInteger(result) || result < minimum || result > maximum) {
|
|
1281
|
+
invalid(`${label} is invalid`);
|
|
1282
|
+
}
|
|
1283
|
+
return result;
|
|
1284
|
+
}
|
|
1285
|
+
function oneOf(value, allowed, label) {
|
|
1286
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
1287
|
+
invalid(`${label} is invalid`);
|
|
1288
|
+
}
|
|
1289
|
+
return value;
|
|
1290
|
+
}
|
|
1291
|
+
function record(value, label) {
|
|
1292
|
+
if (!isRecord(value))
|
|
1293
|
+
invalid(`${label} must be a plain object`);
|
|
1294
|
+
}
|
|
1295
|
+
function isRecord(value) {
|
|
1296
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1297
|
+
return false;
|
|
1298
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1299
|
+
return prototype === Object.prototype || prototype === null;
|
|
1300
|
+
}
|
|
1301
|
+
function exactKeys(value, keys) {
|
|
1302
|
+
const actual = Object.keys(value).sort(compare);
|
|
1303
|
+
const expected = [...keys].sort(compare);
|
|
1304
|
+
if (actual.length !== expected.length ||
|
|
1305
|
+
actual.some((key, index) => key !== expected[index])) {
|
|
1306
|
+
invalid("object fields are invalid");
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
function compare(left, right) {
|
|
1310
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1311
|
+
}
|
|
1312
|
+
function freeze(value) {
|
|
1313
|
+
if (Array.isArray(value)) {
|
|
1314
|
+
for (const item of value)
|
|
1315
|
+
freeze(item);
|
|
1316
|
+
}
|
|
1317
|
+
else if (isRecord(value)) {
|
|
1318
|
+
for (const item of Object.values(value))
|
|
1319
|
+
freeze(item);
|
|
1320
|
+
}
|
|
1321
|
+
return Object.freeze(value);
|
|
1322
|
+
}
|
|
1323
|
+
function invalid(message) {
|
|
1324
|
+
throw new MeshAuthorityContinuityErrorV1("VALIDATION_ERROR", message);
|
|
1325
|
+
}
|
|
1326
|
+
function conflict(message) {
|
|
1327
|
+
throw new MeshAuthorityContinuityErrorV1("STATE_CONFLICT", message);
|
|
1328
|
+
}
|
|
1329
|
+
function rejected(message) {
|
|
1330
|
+
throw new MeshAuthorityContinuityErrorV1("EVIDENCE_REJECTED", message);
|
|
1331
|
+
}
|
|
1332
|
+
function ineligible(message) {
|
|
1333
|
+
throw new MeshAuthorityContinuityErrorV1("SUCCESSOR_INELIGIBLE", message);
|
|
1334
|
+
}
|
|
1335
|
+
function notReady(message) {
|
|
1336
|
+
throw new MeshAuthorityContinuityErrorV1("TRANSITION_NOT_READY", message);
|
|
1337
|
+
}
|
|
1338
|
+
//# sourceMappingURL=authority-continuity.js.map
|