pactium 0.2.0
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/LICENSE +674 -0
- package/README.md +92 -0
- package/README.zh-CN.md +90 -0
- package/SECURITY.md +7 -0
- package/bin/pactium.mjs +121 -0
- package/docs/LICOLITE-ASPECT.md +57 -0
- package/docs/README.md +13 -0
- package/docs/TERM.md +289 -0
- package/docs/architecture/ARCHITECTURE.md +62 -0
- package/docs/protocols/PROFILE.md +124 -0
- package/docs/protocols/PROTOCOLS.md +62 -0
- package/examples/record-operation.mjs +26 -0
- package/package.json +69 -0
- package/src/README.md +13 -0
- package/src/aspects/licolite/aspect.js +278 -0
- package/src/aspects/licolite/constants.js +13 -0
- package/src/aspects/licolite/evidence.js +47 -0
- package/src/aspects/licolite/index.d.ts +51 -0
- package/src/aspects/licolite/index.js +19 -0
- package/src/aspects/licolite/signing.js +78 -0
- package/src/canonical/value.js +40 -0
- package/src/core/append-condition.js +102 -0
- package/src/core/pactium-core.js +1073 -0
- package/src/core/tracking-cursor.js +68 -0
- package/src/http.js +99 -0
- package/src/index-engine/snapshot-merkle-index.js +994 -0
- package/src/index.d.ts +244 -0
- package/src/index.js +73 -0
- package/src/ledger/signed-head.js +204 -0
- package/src/ledger/transparency-log.js +702 -0
- package/src/maintenance/task-engine.js +36 -0
- package/src/proof/bundle-format.js +265 -0
- package/src/proof/bundle.js +77 -0
- package/src/proof/envelope.js +548 -0
- package/src/proof/registry.js +18 -0
- package/src/protocol/constants.js +69 -0
- package/src/protocol/hashing.js +47 -0
- package/src/quality/profile-runner.js +291 -0
- package/src/repair/planner.js +62 -0
- package/src/shared/records.js +32 -0
- package/src/storage/local-json-storage-port.js +360 -0
- package/src/verification/failure.js +31 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { PACTIUM_PROTOCOL } from "../protocol/constants.js";
|
|
2
|
+
import { canonicalDecode, normalizeCanonicalValue } from "../canonical/value.js";
|
|
3
|
+
import { cidForBytes, createId, hashBytes, protocolHash } from "../protocol/hashing.js";
|
|
4
|
+
import { verifyLedgerConsistencyProof, verifyLedgerInclusionProof } from "../ledger/transparency-log.js";
|
|
5
|
+
import { verifyLedgerHeadSignature } from "../ledger/signed-head.js";
|
|
6
|
+
import { asArray, asRecord } from "../shared/records.js";
|
|
7
|
+
import { createVerificationFailure } from "../verification/failure.js";
|
|
8
|
+
import { createIndexedBundleResolver } from "./bundle-format.js";
|
|
9
|
+
import { createDefaultProofVerifierRegistry } from "./registry.js";
|
|
10
|
+
|
|
11
|
+
const CORE_CRITICAL_EXTENSIONS = new Set([]);
|
|
12
|
+
|
|
13
|
+
export async function createProofRef(storage, name, value, refs = []) {
|
|
14
|
+
const block = await storage.putBlock(value, { kind: `proof-material:${name}`, refs });
|
|
15
|
+
return {
|
|
16
|
+
name,
|
|
17
|
+
cid: block.cid,
|
|
18
|
+
payloadHash: block.payloadHash,
|
|
19
|
+
byteLength: block.byteLength
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extensionSigningPayload(envelope) {
|
|
24
|
+
return normalizeCanonicalValue({
|
|
25
|
+
...envelope,
|
|
26
|
+
envelopeId: undefined,
|
|
27
|
+
replayed: false,
|
|
28
|
+
extensions: asArray(envelope.extensions).filter((extension) => extension.name !== "licolite.signature")
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function envelopeIdentityPayload(envelope) {
|
|
33
|
+
return normalizeCanonicalValue({
|
|
34
|
+
...envelope,
|
|
35
|
+
replayed: false,
|
|
36
|
+
envelopeId: undefined
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function envelopeSigningHash(envelope) {
|
|
41
|
+
return protocolHash("proof.envelope.signing", extensionSigningPayload(envelope));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function finalizeEnvelope(envelope) {
|
|
45
|
+
const identity = envelopeIdentityPayload(envelope);
|
|
46
|
+
return {
|
|
47
|
+
...envelope,
|
|
48
|
+
envelopeId: createId("proof_envelope", identity)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function materializeExtension(storage, extension) {
|
|
53
|
+
if (!extension) return null;
|
|
54
|
+
if (extension.valueRef && extension.valueHash) {
|
|
55
|
+
return {
|
|
56
|
+
protocol: PACTIUM_PROTOCOL,
|
|
57
|
+
name: String(extension.name || ""),
|
|
58
|
+
critical: extension.critical === true,
|
|
59
|
+
valueRef: String(extension.valueRef),
|
|
60
|
+
valueHash: String(extension.valueHash),
|
|
61
|
+
metadata: normalizeCanonicalValue(asRecord(extension.metadata))
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const block = await storage.putBlock(extension.value ?? {}, {
|
|
65
|
+
kind: `proof-extension:${extension.name || "extension"}`,
|
|
66
|
+
refs: [
|
|
67
|
+
...asArray(extension.refs),
|
|
68
|
+
extension.value?.evidenceRef || ""
|
|
69
|
+
].filter(Boolean)
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
protocol: PACTIUM_PROTOCOL,
|
|
73
|
+
name: String(extension.name || ""),
|
|
74
|
+
critical: extension.critical === true,
|
|
75
|
+
valueRef: block.cid,
|
|
76
|
+
valueHash: block.payloadHash,
|
|
77
|
+
metadata: normalizeCanonicalValue(asRecord(extension.metadata))
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function resolveBlock({ cid, storage, bundleMap }) {
|
|
82
|
+
if (bundleMap?.has(cid)) {
|
|
83
|
+
const record = await bundleMap.get(cid);
|
|
84
|
+
if (!record) return null;
|
|
85
|
+
const bytes = Buffer.from(String(record.payloadBase64 || ""), "base64");
|
|
86
|
+
const payloadHash = `sha256:${hashBytes(bytes)}`;
|
|
87
|
+
if (payloadHash !== record.payloadHash || cidForBytes(bytes) !== record.cid) {
|
|
88
|
+
throw new Error(`Proof bundle block integrity failure for ${cid}`);
|
|
89
|
+
}
|
|
90
|
+
return { ...record, bytes };
|
|
91
|
+
}
|
|
92
|
+
return storage ? storage.getBlock(cid) : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function decodeBlockValue(block) {
|
|
96
|
+
if (!block) return null;
|
|
97
|
+
if (block.codec === "raw") return block.bytes;
|
|
98
|
+
return canonicalDecode(block.bytes);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function bundleBlockMap(bundle) {
|
|
102
|
+
if (!bundle) return null;
|
|
103
|
+
return createIndexedBundleResolver(bundle);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function proofIsCritical(proof) {
|
|
107
|
+
return asRecord(proof).critical !== false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function verifyEmbeddedProofs({
|
|
111
|
+
proofMaterial,
|
|
112
|
+
registry,
|
|
113
|
+
requireAllProofs,
|
|
114
|
+
failures,
|
|
115
|
+
checked
|
|
116
|
+
}) {
|
|
117
|
+
async function visit(value, path) {
|
|
118
|
+
if (Array.isArray(value)) {
|
|
119
|
+
for (const [index, item] of value.entries()) await visit(item, `${path}[${index}]`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!value || typeof value !== "object") return;
|
|
123
|
+
if (value.proofType) {
|
|
124
|
+
const proofType = String(value.proofType);
|
|
125
|
+
const verifier = registry.get(proofType);
|
|
126
|
+
if (!verifier) {
|
|
127
|
+
if (requireAllProofs || proofIsCritical(value)) {
|
|
128
|
+
failures.push(createVerificationFailure({
|
|
129
|
+
layer: "proof-registry",
|
|
130
|
+
code: "missing_proof_verifier",
|
|
131
|
+
message: `No verifier is registered for proof type ${proofType}.`,
|
|
132
|
+
evidenceRef: path,
|
|
133
|
+
repairable: true,
|
|
134
|
+
details: { proofType, path }
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
try {
|
|
139
|
+
const result = await verifier(value, {
|
|
140
|
+
proofMaterial,
|
|
141
|
+
head: proofMaterial?.ledger?.head,
|
|
142
|
+
oldHead: proofMaterial?.ledger?.previousHead,
|
|
143
|
+
newHead: proofMaterial?.ledger?.head
|
|
144
|
+
});
|
|
145
|
+
const ok = typeof result === "boolean" ? result : result?.ok === true;
|
|
146
|
+
if (!ok) {
|
|
147
|
+
failures.push(createVerificationFailure({
|
|
148
|
+
layer: "proof-registry",
|
|
149
|
+
code: "bad_embedded_proof",
|
|
150
|
+
message: `Embedded proof ${proofType} does not verify.`,
|
|
151
|
+
evidenceRef: path,
|
|
152
|
+
details: { proofType, path }
|
|
153
|
+
}));
|
|
154
|
+
} else {
|
|
155
|
+
checked.push(path);
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
failures.push(createVerificationFailure({
|
|
159
|
+
layer: "proof-registry",
|
|
160
|
+
code: "proof_verifier_threw",
|
|
161
|
+
message: error instanceof Error ? error.message : `Verifier for ${proofType} threw.`,
|
|
162
|
+
evidenceRef: path,
|
|
163
|
+
details: { proofType, path }
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
169
|
+
await visit(nested, path ? `${path}.${key}` : key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await visit(proofMaterial?.proofs || {}, "proofs");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function factRefBindings(envelope, proofMaterial) {
|
|
176
|
+
const proof = proofMaterial?.ledger?.inclusionProof || {};
|
|
177
|
+
const leaf = proof.leaf || {};
|
|
178
|
+
const leafIndex = Number(leaf.index ?? proof.index);
|
|
179
|
+
const leafHash = String(proof.leafHash || "");
|
|
180
|
+
const expectedEventId = Number.isFinite(leafIndex) && leafHash
|
|
181
|
+
? createId("ledger_event", { index: leafIndex, leafHash })
|
|
182
|
+
: "";
|
|
183
|
+
return {
|
|
184
|
+
ok: Boolean(expectedEventId) &&
|
|
185
|
+
envelope.factRef?.ledgerEventId === expectedEventId &&
|
|
186
|
+
Number(envelope.factRef?.ledgerIndex) === leafIndex &&
|
|
187
|
+
envelope.factRef?.factCid === leaf.factCid &&
|
|
188
|
+
envelope.factRef?.factHash === leaf.factHash &&
|
|
189
|
+
envelope.factType === leaf.factType,
|
|
190
|
+
expectedEventId,
|
|
191
|
+
leafIndex
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function verifySemanticBindings({ envelope, proofMaterial, failures }) {
|
|
196
|
+
const proofHead = proofMaterial?.ledger?.head || {};
|
|
197
|
+
if (envelope.ledgerHead && (
|
|
198
|
+
envelope.ledgerHead.rootHash !== proofHead.rootHash ||
|
|
199
|
+
Number(envelope.ledgerHead.size || 0) !== Number(proofHead.size || 0)
|
|
200
|
+
)) {
|
|
201
|
+
failures.push(createVerificationFailure({
|
|
202
|
+
layer: "proof-envelope",
|
|
203
|
+
code: "bad_ledger_head_binding",
|
|
204
|
+
message: "Envelope ledger head does not match the verified proof material head.",
|
|
205
|
+
evidenceRef: envelope.envelopeId
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const factBinding = factRefBindings(envelope, proofMaterial);
|
|
210
|
+
if (!factBinding.ok) {
|
|
211
|
+
failures.push(createVerificationFailure({
|
|
212
|
+
layer: "proof-envelope",
|
|
213
|
+
code: "bad_fact_ref_binding",
|
|
214
|
+
message: "Envelope factRef does not match the verified Ledger inclusion leaf.",
|
|
215
|
+
evidenceRef: envelope.envelopeId,
|
|
216
|
+
details: {
|
|
217
|
+
expectedLedgerEventId: factBinding.expectedEventId,
|
|
218
|
+
expectedLedgerIndex: factBinding.leafIndex
|
|
219
|
+
}
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const proofs = proofMaterial?.proofs || {};
|
|
224
|
+
function badBinding(code, message, details = {}) {
|
|
225
|
+
failures.push(createVerificationFailure({
|
|
226
|
+
layer: "proof-semantics",
|
|
227
|
+
code,
|
|
228
|
+
message,
|
|
229
|
+
evidenceRef: envelope.envelopeId,
|
|
230
|
+
details
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
function expectIndexProof(proof, { root = "", key = "", proofType = "", label = "" } = {}) {
|
|
234
|
+
if (!proof) return;
|
|
235
|
+
if (proofType && proof.proofType !== proofType) {
|
|
236
|
+
badBinding("bad_index_proof_binding", `${label || "Index proof"} has the wrong proof type.`, {
|
|
237
|
+
label,
|
|
238
|
+
expectedProofType: proofType,
|
|
239
|
+
actualProofType: proof.proofType
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (root && proof.indexRoot !== root) {
|
|
243
|
+
badBinding("bad_index_proof_binding", `${label || "Index proof"} does not bind to the declared root.`, {
|
|
244
|
+
label,
|
|
245
|
+
expectedRoot: root,
|
|
246
|
+
actualRoot: proof.indexRoot
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (key && proof.key !== key) {
|
|
250
|
+
badBinding("bad_index_proof_binding", `${label || "Index proof"} does not bind to the declared key.`, {
|
|
251
|
+
label,
|
|
252
|
+
expectedKey: key,
|
|
253
|
+
actualKey: proof.key
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (proofs.workspaceProjection) {
|
|
258
|
+
expectIndexProof(proofs.workspaceProjection.orderProof, {
|
|
259
|
+
root: proofs.workspaceProjection.orderRoot,
|
|
260
|
+
key: proofs.workspaceProjection.orderKey,
|
|
261
|
+
proofType: "index.membership.prolly-path",
|
|
262
|
+
label: "workspace order proof"
|
|
263
|
+
});
|
|
264
|
+
expectIndexProof(proofs.workspaceProjection.membershipProof, {
|
|
265
|
+
root: proofs.workspaceProjection.membershipRoot,
|
|
266
|
+
key: envelope.factRef?.ledgerEventId || "",
|
|
267
|
+
proofType: "index.membership.prolly-path",
|
|
268
|
+
label: "workspace membership proof"
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const stateCommit = proofs.stateCommit;
|
|
272
|
+
if (stateCommit) {
|
|
273
|
+
const touchedKeyProofs = asArray(proofs.state?.touchedKeyProofs);
|
|
274
|
+
const mutationKeys = asArray(stateCommit.mutationKeys).map(String).filter(Boolean);
|
|
275
|
+
const mutationActions = asArray(stateCommit.mutationActions).map(String);
|
|
276
|
+
const invalidStateCommit = stateCommit.factType !== "state.commit" ||
|
|
277
|
+
stateCommit.intentId !== envelope.factId ||
|
|
278
|
+
stateCommit.stateRoot !== proofs.state?.root ||
|
|
279
|
+
Number(stateCommit.mutationCount || 0) !== mutationKeys.length ||
|
|
280
|
+
mutationActions.length !== mutationKeys.length ||
|
|
281
|
+
Number(stateCommit.touchedKeyCount || 0) !== touchedKeyProofs.length;
|
|
282
|
+
if (invalidStateCommit) {
|
|
283
|
+
failures.push(createVerificationFailure({
|
|
284
|
+
layer: "proof-semantics",
|
|
285
|
+
code: "bad_state_commit_binding",
|
|
286
|
+
message: "State Commit material does not bind to the envelope outcome and state proof root.",
|
|
287
|
+
evidenceRef: envelope.envelopeId
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
for (const [index, proof] of asArray(proofs.state?.touchedKeyProofs).entries()) {
|
|
292
|
+
const mutationKey = asArray(stateCommit?.mutationKeys)[index] || "";
|
|
293
|
+
const mutationAction = asArray(stateCommit?.mutationActions)[index] || "";
|
|
294
|
+
expectIndexProof(proof, {
|
|
295
|
+
root: proofs.state?.root || "",
|
|
296
|
+
key: String(mutationKey || proof?.key || ""),
|
|
297
|
+
proofType: mutationAction === "delete" ? "index.non-membership.prolly-path" : "index.membership.prolly-path",
|
|
298
|
+
label: `state touched key proof ${index}`
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const checkpointProof = proofs.checkpoint?.proof;
|
|
303
|
+
if (checkpointProof) {
|
|
304
|
+
expectIndexProof(checkpointProof, {
|
|
305
|
+
root: proofs.checkpoint?.root || "",
|
|
306
|
+
proofType: "index.membership.prolly-path",
|
|
307
|
+
label: "checkpoint proof"
|
|
308
|
+
});
|
|
309
|
+
const metadata = checkpointProof.entry?.metadata || {};
|
|
310
|
+
const isOutcome = envelope.envelopeKind === "operation-outcome";
|
|
311
|
+
const invalidCheckpoint = metadata.ledgerEventId !== envelope.factRef?.ledgerEventId ||
|
|
312
|
+
metadata.checkpointKind !== (isOutcome ? "outcome" : "intent") ||
|
|
313
|
+
(isOutcome && (
|
|
314
|
+
metadata.intentId !== envelope.factId ||
|
|
315
|
+
metadata.outcomeId !== stateCommit?.outcomeId ||
|
|
316
|
+
metadata.stateCommitId !== stateCommit?.stateCommitId
|
|
317
|
+
)) ||
|
|
318
|
+
(!isOutcome && metadata.intentId !== envelope.factId);
|
|
319
|
+
if (invalidCheckpoint) {
|
|
320
|
+
failures.push(createVerificationFailure({
|
|
321
|
+
layer: "proof-semantics",
|
|
322
|
+
code: "bad_checkpoint_binding",
|
|
323
|
+
message: "Checkpoint proof metadata does not bind to the envelope fact.",
|
|
324
|
+
evidenceRef: envelope.envelopeId
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const ledgerLeaf = proofMaterial?.ledger?.inclusionProof?.leaf || {};
|
|
329
|
+
const causalityRefs = asArray(ledgerLeaf.causalityRefs).map(String);
|
|
330
|
+
const causalityOutcomeId = String(stateCommit?.outcomeId || ledgerLeaf.outcomeId || "");
|
|
331
|
+
for (const [index, proof] of asArray(proofs.causality?.proofs).entries()) {
|
|
332
|
+
const expectedCausalityKey = causalityRefs[index] && causalityOutcomeId
|
|
333
|
+
? `${causalityRefs[index]}\u0000${causalityOutcomeId}`
|
|
334
|
+
: "";
|
|
335
|
+
expectIndexProof(proof, {
|
|
336
|
+
root: proofs.causality?.root || "",
|
|
337
|
+
key: expectedCausalityKey,
|
|
338
|
+
proofType: "index.membership.prolly-path",
|
|
339
|
+
label: `causality proof ${index}`
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (proofs.openIntent) {
|
|
343
|
+
expectIndexProof(proofs.openIntent, {
|
|
344
|
+
key: envelope.factId,
|
|
345
|
+
proofType: "index.membership.prolly-path",
|
|
346
|
+
label: "open intent proof"
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (proofs.outcome) {
|
|
350
|
+
expectIndexProof(proofs.outcome, {
|
|
351
|
+
key: stateCommit?.intentId || proofs.checkpoint?.proof?.entry?.metadata?.intentId || "",
|
|
352
|
+
proofType: "index.membership.prolly-path",
|
|
353
|
+
label: "outcome proof"
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
if (proofs.openIntentRemoved) {
|
|
357
|
+
expectIndexProof(proofs.openIntentRemoved, {
|
|
358
|
+
key: stateCommit?.intentId || proofs.checkpoint?.proof?.entry?.metadata?.intentId || "",
|
|
359
|
+
proofType: "index.non-membership.prolly-path",
|
|
360
|
+
label: "open intent removal proof"
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function verifyProofEnvelope(envelope, {
|
|
366
|
+
storage = null,
|
|
367
|
+
bundle = null,
|
|
368
|
+
supportedCriticalExtensions = [],
|
|
369
|
+
proofVerifiers = {},
|
|
370
|
+
requireAllProofs = true,
|
|
371
|
+
verifierManifest = null,
|
|
372
|
+
ledgerHeadSignatures = [],
|
|
373
|
+
bundleResolver = null,
|
|
374
|
+
includeBundleResolverFailures = true
|
|
375
|
+
} = {}) {
|
|
376
|
+
const failures = [];
|
|
377
|
+
const checkedProofPaths = [];
|
|
378
|
+
const supported = new Set([...CORE_CRITICAL_EXTENSIONS, ...supportedCriticalExtensions]);
|
|
379
|
+
const bundleMap = bundleResolver || bundleBlockMap(bundle);
|
|
380
|
+
if (!envelope || envelope.protocol !== PACTIUM_PROTOCOL || envelope.envelopeType !== "pactium.proof-envelope") {
|
|
381
|
+
return {
|
|
382
|
+
protocol: PACTIUM_PROTOCOL,
|
|
383
|
+
ok: false,
|
|
384
|
+
failures: [createVerificationFailure({
|
|
385
|
+
layer: "proof-envelope",
|
|
386
|
+
code: "malformed_envelope",
|
|
387
|
+
message: "Proof Envelope is missing or has the wrong protocol."
|
|
388
|
+
})]
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (finalizeEnvelope(envelope).envelopeId !== envelope.envelopeId) {
|
|
392
|
+
failures.push(createVerificationFailure({
|
|
393
|
+
layer: "proof-envelope",
|
|
394
|
+
code: "bad_envelope_id",
|
|
395
|
+
message: "Proof Envelope id does not match its hash-bound body.",
|
|
396
|
+
evidenceRef: envelope.envelopeId
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
for (const critical of asArray(envelope.criticalExtensions)) {
|
|
400
|
+
if (!supported.has(critical)) {
|
|
401
|
+
failures.push(createVerificationFailure({
|
|
402
|
+
layer: "proof-extension",
|
|
403
|
+
code: "unsupported_critical_extension",
|
|
404
|
+
message: `Unsupported critical Proof Extension: ${critical}`,
|
|
405
|
+
evidenceRef: critical,
|
|
406
|
+
repairable: true
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
let proofMaterial = null;
|
|
411
|
+
for (const proofRef of asArray(envelope.proofRefs)) {
|
|
412
|
+
let block = null;
|
|
413
|
+
try {
|
|
414
|
+
block = await resolveBlock({ cid: proofRef.cid, storage, bundleMap });
|
|
415
|
+
} catch (error) {
|
|
416
|
+
failures.push(createVerificationFailure({
|
|
417
|
+
layer: "proof-material",
|
|
418
|
+
code: "replaced_proof_material",
|
|
419
|
+
message: error instanceof Error ? error.message : "Proof material ref was replaced or corrupted.",
|
|
420
|
+
evidenceRef: proofRef.cid
|
|
421
|
+
}));
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (!block) {
|
|
425
|
+
failures.push(createVerificationFailure({
|
|
426
|
+
layer: "proof-material",
|
|
427
|
+
code: "missing_proof_material",
|
|
428
|
+
message: "Proof material ref is missing.",
|
|
429
|
+
evidenceRef: proofRef.cid,
|
|
430
|
+
repairable: true
|
|
431
|
+
}));
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (block.payloadHash !== proofRef.payloadHash || block.byteLength !== proofRef.byteLength) {
|
|
435
|
+
failures.push(createVerificationFailure({
|
|
436
|
+
layer: "proof-material",
|
|
437
|
+
code: "replaced_proof_material",
|
|
438
|
+
message: "Proof material ref was replaced or corrupted.",
|
|
439
|
+
evidenceRef: proofRef.cid
|
|
440
|
+
}));
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const value = await decodeBlockValue(block);
|
|
444
|
+
if (value?.materialType === "pactium.proof-material") proofMaterial = value;
|
|
445
|
+
}
|
|
446
|
+
for (const extension of asArray(envelope.extensions)) {
|
|
447
|
+
let block = null;
|
|
448
|
+
try {
|
|
449
|
+
block = await resolveBlock({ cid: extension.valueRef, storage, bundleMap });
|
|
450
|
+
} catch (error) {
|
|
451
|
+
failures.push(createVerificationFailure({
|
|
452
|
+
layer: "proof-extension",
|
|
453
|
+
code: "bad_extension_hash",
|
|
454
|
+
message: error instanceof Error ? error.message : "Proof Extension material was replaced or corrupted.",
|
|
455
|
+
evidenceRef: extension.valueRef
|
|
456
|
+
}));
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (!block) {
|
|
460
|
+
failures.push(createVerificationFailure({
|
|
461
|
+
layer: "proof-extension",
|
|
462
|
+
code: "missing_extension_material",
|
|
463
|
+
message: "Proof Extension material is missing.",
|
|
464
|
+
evidenceRef: extension.valueRef,
|
|
465
|
+
repairable: true
|
|
466
|
+
}));
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (block.payloadHash !== extension.valueHash) {
|
|
470
|
+
failures.push(createVerificationFailure({
|
|
471
|
+
layer: "proof-extension",
|
|
472
|
+
code: "bad_extension_hash",
|
|
473
|
+
message: "Proof Extension material hash does not match the envelope binding.",
|
|
474
|
+
evidenceRef: extension.valueRef
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (!proofMaterial?.ledger) {
|
|
479
|
+
failures.push(createVerificationFailure({
|
|
480
|
+
layer: "ledger",
|
|
481
|
+
code: "missing_ledger_proof",
|
|
482
|
+
message: "Proof Envelope has no Ledger inclusion material.",
|
|
483
|
+
repairable: true
|
|
484
|
+
}));
|
|
485
|
+
} else {
|
|
486
|
+
if (!verifyLedgerInclusionProof({
|
|
487
|
+
head: proofMaterial.ledger.head,
|
|
488
|
+
proof: proofMaterial.ledger.inclusionProof
|
|
489
|
+
})) {
|
|
490
|
+
failures.push(createVerificationFailure({
|
|
491
|
+
layer: "ledger",
|
|
492
|
+
code: "bad_ledger_inclusion",
|
|
493
|
+
message: "Ledger inclusion proof does not verify.",
|
|
494
|
+
evidenceRef: envelope.factRef?.ledgerEventId || ""
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
if (!verifyLedgerConsistencyProof({
|
|
498
|
+
oldHead: proofMaterial.ledger.previousHead,
|
|
499
|
+
newHead: proofMaterial.ledger.head,
|
|
500
|
+
proof: proofMaterial.ledger.consistencyProof
|
|
501
|
+
})) {
|
|
502
|
+
failures.push(createVerificationFailure({
|
|
503
|
+
layer: "ledger",
|
|
504
|
+
code: "bad_ledger_consistency",
|
|
505
|
+
message: "Ledger consistency proof does not verify.",
|
|
506
|
+
repairable: true
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
const manifestForHead = verifierManifest || proofMaterial.ledger.verifierManifest || proofMaterial.ledger.head?.verifierManifest || null;
|
|
510
|
+
const signaturesForHead = asArray(ledgerHeadSignatures).length > 0
|
|
511
|
+
? ledgerHeadSignatures
|
|
512
|
+
: asArray(proofMaterial.ledger.ledgerHeadSignatures || proofMaterial.ledger.head?.signatures);
|
|
513
|
+
if (manifestForHead) {
|
|
514
|
+
const signatureResult = verifyLedgerHeadSignature(proofMaterial.ledger.head, manifestForHead, {
|
|
515
|
+
signatures: signaturesForHead
|
|
516
|
+
});
|
|
517
|
+
failures.push(...signatureResult.failures);
|
|
518
|
+
if (signatureResult.ok) checkedProofPaths.push("ledger-head-signature");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (proofMaterial) {
|
|
522
|
+
verifySemanticBindings({ envelope, proofMaterial, failures });
|
|
523
|
+
await verifyEmbeddedProofs({
|
|
524
|
+
proofMaterial,
|
|
525
|
+
registry: createDefaultProofVerifierRegistry(proofVerifiers),
|
|
526
|
+
requireAllProofs,
|
|
527
|
+
failures,
|
|
528
|
+
checked: checkedProofPaths
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (includeBundleResolverFailures && bundleMap?.failures) {
|
|
532
|
+
failures.push(...bundleMap.failures);
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
protocol: PACTIUM_PROTOCOL,
|
|
536
|
+
envelopeId: envelope.envelopeId,
|
|
537
|
+
ok: failures.length === 0,
|
|
538
|
+
failures,
|
|
539
|
+
checked: [
|
|
540
|
+
"envelope-id",
|
|
541
|
+
"proof-material-refs",
|
|
542
|
+
"critical-extensions",
|
|
543
|
+
"ledger-inclusion",
|
|
544
|
+
"ledger-consistency",
|
|
545
|
+
...checkedProofPaths
|
|
546
|
+
]
|
|
547
|
+
};
|
|
548
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { verifyIndexProof } from "../index-engine/snapshot-merkle-index.js";
|
|
2
|
+
import { verifyLedgerConsistencyProof, verifyLedgerInclusionProof } from "../ledger/transparency-log.js";
|
|
3
|
+
import { PACTIUM_PROOF_TYPES } from "../protocol/constants.js";
|
|
4
|
+
import { asRecord } from "../shared/records.js";
|
|
5
|
+
|
|
6
|
+
export function createDefaultProofVerifierRegistry(extraVerifiers = {}) {
|
|
7
|
+
return new Map([
|
|
8
|
+
[PACTIUM_PROOF_TYPES.ledgerInclusion, (proof, context = {}) => verifyLedgerInclusionProof({ head: context.head || {}, proof })],
|
|
9
|
+
[PACTIUM_PROOF_TYPES.ledgerConsistency, (proof, context = {}) => verifyLedgerConsistencyProof({
|
|
10
|
+
oldHead: context.oldHead || {},
|
|
11
|
+
newHead: context.newHead || {},
|
|
12
|
+
proof
|
|
13
|
+
})],
|
|
14
|
+
[PACTIUM_PROOF_TYPES.indexMembership, verifyIndexProof],
|
|
15
|
+
[PACTIUM_PROOF_TYPES.indexNonMembership, verifyIndexProof],
|
|
16
|
+
...Object.entries(asRecord(extraVerifiers))
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const PACTIUM_PROTOCOL = "pactium.v0.2";
|
|
2
|
+
export const PACTIUM_SCHEMA_VERSION = "pactium.v0.2.schema.latest";
|
|
3
|
+
export const PACTIUM_PACKAGE_VERSION = "0.2.0";
|
|
4
|
+
export const PACTIUM_INDEX_ENGINE = "pactium.verifiable-index-engine";
|
|
5
|
+
export const PACTIUM_INDEX_SPLITTER = "pactium-cdc-boundary";
|
|
6
|
+
export const PACTIUM_PROOF_BUNDLE_TYPE = "pactium.proof-bundle.indexed";
|
|
7
|
+
export const PACTIUM_BUNDLE_ENCODING = "pactium.bundle.indexed-record-stream";
|
|
8
|
+
export const PACTIUM_PROOF_TYPES = Object.freeze({
|
|
9
|
+
ledgerInclusion: "ledger.inclusion.audit-path",
|
|
10
|
+
ledgerConsistency: "ledger.consistency.audit-path",
|
|
11
|
+
indexMembership: "index.membership.prolly-path",
|
|
12
|
+
indexNonMembership: "index.non-membership.prolly-path"
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const PACTIUM_PROTOCOL_PROFILE = Object.freeze({
|
|
16
|
+
protocol: PACTIUM_PROTOCOL,
|
|
17
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
18
|
+
hash: "sha256",
|
|
19
|
+
cid: "cid:sha256:<hex>",
|
|
20
|
+
canonicalValue: "restricted-ipld-dag-cbor-style",
|
|
21
|
+
orderingAuthority: "operation-ledger",
|
|
22
|
+
ledger: {
|
|
23
|
+
model: "rfc6962-transparency-log",
|
|
24
|
+
leafHash: "H(0x00 || canonical(leaf))",
|
|
25
|
+
nodeHash: "H(0x01 || leftHash || rightHash)",
|
|
26
|
+
emptyTreeHash: "H(\"\")",
|
|
27
|
+
appendLane: "single-ledger-append-lane"
|
|
28
|
+
},
|
|
29
|
+
indexEngine: {
|
|
30
|
+
structure: "canonical-prolly-tree",
|
|
31
|
+
chunking: {
|
|
32
|
+
minEntries: 32,
|
|
33
|
+
targetEntries: 64,
|
|
34
|
+
maxEntries: 128,
|
|
35
|
+
boundaryMask: 63
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
licoLite: {
|
|
39
|
+
exportPath: "pactium/licolite",
|
|
40
|
+
workspaceProjection: "enabled-by-default",
|
|
41
|
+
signing: "enabled-by-default",
|
|
42
|
+
criticalExtensions: [
|
|
43
|
+
"licolite.policy",
|
|
44
|
+
"licolite.workspaceEffect"
|
|
45
|
+
],
|
|
46
|
+
dataSupport: "latest-schema-only"
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const HASH_DOMAINS = Object.freeze({
|
|
51
|
+
block: "pactium.v0.2.block",
|
|
52
|
+
"append.condition": "pactium.v0.2.append.condition",
|
|
53
|
+
"checkpoint.node": "pactium.v0.2.checkpoint.node",
|
|
54
|
+
"index.boundary": "pactium.v0.2.index.boundary",
|
|
55
|
+
"index.leaf": "pactium.v0.2.index.leaf",
|
|
56
|
+
"index.node": "pactium.v0.2.index.node",
|
|
57
|
+
"ledger.consistency": "pactium.v0.2.ledger.consistency",
|
|
58
|
+
"ledger.event-id": "pactium.v0.2.ledger.event-id",
|
|
59
|
+
"ledger.head.signing": "pactium.v0.2.ledger.head.signing",
|
|
60
|
+
"operation.intent": "pactium.v0.2.operation.intent",
|
|
61
|
+
"operation.outcome": "pactium.v0.2.operation.outcome",
|
|
62
|
+
"proof.bundle": "pactium.v0.2.proof.bundle",
|
|
63
|
+
"proof.envelope": "pactium.v0.2.proof.envelope",
|
|
64
|
+
"proof.envelope.signing": "pactium.v0.2.proof.envelope.signing",
|
|
65
|
+
"proof.extension": "pactium.v0.2.proof.extension",
|
|
66
|
+
"state.commit": "pactium.v0.2.state.commit",
|
|
67
|
+
"verifier.manifest": "pactium.v0.2.verifier.manifest",
|
|
68
|
+
"workspace.projection": "pactium.v0.2.workspace.projection"
|
|
69
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { HASH_DOMAINS, PACTIUM_PROTOCOL } from "./constants.js";
|
|
3
|
+
import { canonicalEncode } from "../canonical/value.js";
|
|
4
|
+
|
|
5
|
+
export function hashBytes(bytes) {
|
|
6
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function hexToBytes(hex) {
|
|
10
|
+
return Buffer.from(String(hex || ""), "hex");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function cidFromHex(hex) {
|
|
14
|
+
return `cid:sha256:${hex}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hexFromCid(cid) {
|
|
18
|
+
const text = String(cid || "");
|
|
19
|
+
return text.startsWith("cid:sha256:") ? text.slice("cid:sha256:".length) : "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createId(prefix, value) {
|
|
23
|
+
return `${prefix}_${protocolHashHex(prefix, value).slice(0, 32)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function protocolHashHex(domain, value) {
|
|
27
|
+
const separator = HASH_DOMAINS[domain] || String(domain || "pactium.v0.2.generic");
|
|
28
|
+
const bytes = Buffer.isBuffer(value) || value instanceof Uint8Array
|
|
29
|
+
? Buffer.from(value)
|
|
30
|
+
: Buffer.from(canonicalEncode(value));
|
|
31
|
+
return hashBytes(Buffer.concat([
|
|
32
|
+
Buffer.from(`${PACTIUM_PROTOCOL}:${separator}\0`, "utf8"),
|
|
33
|
+
bytes
|
|
34
|
+
]));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function protocolHash(domain, value) {
|
|
38
|
+
return `sha256:${protocolHashHex(domain, value)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function cidForBytes(bytes) {
|
|
42
|
+
return cidFromHex(hashBytes(Buffer.from(bytes || "")));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function cidForCanonical(value) {
|
|
46
|
+
return cidForBytes(canonicalEncode(value));
|
|
47
|
+
}
|