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,702 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { PACTIUM_PROOF_TYPES, PACTIUM_PROTOCOL, PACTIUM_SCHEMA_VERSION } from "../protocol/constants.js";
|
|
4
|
+
import { canonicalEncode } from "../canonical/value.js";
|
|
5
|
+
import { cidFromHex, createId, hashBytes, hexToBytes, protocolHash } from "../protocol/hashing.js";
|
|
6
|
+
import { createStoragePort } from "../storage/local-json-storage-port.js";
|
|
7
|
+
import { asArray, nowIso } from "../shared/records.js";
|
|
8
|
+
import { createVerifierManifest, signLedgerHead } from "./signed-head.js";
|
|
9
|
+
|
|
10
|
+
export function ledgerLeafHash(leaf) {
|
|
11
|
+
return hashBytes(Buffer.concat([Buffer.from([0x00]), Buffer.from(canonicalEncode(leaf))]));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ledgerNodeHash(leftHash, rightHash) {
|
|
15
|
+
return hashBytes(Buffer.concat([Buffer.from([0x01]), hexToBytes(leftHash), hexToBytes(rightHash)]));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function emptyTreeHash() {
|
|
19
|
+
return hashBytes(Buffer.alloc(0));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hashFromAuditItem(item) {
|
|
23
|
+
return typeof item === "string" ? item : String(item?.hash || "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isHashHex(value) {
|
|
27
|
+
return /^[a-f0-9]{64}$/i.test(String(value || ""));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function auditPathHashesAreValid(auditPath) {
|
|
31
|
+
return asArray(auditPath).every((item) => isHashHex(hashFromAuditItem(item)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function largestPowerOfTwoLessThan(value) {
|
|
35
|
+
let power = 1;
|
|
36
|
+
while (power * 2 < value) power *= 2;
|
|
37
|
+
return power;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function rootHashFromLeafHashes(leafHashes) {
|
|
41
|
+
const hashes = asArray(leafHashes);
|
|
42
|
+
if (hashes.length === 0) return emptyTreeHash();
|
|
43
|
+
if (hashes.length === 1) return hashes[0];
|
|
44
|
+
const split = largestPowerOfTwoLessThan(hashes.length);
|
|
45
|
+
return ledgerNodeHash(
|
|
46
|
+
rootHashFromLeafHashes(hashes.slice(0, split)),
|
|
47
|
+
rootHashFromLeafHashes(hashes.slice(split))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inclusionPath(index, hashes) {
|
|
52
|
+
if (hashes.length <= 1) return [];
|
|
53
|
+
const split = largestPowerOfTwoLessThan(hashes.length);
|
|
54
|
+
if (index < split) {
|
|
55
|
+
return [
|
|
56
|
+
...inclusionPath(index, hashes.slice(0, split)),
|
|
57
|
+
{ side: "right", hash: rootHashFromLeafHashes(hashes.slice(split)) }
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
return [
|
|
61
|
+
{ side: "left", hash: rootHashFromLeafHashes(hashes.slice(0, split)) },
|
|
62
|
+
...inclusionPath(index - split, hashes.slice(split))
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function rootFromInclusion(index, size, leafHash, proof, offset = 0) {
|
|
67
|
+
if (size === 1) return { rootHash: leafHash, offset };
|
|
68
|
+
const split = largestPowerOfTwoLessThan(size);
|
|
69
|
+
if (index < split) {
|
|
70
|
+
const left = rootFromInclusion(index, split, leafHash, proof, offset);
|
|
71
|
+
const item = proof[left.offset];
|
|
72
|
+
if (item && typeof item === "object" && item.side && item.side !== "right") return { rootHash: "", offset: proof.length + 1 };
|
|
73
|
+
return { rootHash: ledgerNodeHash(left.rootHash, hashFromAuditItem(item)), offset: left.offset + 1 };
|
|
74
|
+
}
|
|
75
|
+
const item = proof[offset];
|
|
76
|
+
if (item && typeof item === "object" && item.side && item.side !== "left") return { rootHash: "", offset: proof.length + 1 };
|
|
77
|
+
const right = rootFromInclusion(index - split, size - split, leafHash, proof, offset + 1);
|
|
78
|
+
return { rootHash: ledgerNodeHash(hashFromAuditItem(item), right.rootHash), offset: right.offset };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function consistencyPath(oldSize, hashes, trusted = true) {
|
|
82
|
+
const newSize = hashes.length;
|
|
83
|
+
if (oldSize === 0) return [];
|
|
84
|
+
if (oldSize === newSize) return trusted ? [] : [rootHashFromLeafHashes(hashes)];
|
|
85
|
+
const split = largestPowerOfTwoLessThan(newSize);
|
|
86
|
+
if (oldSize <= split) {
|
|
87
|
+
return [
|
|
88
|
+
...consistencyPath(oldSize, hashes.slice(0, split), trusted),
|
|
89
|
+
rootHashFromLeafHashes(hashes.slice(split))
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
return [
|
|
93
|
+
...consistencyPath(oldSize - split, hashes.slice(split), false),
|
|
94
|
+
rootHashFromLeafHashes(hashes.slice(0, split))
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function verifyConsistencyPath({ oldSize, newSize, oldRootHash, newRootHash, auditPath }) {
|
|
99
|
+
const proof = asArray(auditPath).map(hashFromAuditItem);
|
|
100
|
+
if (!isHashHex(oldRootHash) || !isHashHex(newRootHash) || !auditPathHashesAreValid(proof)) return false;
|
|
101
|
+
if (oldSize > newSize) return false;
|
|
102
|
+
if (oldSize === 0) return oldRootHash === emptyTreeHash() && proof.length === 0;
|
|
103
|
+
if (oldSize === newSize) return oldRootHash === newRootHash && proof.length === 0;
|
|
104
|
+
if (proof.length === 0) return false;
|
|
105
|
+
|
|
106
|
+
let first = oldSize - 1;
|
|
107
|
+
let second = newSize - 1;
|
|
108
|
+
while ((first & 1) === 1) {
|
|
109
|
+
first >>= 1;
|
|
110
|
+
second >>= 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let offset = 0;
|
|
114
|
+
let oldHash;
|
|
115
|
+
let newHash;
|
|
116
|
+
if (first === 0) {
|
|
117
|
+
oldHash = oldRootHash;
|
|
118
|
+
newHash = oldRootHash;
|
|
119
|
+
} else {
|
|
120
|
+
oldHash = proof[offset];
|
|
121
|
+
newHash = proof[offset];
|
|
122
|
+
offset += 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
while (first !== 0) {
|
|
126
|
+
if ((first & 1) === 1) {
|
|
127
|
+
if (offset >= proof.length) return false;
|
|
128
|
+
oldHash = ledgerNodeHash(proof[offset], oldHash);
|
|
129
|
+
newHash = ledgerNodeHash(proof[offset], newHash);
|
|
130
|
+
offset += 1;
|
|
131
|
+
} else if (first < second) {
|
|
132
|
+
if (offset >= proof.length) return false;
|
|
133
|
+
newHash = ledgerNodeHash(newHash, proof[offset]);
|
|
134
|
+
offset += 1;
|
|
135
|
+
}
|
|
136
|
+
first >>= 1;
|
|
137
|
+
second >>= 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
while (second !== 0) {
|
|
141
|
+
if (offset >= proof.length) return false;
|
|
142
|
+
newHash = ledgerNodeHash(newHash, proof[offset]);
|
|
143
|
+
offset += 1;
|
|
144
|
+
second >>= 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return offset === proof.length && oldHash === oldRootHash && newHash === newRootHash;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function compactRangeRoot(peaks) {
|
|
151
|
+
const normalized = asArray(peaks);
|
|
152
|
+
if (normalized.length === 0) return emptyTreeHash();
|
|
153
|
+
let current = normalized[normalized.length - 1].hash;
|
|
154
|
+
for (let index = normalized.length - 2; index >= 0; index -= 1) {
|
|
155
|
+
current = ledgerNodeHash(normalized[index].hash, current);
|
|
156
|
+
}
|
|
157
|
+
return current;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function levelSize(level) {
|
|
161
|
+
return 2 ** Number(level || 0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function ledgerHeadFromCompactRange({ peaks = [], size = 0, previousHeadId = "", createdAt = nowIso(), ledgerId = "pactium-operation-ledger" } = {}) {
|
|
165
|
+
const rootHash = compactRangeRoot(peaks);
|
|
166
|
+
const headBase = {
|
|
167
|
+
protocol: PACTIUM_PROTOCOL,
|
|
168
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
169
|
+
ledgerId,
|
|
170
|
+
size,
|
|
171
|
+
rootHash,
|
|
172
|
+
root: cidFromHex(rootHash),
|
|
173
|
+
previousHeadId,
|
|
174
|
+
createdAt
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
...headBase,
|
|
178
|
+
headId: createId("ledger_head", headBase)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function createLedgerInclusionProof({ leafHashes = [], index = 0, leaf = null, headRef = "" } = {}) {
|
|
183
|
+
const hashes = asArray(leafHashes);
|
|
184
|
+
if (index < 0 || index >= hashes.length) {
|
|
185
|
+
throw new RangeError("Ledger inclusion proof index is out of range.");
|
|
186
|
+
}
|
|
187
|
+
const rootHash = rootHashFromLeafHashes(hashes);
|
|
188
|
+
return {
|
|
189
|
+
protocol: PACTIUM_PROTOCOL,
|
|
190
|
+
proofType: PACTIUM_PROOF_TYPES.ledgerInclusion,
|
|
191
|
+
index,
|
|
192
|
+
size: hashes.length,
|
|
193
|
+
leafHash: hashes[index],
|
|
194
|
+
leaf,
|
|
195
|
+
auditPath: inclusionPath(index, hashes),
|
|
196
|
+
rootHash,
|
|
197
|
+
headRef
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function verifyLedgerInclusionProof({ head = {}, proof = {} } = {}) {
|
|
202
|
+
if (!proof || proof.proofType !== PACTIUM_PROOF_TYPES.ledgerInclusion) return false;
|
|
203
|
+
if (proof.size === 0 || proof.index < 0 || proof.index >= proof.size) return false;
|
|
204
|
+
if (!isHashHex(proof.leafHash) || !isHashHex(proof.rootHash) || !auditPathHashesAreValid(proof.auditPath)) return false;
|
|
205
|
+
const leafHash = proof.leaf ? ledgerLeafHash(proof.leaf) : proof.leafHash;
|
|
206
|
+
if (leafHash !== proof.leafHash) return false;
|
|
207
|
+
const result = rootFromInclusion(proof.index, proof.size, proof.leafHash, asArray(proof.auditPath));
|
|
208
|
+
return result.offset === asArray(proof.auditPath).length &&
|
|
209
|
+
result.rootHash === proof.rootHash &&
|
|
210
|
+
result.rootHash === head.rootHash &&
|
|
211
|
+
Number(head.size) === Number(proof.size);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function createLedgerConsistencyProof({ oldHead = {}, newEntries = [], headRef = "", oldHeadRef = "" } = {}) {
|
|
215
|
+
const hashes = asArray(newEntries).map((entry) => entry.leafHash);
|
|
216
|
+
const oldSize = Number(oldHead.size || 0);
|
|
217
|
+
if (oldSize > hashes.length) {
|
|
218
|
+
throw new RangeError("Ledger consistency proof old size is greater than new size.");
|
|
219
|
+
}
|
|
220
|
+
const oldRootHash = oldSize === 0 ? emptyTreeHash() : rootHashFromLeafHashes(hashes.slice(0, oldSize));
|
|
221
|
+
const newRootHash = rootHashFromLeafHashes(hashes);
|
|
222
|
+
const auditPath = consistencyPath(oldSize, hashes);
|
|
223
|
+
return {
|
|
224
|
+
protocol: PACTIUM_PROTOCOL,
|
|
225
|
+
proofType: PACTIUM_PROOF_TYPES.ledgerConsistency,
|
|
226
|
+
oldSize,
|
|
227
|
+
newSize: hashes.length,
|
|
228
|
+
oldRootHash,
|
|
229
|
+
newRootHash,
|
|
230
|
+
auditPath,
|
|
231
|
+
oldHeadRef,
|
|
232
|
+
newHeadRef: headRef,
|
|
233
|
+
proofHash: protocolHash("ledger.consistency", { oldSize, newSize: hashes.length, oldRootHash, newRootHash, auditPath })
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function verifyLedgerConsistencyProof({ oldHead = {}, newHead = {}, proof = {} } = {}) {
|
|
238
|
+
if (!proof || proof.proofType !== PACTIUM_PROOF_TYPES.ledgerConsistency) return false;
|
|
239
|
+
if (Number(oldHead.size || 0) !== Number(proof.oldSize)) return false;
|
|
240
|
+
if (Number(newHead.size || 0) !== Number(proof.newSize)) return false;
|
|
241
|
+
if (proof.oldSize > proof.newSize) return false;
|
|
242
|
+
if (!isHashHex(proof.oldRootHash) || !isHashHex(proof.newRootHash) || !auditPathHashesAreValid(proof.auditPath)) return false;
|
|
243
|
+
return proof.oldRootHash === oldHead.rootHash &&
|
|
244
|
+
proof.newRootHash === newHead.rootHash &&
|
|
245
|
+
verifyConsistencyPath({
|
|
246
|
+
oldSize: Number(proof.oldSize || 0),
|
|
247
|
+
newSize: Number(proof.newSize || 0),
|
|
248
|
+
oldRootHash: proof.oldRootHash,
|
|
249
|
+
newRootHash: proof.newRootHash,
|
|
250
|
+
auditPath: proof.auditPath
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function createCompactRange({ size = 0, peaks = [] } = {}) {
|
|
255
|
+
return {
|
|
256
|
+
protocol: PACTIUM_PROTOCOL,
|
|
257
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
258
|
+
rangeType: "pactium.ledger.compact-range",
|
|
259
|
+
size: Number(size || 0),
|
|
260
|
+
peaks: asArray(peaks)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isPowerOfTwo(value) {
|
|
265
|
+
return value > 0 && (value & (value - 1)) === 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function levelForSize(size) {
|
|
269
|
+
return Math.log2(size);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function createLedgerTransparencyLog({
|
|
273
|
+
storage = createStoragePort({ inMemory: true }),
|
|
274
|
+
ledgerId = "pactium-operation-ledger",
|
|
275
|
+
signer = "auto",
|
|
276
|
+
verifierManifest = null
|
|
277
|
+
} = {}) {
|
|
278
|
+
let entries = [];
|
|
279
|
+
let compactRange = createCompactRange();
|
|
280
|
+
let currentHead = null;
|
|
281
|
+
let signingState = null;
|
|
282
|
+
let loaded = false;
|
|
283
|
+
let loadPromise = null;
|
|
284
|
+
let appendLane = Promise.resolve();
|
|
285
|
+
|
|
286
|
+
async function readLegacyEntries() {
|
|
287
|
+
return asArray(await storage.getProtocolObject("ledger", "operation-ledger", []));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function load() {
|
|
291
|
+
if (loaded) return;
|
|
292
|
+
if (loadPromise) return loadPromise;
|
|
293
|
+
loadPromise = (async () => {
|
|
294
|
+
compactRange = await storage.getProtocolObject("ledger", "compact-range-current", null);
|
|
295
|
+
currentHead = await storage.getProtocolObject("ledger", "head-current", null);
|
|
296
|
+
if (compactRange && currentHead) {
|
|
297
|
+
entries = [];
|
|
298
|
+
loaded = true;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
entries = await readLegacyEntries();
|
|
302
|
+
compactRange = createCompactRange();
|
|
303
|
+
for (const entry of entries) {
|
|
304
|
+
await storage.putProtocolObject("ledger-leaf", String(entry.index), entry);
|
|
305
|
+
await mergeLeafIntoCompactRange(entry);
|
|
306
|
+
}
|
|
307
|
+
currentHead = ledgerHeadFromCompactRange({ peaks: compactRange.peaks, size: entries.length, ledgerId });
|
|
308
|
+
loaded = true;
|
|
309
|
+
})();
|
|
310
|
+
try {
|
|
311
|
+
return await loadPromise;
|
|
312
|
+
} finally {
|
|
313
|
+
loadPromise = null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function reload() {
|
|
318
|
+
await appendLane.catch(() => null);
|
|
319
|
+
entries = [];
|
|
320
|
+
compactRange = createCompactRange();
|
|
321
|
+
currentHead = null;
|
|
322
|
+
signingState = null;
|
|
323
|
+
loaded = false;
|
|
324
|
+
loadPromise = null;
|
|
325
|
+
await load();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function writeNodeRecord(node) {
|
|
329
|
+
await storage.putProtocolObject("ledger-node", `${node.level}-${node.index}`, node);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function readNodeRecord(level, index) {
|
|
333
|
+
return storage.getProtocolObject("ledger-node", `${level}-${index}`, null);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function readLeafRecord(index) {
|
|
337
|
+
return storage.getProtocolObject("ledger-leaf", String(index), null);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function requireLeafRecord(index) {
|
|
341
|
+
const leaf = await readLeafRecord(index);
|
|
342
|
+
if (!leaf) throw new Error(`Ledger leaf missing for ${index}`);
|
|
343
|
+
return leaf;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function rangeRoot(start, size) {
|
|
347
|
+
if (size === 0) return emptyTreeHash();
|
|
348
|
+
if (size === 1) {
|
|
349
|
+
const leaf = await requireLeafRecord(start);
|
|
350
|
+
return leaf.leafHash;
|
|
351
|
+
}
|
|
352
|
+
if (isPowerOfTwo(size)) {
|
|
353
|
+
const level = levelForSize(size);
|
|
354
|
+
const node = await readNodeRecord(level, Math.floor(start / size));
|
|
355
|
+
if (!node) throw new Error(`Ledger node missing for level ${level} index ${Math.floor(start / size)}`);
|
|
356
|
+
return node.hash;
|
|
357
|
+
}
|
|
358
|
+
const split = largestPowerOfTwoLessThan(size);
|
|
359
|
+
return ledgerNodeHash(
|
|
360
|
+
await rangeRoot(start, split),
|
|
361
|
+
await rangeRoot(start + split, size - split)
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function inclusionPathFromStore(index, size, start = 0) {
|
|
366
|
+
if (size <= 1) return [];
|
|
367
|
+
const split = largestPowerOfTwoLessThan(size);
|
|
368
|
+
if (index < split) {
|
|
369
|
+
return [
|
|
370
|
+
...await inclusionPathFromStore(index, split, start),
|
|
371
|
+
{ side: "right", hash: await rangeRoot(start + split, size - split) }
|
|
372
|
+
];
|
|
373
|
+
}
|
|
374
|
+
return [
|
|
375
|
+
{ side: "left", hash: await rangeRoot(start, split) },
|
|
376
|
+
...await inclusionPathFromStore(index - split, size - split, start + split)
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function consistencyPathFromStore(oldSize, newSize, start = 0, trusted = true) {
|
|
381
|
+
if (oldSize === 0) return [];
|
|
382
|
+
if (oldSize === newSize) return trusted ? [] : [await rangeRoot(start, newSize)];
|
|
383
|
+
const split = largestPowerOfTwoLessThan(newSize);
|
|
384
|
+
if (oldSize <= split) {
|
|
385
|
+
return [
|
|
386
|
+
...await consistencyPathFromStore(oldSize, split, start, trusted),
|
|
387
|
+
await rangeRoot(start + split, newSize - split)
|
|
388
|
+
];
|
|
389
|
+
}
|
|
390
|
+
return [
|
|
391
|
+
...await consistencyPathFromStore(oldSize - split, newSize - split, start + split, false),
|
|
392
|
+
await rangeRoot(start, split)
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function createStoredInclusionProof({ index, leaf, head }) {
|
|
397
|
+
if (Number(index || 0) < 0 || Number(index || 0) >= Number(head.size || 0)) {
|
|
398
|
+
throw new RangeError("Ledger inclusion proof index is out of range.");
|
|
399
|
+
}
|
|
400
|
+
const leafRecord = await requireLeafRecord(index);
|
|
401
|
+
return {
|
|
402
|
+
protocol: PACTIUM_PROTOCOL,
|
|
403
|
+
proofType: PACTIUM_PROOF_TYPES.ledgerInclusion,
|
|
404
|
+
index,
|
|
405
|
+
size: Number(head.size || 0),
|
|
406
|
+
leafHash: leafRecord.leafHash,
|
|
407
|
+
leaf,
|
|
408
|
+
auditPath: await inclusionPathFromStore(index, Number(head.size || 0)),
|
|
409
|
+
rootHash: head.rootHash,
|
|
410
|
+
headRef: head.headId || ""
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function createStoredConsistencyProof({ oldHead, newHead }) {
|
|
415
|
+
const oldSize = Number(oldHead.size || 0);
|
|
416
|
+
const newSize = Number(newHead.size || 0);
|
|
417
|
+
if (oldSize > newSize) {
|
|
418
|
+
throw new RangeError("Ledger consistency proof old size is greater than new size.");
|
|
419
|
+
}
|
|
420
|
+
const auditPath = await consistencyPathFromStore(oldSize, newSize);
|
|
421
|
+
return {
|
|
422
|
+
protocol: PACTIUM_PROTOCOL,
|
|
423
|
+
proofType: PACTIUM_PROOF_TYPES.ledgerConsistency,
|
|
424
|
+
oldSize,
|
|
425
|
+
newSize,
|
|
426
|
+
oldRootHash: oldHead.rootHash,
|
|
427
|
+
newRootHash: newHead.rootHash,
|
|
428
|
+
auditPath,
|
|
429
|
+
oldHeadRef: oldHead.headId || "",
|
|
430
|
+
newHeadRef: newHead.headId || "",
|
|
431
|
+
proofHash: protocolHash("ledger.consistency", {
|
|
432
|
+
oldSize,
|
|
433
|
+
newSize,
|
|
434
|
+
oldRootHash: oldHead.rootHash,
|
|
435
|
+
newRootHash: newHead.rootHash,
|
|
436
|
+
auditPath
|
|
437
|
+
})
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function ensureSigningState() {
|
|
442
|
+
if (signer === false) return null;
|
|
443
|
+
if (signingState) return signingState;
|
|
444
|
+
if (signer && signer !== "auto") {
|
|
445
|
+
const manifest = createVerifierManifest(verifierManifest || signer.manifest || {
|
|
446
|
+
signers: [{
|
|
447
|
+
signerId: signer.signerId || "pactium-ledger-signer",
|
|
448
|
+
algorithm: "ed25519",
|
|
449
|
+
publicKey: signer.publicKey,
|
|
450
|
+
roles: ["ledger-head"]
|
|
451
|
+
}]
|
|
452
|
+
});
|
|
453
|
+
signingState = {
|
|
454
|
+
signerId: signer.signerId || manifest.signers[0]?.signerId || "pactium-ledger-signer",
|
|
455
|
+
privateKey: signer.privateKey,
|
|
456
|
+
manifest
|
|
457
|
+
};
|
|
458
|
+
await storage.putProtocolObject("ledger", "verifier-manifest-current", manifest);
|
|
459
|
+
return signingState;
|
|
460
|
+
}
|
|
461
|
+
const stored = await storage.getProtocolObject("ledger-signer", "default", null);
|
|
462
|
+
if (stored) {
|
|
463
|
+
signingState = stored;
|
|
464
|
+
return signingState;
|
|
465
|
+
}
|
|
466
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
467
|
+
const signerId = createId("ledger_signer", { ledgerId, publicKey: publicKey.export({ type: "spki", format: "pem" }) });
|
|
468
|
+
const manifest = createVerifierManifest({
|
|
469
|
+
signers: [{
|
|
470
|
+
signerId,
|
|
471
|
+
algorithm: "ed25519",
|
|
472
|
+
publicKey: publicKey.export({ type: "spki", format: "pem" }),
|
|
473
|
+
roles: ["ledger-head"]
|
|
474
|
+
}],
|
|
475
|
+
quorum: 1
|
|
476
|
+
});
|
|
477
|
+
signingState = {
|
|
478
|
+
protocol: PACTIUM_PROTOCOL,
|
|
479
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
480
|
+
signerType: "pactium.local-ledger-signer",
|
|
481
|
+
signerId,
|
|
482
|
+
algorithm: "ed25519",
|
|
483
|
+
privateKey: privateKey.export({ type: "pkcs8", format: "pem" }),
|
|
484
|
+
publicKey: publicKey.export({ type: "spki", format: "pem" }),
|
|
485
|
+
manifest
|
|
486
|
+
};
|
|
487
|
+
await storage.putProtocolObject("ledger-signer", "default", signingState);
|
|
488
|
+
await storage.putProtocolObject("ledger", "verifier-manifest-current", manifest);
|
|
489
|
+
return signingState;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function signHead(head) {
|
|
493
|
+
const state = await ensureSigningState();
|
|
494
|
+
if (!state) return head;
|
|
495
|
+
const signature = signLedgerHead(head, {
|
|
496
|
+
privateKey: state.privateKey,
|
|
497
|
+
signerId: state.signerId,
|
|
498
|
+
manifest: state.manifest
|
|
499
|
+
});
|
|
500
|
+
const signatureBlock = await storage.putBlock(signature, { kind: "ledger-head-signature" });
|
|
501
|
+
return {
|
|
502
|
+
...head,
|
|
503
|
+
signatureRef: signatureBlock.cid,
|
|
504
|
+
signatureHash: signatureBlock.payloadHash,
|
|
505
|
+
verifierManifest: state.manifest,
|
|
506
|
+
verifierManifestRef: state.manifest.manifestId,
|
|
507
|
+
signatures: [signature]
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function mergeLeafIntoCompactRange(entry) {
|
|
512
|
+
let cursor = {
|
|
513
|
+
level: 0,
|
|
514
|
+
index: entry.index,
|
|
515
|
+
hash: entry.leafHash,
|
|
516
|
+
size: 1,
|
|
517
|
+
leafRef: `ledger/leaf/${entry.index}`
|
|
518
|
+
};
|
|
519
|
+
const peaks = [...asArray(compactRange.peaks)];
|
|
520
|
+
while (peaks.length > 0 && Number(peaks[peaks.length - 1].level || 0) === Number(cursor.level || 0)) {
|
|
521
|
+
const left = peaks.pop();
|
|
522
|
+
const right = cursor;
|
|
523
|
+
const parentLevel = Number(left.level || 0) + 1;
|
|
524
|
+
const parentIndex = Math.floor(Number(left.index || 0) / 2);
|
|
525
|
+
const hash = ledgerNodeHash(left.hash, right.hash);
|
|
526
|
+
const node = {
|
|
527
|
+
protocol: PACTIUM_PROTOCOL,
|
|
528
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
529
|
+
nodeType: "pactium.ledger.node",
|
|
530
|
+
level: parentLevel,
|
|
531
|
+
index: parentIndex,
|
|
532
|
+
hash,
|
|
533
|
+
leftRef: left.nodeRef || left.leafRef || "",
|
|
534
|
+
rightRef: right.nodeRef || right.leafRef || "",
|
|
535
|
+
leftHash: left.hash,
|
|
536
|
+
rightHash: right.hash,
|
|
537
|
+
size: levelSize(parentLevel),
|
|
538
|
+
createdAt: entry.timestamp || nowIso()
|
|
539
|
+
};
|
|
540
|
+
await writeNodeRecord(node);
|
|
541
|
+
cursor = {
|
|
542
|
+
level: parentLevel,
|
|
543
|
+
index: parentIndex,
|
|
544
|
+
hash,
|
|
545
|
+
size: node.size,
|
|
546
|
+
nodeRef: `ledger/node/${parentLevel}/${parentIndex}`
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
peaks.push(cursor);
|
|
550
|
+
compactRange = createCompactRange({ size: Number(compactRange.size || 0) + 1, peaks });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function persistHead(head) {
|
|
554
|
+
await storage.putProtocolObject("ledger", "head-current", head);
|
|
555
|
+
await storage.putProtocolObject("ledger-head", head.headId, head);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function append(fact, { timestamp = nowIso() } = {}) {
|
|
559
|
+
await load();
|
|
560
|
+
const run = appendLane.then(async () => {
|
|
561
|
+
const previousHead = currentHead;
|
|
562
|
+
const previousHeadRef = previousHead.headId || "";
|
|
563
|
+
const index = Number(currentHead.size || 0);
|
|
564
|
+
const factBlock = await storage.putBlock(fact, { kind: "ledger-fact" });
|
|
565
|
+
const leaf = {
|
|
566
|
+
protocol: PACTIUM_PROTOCOL,
|
|
567
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
568
|
+
index,
|
|
569
|
+
factType: fact.factType,
|
|
570
|
+
factCid: factBlock.cid,
|
|
571
|
+
factHash: factBlock.payloadHash,
|
|
572
|
+
timestamp
|
|
573
|
+
};
|
|
574
|
+
const leafHash = ledgerLeafHash(leaf);
|
|
575
|
+
const eventId = createId("ledger_event", { index, leafHash });
|
|
576
|
+
const entry = {
|
|
577
|
+
protocol: PACTIUM_PROTOCOL,
|
|
578
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
579
|
+
eventId,
|
|
580
|
+
index,
|
|
581
|
+
fact,
|
|
582
|
+
factCid: factBlock.cid,
|
|
583
|
+
factHash: factBlock.payloadHash,
|
|
584
|
+
leaf,
|
|
585
|
+
leafHash,
|
|
586
|
+
timestamp
|
|
587
|
+
};
|
|
588
|
+
entries[index] = entry;
|
|
589
|
+
await storage.putProtocolObject("ledger-leaf", String(index), entry);
|
|
590
|
+
await mergeLeafIntoCompactRange(entry);
|
|
591
|
+
currentHead = ledgerHeadFromCompactRange({
|
|
592
|
+
peaks: compactRange.peaks,
|
|
593
|
+
size: index + 1,
|
|
594
|
+
previousHeadId: previousHeadRef,
|
|
595
|
+
createdAt: timestamp,
|
|
596
|
+
ledgerId
|
|
597
|
+
});
|
|
598
|
+
currentHead = await signHead(currentHead);
|
|
599
|
+
await storage.putProtocolObject("ledger", "compact-range-current", compactRange);
|
|
600
|
+
await persistHead(currentHead);
|
|
601
|
+
return {
|
|
602
|
+
protocol: PACTIUM_PROTOCOL,
|
|
603
|
+
entry,
|
|
604
|
+
head: currentHead,
|
|
605
|
+
previousHead,
|
|
606
|
+
inclusionProof: await createStoredInclusionProof({
|
|
607
|
+
index,
|
|
608
|
+
leaf,
|
|
609
|
+
head: currentHead
|
|
610
|
+
}),
|
|
611
|
+
consistencyProof: await createStoredConsistencyProof({
|
|
612
|
+
oldHead: previousHead,
|
|
613
|
+
newHead: currentHead
|
|
614
|
+
})
|
|
615
|
+
};
|
|
616
|
+
});
|
|
617
|
+
appendLane = run.catch(() => null);
|
|
618
|
+
return run;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return Object.freeze({
|
|
622
|
+
protocol: PACTIUM_PROTOCOL,
|
|
623
|
+
append,
|
|
624
|
+
reload,
|
|
625
|
+
async head() {
|
|
626
|
+
await load();
|
|
627
|
+
return currentHead;
|
|
628
|
+
},
|
|
629
|
+
async getHead(id = "current") {
|
|
630
|
+
await load();
|
|
631
|
+
if (!id || id === "current") return currentHead;
|
|
632
|
+
return storage.getProtocolObject("ledger-head", id, null);
|
|
633
|
+
},
|
|
634
|
+
async getLeaf(index) {
|
|
635
|
+
await load();
|
|
636
|
+
return readLeafRecord(index);
|
|
637
|
+
},
|
|
638
|
+
async compactRange() {
|
|
639
|
+
await load();
|
|
640
|
+
return compactRange;
|
|
641
|
+
},
|
|
642
|
+
async entries() {
|
|
643
|
+
await load();
|
|
644
|
+
const output = [];
|
|
645
|
+
for (let index = 0; index < Number(currentHead.size || 0); index += 1) {
|
|
646
|
+
output.push({ ...await requireLeafRecord(index) });
|
|
647
|
+
}
|
|
648
|
+
return output;
|
|
649
|
+
},
|
|
650
|
+
async pageEntries({ start = 0, limit = 100 } = {}) {
|
|
651
|
+
await load();
|
|
652
|
+
const normalizedStart = Math.max(0, Number(start || 0));
|
|
653
|
+
const pageLimit = Math.max(1, Math.min(Number(limit || 100), 10000));
|
|
654
|
+
const head = currentHead;
|
|
655
|
+
const entriesPage = [];
|
|
656
|
+
const end = Math.min(Number(head.size || 0), normalizedStart + pageLimit);
|
|
657
|
+
for (let index = normalizedStart; index < end; index += 1) {
|
|
658
|
+
entriesPage.push({ ...await requireLeafRecord(index) });
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
protocol: PACTIUM_PROTOCOL,
|
|
662
|
+
start: normalizedStart,
|
|
663
|
+
limit: pageLimit,
|
|
664
|
+
entries: entriesPage,
|
|
665
|
+
nextPosition: entriesPage.length > 0 ? Number(entriesPage[entriesPage.length - 1].index || 0) + 1 : normalizedStart,
|
|
666
|
+
head
|
|
667
|
+
};
|
|
668
|
+
},
|
|
669
|
+
async getEntry(eventId) {
|
|
670
|
+
await load();
|
|
671
|
+
for (let index = 0; index < Number(currentHead.size || 0); index += 1) {
|
|
672
|
+
const entry = await requireLeafRecord(index);
|
|
673
|
+
if (entry.eventId === eventId) return entry;
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
},
|
|
677
|
+
async verifierManifest() {
|
|
678
|
+
await load();
|
|
679
|
+
const state = await ensureSigningState();
|
|
680
|
+
return state?.manifest || storage.getProtocolObject("ledger", "verifier-manifest-current", null);
|
|
681
|
+
},
|
|
682
|
+
async createInclusionProof(index, head = null) {
|
|
683
|
+
await load();
|
|
684
|
+
const leafRecord = await readLeafRecord(Number(index || 0));
|
|
685
|
+
if (!leafRecord) throw new RangeError("Ledger inclusion proof index is out of range.");
|
|
686
|
+
return createStoredInclusionProof({
|
|
687
|
+
index: Number(index || 0),
|
|
688
|
+
leaf: leafRecord.leaf,
|
|
689
|
+
head: head || currentHead
|
|
690
|
+
});
|
|
691
|
+
},
|
|
692
|
+
async createConsistencyProof(oldHead, newHead = null) {
|
|
693
|
+
await load();
|
|
694
|
+
return createStoredConsistencyProof({
|
|
695
|
+
oldHead,
|
|
696
|
+
newHead: newHead || currentHead
|
|
697
|
+
});
|
|
698
|
+
},
|
|
699
|
+
verifyInclusion: verifyLedgerInclusionProof,
|
|
700
|
+
verifyConsistency: verifyLedgerConsistencyProof
|
|
701
|
+
});
|
|
702
|
+
}
|