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,36 @@
|
|
|
1
|
+
import { PACTIUM_PROTOCOL } from "../protocol/constants.js";
|
|
2
|
+
import { normalizeCanonicalValue } from "../canonical/value.js";
|
|
3
|
+
import { createId } from "../protocol/hashing.js";
|
|
4
|
+
|
|
5
|
+
export function createMaintenanceTaskEngine({ pactium = null } = {}) {
|
|
6
|
+
function planTask(taskType, input = {}) {
|
|
7
|
+
return {
|
|
8
|
+
protocol: PACTIUM_PROTOCOL,
|
|
9
|
+
taskId: createId("maintenance_task", { taskType, input }),
|
|
10
|
+
taskType,
|
|
11
|
+
input: normalizeCanonicalValue(input),
|
|
12
|
+
scheduler: "host-owned",
|
|
13
|
+
deterministic: true
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function runTask(task) {
|
|
17
|
+
if (task.taskType === "doctor" && pactium) {
|
|
18
|
+
return {
|
|
19
|
+
protocol: PACTIUM_PROTOCOL,
|
|
20
|
+
taskId: task.taskId,
|
|
21
|
+
ok: true,
|
|
22
|
+
result: await pactium.doctor()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
protocol: PACTIUM_PROTOCOL,
|
|
27
|
+
taskId: task.taskId,
|
|
28
|
+
ok: true,
|
|
29
|
+
result: {
|
|
30
|
+
plannedOnly: task.taskType !== "doctor",
|
|
31
|
+
daemon: false
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze({ protocol: PACTIUM_PROTOCOL, planTask, runTask });
|
|
36
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { canonicalDecode } from "../canonical/value.js";
|
|
2
|
+
import { PACTIUM_PROOF_BUNDLE_TYPE } from "../protocol/constants.js";
|
|
3
|
+
import { cidForBytes, hashBytes } from "../protocol/hashing.js";
|
|
4
|
+
import { asArray, asRecord } from "../shared/records.js";
|
|
5
|
+
import { createVerificationFailure } from "../verification/failure.js";
|
|
6
|
+
|
|
7
|
+
export function decodeVarint(bytes, offset = 0) {
|
|
8
|
+
let value = 0;
|
|
9
|
+
let shift = 0;
|
|
10
|
+
let cursor = offset;
|
|
11
|
+
while (cursor < bytes.length) {
|
|
12
|
+
const byte = bytes[cursor];
|
|
13
|
+
value += (byte & 0x7f) * (2 ** shift);
|
|
14
|
+
cursor += 1;
|
|
15
|
+
if ((byte & 0x80) === 0) return { value, nextOffset: cursor };
|
|
16
|
+
shift += 7;
|
|
17
|
+
if (shift > 56) throw new Error("Bundle varint is too large.");
|
|
18
|
+
}
|
|
19
|
+
throw new Error("Bundle varint is truncated.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function emptyResolver(indexFailures = []) {
|
|
23
|
+
const readFailures = [];
|
|
24
|
+
return {
|
|
25
|
+
blockCids: new Set(),
|
|
26
|
+
indexFailures,
|
|
27
|
+
readFailures,
|
|
28
|
+
get failures() {
|
|
29
|
+
return [...indexFailures, ...readFailures];
|
|
30
|
+
},
|
|
31
|
+
has(cid) {
|
|
32
|
+
return false;
|
|
33
|
+
},
|
|
34
|
+
get(cid) {
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
verifyAll() {
|
|
38
|
+
return { blocks: [], failures: this.failures };
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function bundleFailure(code, evidenceRef, message = "", repairable = false) {
|
|
44
|
+
return createVerificationFailure({
|
|
45
|
+
layer: "proof-bundle",
|
|
46
|
+
code,
|
|
47
|
+
message,
|
|
48
|
+
evidenceRef,
|
|
49
|
+
repairable
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function decodeIndexedMetadata({ bytes, item, maxHeaderSize, maxBlockSize }) {
|
|
54
|
+
const failures = [];
|
|
55
|
+
const offset = Number(item.offset || 0);
|
|
56
|
+
if (offset < 0 || offset >= bytes.length) {
|
|
57
|
+
return {
|
|
58
|
+
metadata: null,
|
|
59
|
+
failures: [bundleFailure("bad_bundle_offset", String(offset), "", true)]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
let decoded;
|
|
63
|
+
try {
|
|
64
|
+
decoded = decodeVarint(bytes, offset);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
metadata: null,
|
|
68
|
+
failures: [bundleFailure(
|
|
69
|
+
"bad_bundle_varint",
|
|
70
|
+
String(offset),
|
|
71
|
+
error instanceof Error ? error.message : "Bundle record varint could not be decoded."
|
|
72
|
+
)]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const recordLength = Number(item.recordLength || 0);
|
|
76
|
+
const headerLength = Number(item.headerLength || 0);
|
|
77
|
+
const payloadLength = Number(item.byteLength || 0);
|
|
78
|
+
if (decoded.value !== recordLength || recordLength !== headerLength + payloadLength) {
|
|
79
|
+
return {
|
|
80
|
+
metadata: null,
|
|
81
|
+
failures: [bundleFailure("bad_bundle_record_length", item.cid)]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (headerLength > maxHeaderSize) failures.push(bundleFailure("oversized_bundle_header", item.cid));
|
|
85
|
+
if (payloadLength > maxBlockSize) failures.push(bundleFailure("oversized_bundle_block", item.cid));
|
|
86
|
+
const headerStart = decoded.nextOffset;
|
|
87
|
+
const payloadStart = headerStart + headerLength;
|
|
88
|
+
const payloadEnd = payloadStart + payloadLength;
|
|
89
|
+
if (payloadEnd > bytes.length) {
|
|
90
|
+
return {
|
|
91
|
+
metadata: null,
|
|
92
|
+
failures: [bundleFailure("bad_bundle_offset", String(offset), "", true)]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const headerBytes = bytes.subarray(headerStart, payloadStart);
|
|
96
|
+
let header;
|
|
97
|
+
try {
|
|
98
|
+
header = asRecord(canonicalDecode(headerBytes));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
metadata: null,
|
|
102
|
+
failures: [bundleFailure(
|
|
103
|
+
"bad_bundle_header",
|
|
104
|
+
item.cid,
|
|
105
|
+
error instanceof Error ? error.message : "Bundle record header could not be decoded."
|
|
106
|
+
)]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (header.cid !== item.cid ||
|
|
110
|
+
header.payloadHash !== item.payloadHash ||
|
|
111
|
+
Number(header.byteLength || 0) !== payloadLength) {
|
|
112
|
+
return {
|
|
113
|
+
metadata: null,
|
|
114
|
+
failures: [bundleFailure("bad_bundle_index", item.cid)]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (failures.length > 0) return { metadata: null, failures };
|
|
118
|
+
return {
|
|
119
|
+
failures,
|
|
120
|
+
metadata: {
|
|
121
|
+
header,
|
|
122
|
+
payloadStart,
|
|
123
|
+
payloadEnd,
|
|
124
|
+
payloadLength
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function decodeIndexedBlock({ bytes, item, metadata, maxHeaderSize, maxBlockSize }) {
|
|
130
|
+
const decoded = metadata || decodeIndexedMetadata({ bytes, item, maxHeaderSize, maxBlockSize });
|
|
131
|
+
if (!decoded.metadata || decoded.failures.length > 0) {
|
|
132
|
+
return { block: null, failures: decoded.failures };
|
|
133
|
+
}
|
|
134
|
+
const { header, payloadStart, payloadEnd, payloadLength } = decoded.metadata;
|
|
135
|
+
const payloadBytes = bytes.subarray(payloadStart, payloadEnd);
|
|
136
|
+
const payloadHash = `sha256:${hashBytes(payloadBytes)}`;
|
|
137
|
+
if (payloadHash !== item.payloadHash || cidForBytes(payloadBytes) !== item.cid) {
|
|
138
|
+
return {
|
|
139
|
+
block: null,
|
|
140
|
+
failures: [bundleFailure("bad_bundle_index", item.cid)]
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
failures: [],
|
|
145
|
+
block: {
|
|
146
|
+
protocol: header.protocol,
|
|
147
|
+
cid: header.cid,
|
|
148
|
+
codec: header.codec,
|
|
149
|
+
kind: header.kind,
|
|
150
|
+
refs: asArray(header.refs),
|
|
151
|
+
byteLength: payloadLength,
|
|
152
|
+
payloadHash,
|
|
153
|
+
payloadBase64: payloadBytes.toString("base64")
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function createIndexedBundleResolver(bundle, {
|
|
159
|
+
maxHeaderSize = 16 * 1024,
|
|
160
|
+
maxBlockSize = 64 * 1024 * 1024
|
|
161
|
+
} = {}) {
|
|
162
|
+
if (bundle?.bundleType !== PACTIUM_PROOF_BUNDLE_TYPE) {
|
|
163
|
+
return emptyResolver([
|
|
164
|
+
bundleFailure(
|
|
165
|
+
"malformed_bundle",
|
|
166
|
+
String(bundle?.bundleType || ""),
|
|
167
|
+
"Proof Bundle must use the indexed bundle type."
|
|
168
|
+
)
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
if (!bundle.binaryBase64) {
|
|
172
|
+
return emptyResolver([
|
|
173
|
+
bundleFailure(
|
|
174
|
+
"missing_bundle_binary",
|
|
175
|
+
PACTIUM_PROOF_BUNDLE_TYPE,
|
|
176
|
+
"Proof Bundle is missing indexed binary records.",
|
|
177
|
+
true
|
|
178
|
+
)
|
|
179
|
+
]);
|
|
180
|
+
}
|
|
181
|
+
const bytes = Buffer.from(String(bundle.binaryBase64 || ""), "base64");
|
|
182
|
+
const offsets = new Set();
|
|
183
|
+
const cids = new Set();
|
|
184
|
+
const entries = asArray(bundle.index).map((item, ordinal) => ({ item, ordinal }));
|
|
185
|
+
const indexByCid = new Map();
|
|
186
|
+
const cache = new Map();
|
|
187
|
+
const metadataCache = new Map();
|
|
188
|
+
const payloadCache = new Map();
|
|
189
|
+
const indexFailures = [];
|
|
190
|
+
const readFailures = [];
|
|
191
|
+
function metadataFor(entry) {
|
|
192
|
+
if (metadataCache.has(entry.ordinal)) return metadataCache.get(entry.ordinal);
|
|
193
|
+
const decoded = decodeIndexedMetadata({
|
|
194
|
+
bytes,
|
|
195
|
+
item: entry.item,
|
|
196
|
+
maxHeaderSize,
|
|
197
|
+
maxBlockSize
|
|
198
|
+
});
|
|
199
|
+
metadataCache.set(entry.ordinal, decoded);
|
|
200
|
+
return decoded;
|
|
201
|
+
}
|
|
202
|
+
function decodePayload(entry) {
|
|
203
|
+
if (payloadCache.has(entry.ordinal)) return payloadCache.get(entry.ordinal);
|
|
204
|
+
const decoded = decodeIndexedBlock({
|
|
205
|
+
bytes,
|
|
206
|
+
item: entry.item,
|
|
207
|
+
metadata: metadataFor(entry),
|
|
208
|
+
maxHeaderSize,
|
|
209
|
+
maxBlockSize
|
|
210
|
+
});
|
|
211
|
+
if (decoded.failures.length > 0 && metadataFor(entry).failures.length === 0) {
|
|
212
|
+
readFailures.push(...decoded.failures);
|
|
213
|
+
}
|
|
214
|
+
payloadCache.set(entry.ordinal, decoded.block);
|
|
215
|
+
return decoded.block;
|
|
216
|
+
}
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const { item } = entry;
|
|
219
|
+
const offset = Number(item.offset || 0);
|
|
220
|
+
if (offsets.has(offset)) {
|
|
221
|
+
indexFailures.push(bundleFailure("duplicate_bundle_offset", String(offset)));
|
|
222
|
+
}
|
|
223
|
+
offsets.add(offset);
|
|
224
|
+
if (cids.has(item.cid)) {
|
|
225
|
+
indexFailures.push(bundleFailure("duplicate_bundle_cid", item.cid));
|
|
226
|
+
} else {
|
|
227
|
+
indexByCid.set(item.cid, entry);
|
|
228
|
+
}
|
|
229
|
+
cids.add(item.cid);
|
|
230
|
+
if (offset < 0 || offset >= bytes.length) {
|
|
231
|
+
indexFailures.push(bundleFailure("bad_bundle_offset", String(offset), "", true));
|
|
232
|
+
}
|
|
233
|
+
indexFailures.push(...metadataFor(entry).failures);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
blockCids: new Set(indexByCid.keys()),
|
|
237
|
+
indexFailures,
|
|
238
|
+
readFailures,
|
|
239
|
+
get failures() {
|
|
240
|
+
return [...indexFailures, ...readFailures];
|
|
241
|
+
},
|
|
242
|
+
has(cid) {
|
|
243
|
+
return indexByCid.has(cid);
|
|
244
|
+
},
|
|
245
|
+
get(cid) {
|
|
246
|
+
if (!indexByCid.has(cid)) return null;
|
|
247
|
+
if (cache.has(cid)) return cache.get(cid);
|
|
248
|
+
const block = decodePayload(indexByCid.get(cid));
|
|
249
|
+
cache.set(cid, block);
|
|
250
|
+
return block;
|
|
251
|
+
},
|
|
252
|
+
verifyAll() {
|
|
253
|
+
const blocks = [];
|
|
254
|
+
for (const entry of entries) {
|
|
255
|
+
const block = decodePayload(entry);
|
|
256
|
+
if (block) blocks.push(block);
|
|
257
|
+
}
|
|
258
|
+
return { blocks, failures: this.failures };
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function indexedBlocksFromBundle(bundle, options = {}) {
|
|
264
|
+
return createIndexedBundleResolver(bundle, options).verifyAll();
|
|
265
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { PACTIUM_PROOF_BUNDLE_TYPE, PACTIUM_PROTOCOL } from "../protocol/constants.js";
|
|
2
|
+
import { asArray } from "../shared/records.js";
|
|
3
|
+
import { protocolHash } from "../protocol/hashing.js";
|
|
4
|
+
import { createVerificationFailure } from "../verification/failure.js";
|
|
5
|
+
import { createIndexedBundleResolver } from "./bundle-format.js";
|
|
6
|
+
import { verifyProofEnvelope } from "./envelope.js";
|
|
7
|
+
|
|
8
|
+
export async function verifyProofBundle(bundle, options = {}) {
|
|
9
|
+
if (!bundle || bundle.protocol !== PACTIUM_PROTOCOL || bundle.bundleType !== PACTIUM_PROOF_BUNDLE_TYPE) {
|
|
10
|
+
return {
|
|
11
|
+
protocol: PACTIUM_PROTOCOL,
|
|
12
|
+
ok: false,
|
|
13
|
+
failures: [createVerificationFailure({
|
|
14
|
+
layer: "proof-bundle",
|
|
15
|
+
code: "malformed_bundle",
|
|
16
|
+
message: "Proof Bundle is missing or has the wrong protocol."
|
|
17
|
+
})]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const failures = [];
|
|
21
|
+
const maxHeaderSize = Number(options.maxHeaderSize || 16 * 1024);
|
|
22
|
+
const maxBlockSize = Number(options.maxBlockSize || 64 * 1024 * 1024);
|
|
23
|
+
const resolver = createIndexedBundleResolver(bundle, { maxHeaderSize, maxBlockSize });
|
|
24
|
+
failures.push(...resolver.indexFailures);
|
|
25
|
+
const blockCids = resolver.blockCids;
|
|
26
|
+
const expectedBundleHash = protocolHash("proof.bundle", {
|
|
27
|
+
manifest: bundle.manifest,
|
|
28
|
+
envelope: bundle.envelope,
|
|
29
|
+
index: asArray(bundle.index).map((item) => ({
|
|
30
|
+
cid: item.cid,
|
|
31
|
+
offset: item.offset,
|
|
32
|
+
recordLength: item.recordLength,
|
|
33
|
+
headerLength: item.headerLength,
|
|
34
|
+
byteLength: item.byteLength,
|
|
35
|
+
payloadHash: item.payloadHash
|
|
36
|
+
}))
|
|
37
|
+
});
|
|
38
|
+
if (bundle.bundleHash && bundle.bundleHash !== expectedBundleHash) {
|
|
39
|
+
failures.push(createVerificationFailure({
|
|
40
|
+
layer: "proof-bundle",
|
|
41
|
+
code: "bad_bundle_hash",
|
|
42
|
+
message: "Proof Bundle hash does not match its indexed contents.",
|
|
43
|
+
evidenceRef: bundle.bundleHash
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
for (const required of asArray(bundle.manifest?.requiredBlocks)) {
|
|
47
|
+
if (!blockCids.has(required)) {
|
|
48
|
+
failures.push(createVerificationFailure({
|
|
49
|
+
layer: "proof-bundle",
|
|
50
|
+
code: "missing_bundle_block",
|
|
51
|
+
message: "Proof Bundle is missing a required block.",
|
|
52
|
+
evidenceRef: required,
|
|
53
|
+
repairable: true
|
|
54
|
+
}));
|
|
55
|
+
} else {
|
|
56
|
+
resolver.get(required);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (options.verifyAllBlocks === true) resolver.verifyAll();
|
|
60
|
+
const envelopeResult = await verifyProofEnvelope(bundle.envelope, {
|
|
61
|
+
bundle,
|
|
62
|
+
bundleResolver: resolver,
|
|
63
|
+
includeBundleResolverFailures: false,
|
|
64
|
+
supportedCriticalExtensions: options.supportedCriticalExtensions || [],
|
|
65
|
+
proofVerifiers: options.proofVerifiers || {},
|
|
66
|
+
requireAllProofs: options.requireAllProofs !== false,
|
|
67
|
+
verifierManifest: options.verifierManifest || null,
|
|
68
|
+
ledgerHeadSignatures: options.ledgerHeadSignatures || []
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
protocol: PACTIUM_PROTOCOL,
|
|
72
|
+
bundleHash: bundle.bundleHash,
|
|
73
|
+
ok: failures.length === 0 && envelopeResult.ok,
|
|
74
|
+
failures: [...failures, ...resolver.readFailures, ...envelopeResult.failures],
|
|
75
|
+
envelope: envelopeResult
|
|
76
|
+
};
|
|
77
|
+
}
|