@unrdf/receipts 26.4.2
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/package.json +69 -0
- package/src/batch-receipt-generator.mjs +308 -0
- package/src/dilithium3.mjs +274 -0
- package/src/hybrid-signature.mjs +307 -0
- package/src/index.mjs +64 -0
- package/src/merkle-batcher.mjs +395 -0
- package/src/pq-merkle.mjs +438 -0
- package/src/pq-signer.mjs +381 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KGC Receipts - Merkle Tree Batcher
|
|
3
|
+
* Implements Merkle tree construction for batch receipt verification
|
|
4
|
+
*
|
|
5
|
+
* @module @unrdf/receipts/merkle-batcher
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { blake3 } from 'hash-wasm';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Merkle Node Schema
|
|
13
|
+
*/
|
|
14
|
+
const MerkleNodeSchema = z.object({
|
|
15
|
+
hash: z.string().regex(/^[a-f0-9]{64}$/), // BLAKE3 hash (64 hex chars)
|
|
16
|
+
left: z.any().optional(), // Left child (recursive)
|
|
17
|
+
right: z.any().optional(), // Right child (recursive)
|
|
18
|
+
data: z.any().optional(), // Leaf data (if leaf node)
|
|
19
|
+
index: z.number().int().nonnegative(), // Node index
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Merkle Proof Schema
|
|
24
|
+
*/
|
|
25
|
+
const MerkleProofSchema = z.object({
|
|
26
|
+
leaf: z.string(), // Leaf hash
|
|
27
|
+
index: z.number().int().nonnegative(), // Leaf index
|
|
28
|
+
proof: z.array(z.object({ // Proof path
|
|
29
|
+
hash: z.string(), // Sibling hash
|
|
30
|
+
position: z.enum(['left', 'right']), // Sibling position
|
|
31
|
+
})),
|
|
32
|
+
root: z.string(), // Merkle root
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compute Leaf Hash
|
|
37
|
+
* Hashes leaf data using BLAKE3
|
|
38
|
+
*
|
|
39
|
+
* @param {*} data - Leaf data (will be JSON serialized)
|
|
40
|
+
* @returns {Promise<string>} BLAKE3 hash
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const hash = await computeLeafHash({ subject: 'ex:s1', ... });
|
|
44
|
+
* console.assert(hash.length === 64);
|
|
45
|
+
*/
|
|
46
|
+
async function computeLeafHash(data) {
|
|
47
|
+
const serialized = JSON.stringify(data, (key, value) =>
|
|
48
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
49
|
+
);
|
|
50
|
+
return blake3(serialized);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compute Parent Hash
|
|
55
|
+
* Combines two child hashes to create parent hash
|
|
56
|
+
*
|
|
57
|
+
* @param {string} leftHash - Left child hash
|
|
58
|
+
* @param {string} rightHash - Right child hash
|
|
59
|
+
* @returns {Promise<string>} Parent hash
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const parent = await computeParentHash(hashA, hashB);
|
|
63
|
+
*/
|
|
64
|
+
async function computeParentHash(leftHash, rightHash) {
|
|
65
|
+
return blake3(leftHash + rightHash);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build Merkle Tree
|
|
70
|
+
* Constructs complete Merkle tree from array of data
|
|
71
|
+
*
|
|
72
|
+
* @param {Array<*>} data - Array of leaf data
|
|
73
|
+
* @returns {Promise<Object>} Merkle tree root node
|
|
74
|
+
* @throws {Error} If data is empty or invalid
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* import { buildMerkleTree } from './merkle-batcher.mjs';
|
|
78
|
+
* const tree = await buildMerkleTree([
|
|
79
|
+
* { subject: 'ex:s1', predicate: 'ex:p1', object: 'ex:o1' },
|
|
80
|
+
* { subject: 'ex:s2', predicate: 'ex:p2', object: 'ex:o2' },
|
|
81
|
+
* ]);
|
|
82
|
+
* console.log('Merkle root:', tree.hash);
|
|
83
|
+
*/
|
|
84
|
+
export async function buildMerkleTree(data) {
|
|
85
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
86
|
+
throw new TypeError('buildMerkleTree: data must be non-empty array');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Build leaf nodes
|
|
90
|
+
const leaves = await Promise.all(
|
|
91
|
+
data.map(async (item, index) => ({
|
|
92
|
+
hash: await computeLeafHash(item),
|
|
93
|
+
data: item,
|
|
94
|
+
index,
|
|
95
|
+
}))
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Build tree bottom-up
|
|
99
|
+
let currentLevel = leaves;
|
|
100
|
+
|
|
101
|
+
while (currentLevel.length > 1) {
|
|
102
|
+
const nextLevel = [];
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < currentLevel.length; i += 2) {
|
|
105
|
+
const left = currentLevel[i];
|
|
106
|
+
const right = currentLevel[i + 1];
|
|
107
|
+
|
|
108
|
+
if (right) {
|
|
109
|
+
// Two children - compute parent
|
|
110
|
+
const parentHash = await computeParentHash(left.hash, right.hash);
|
|
111
|
+
nextLevel.push({
|
|
112
|
+
hash: parentHash,
|
|
113
|
+
left,
|
|
114
|
+
right,
|
|
115
|
+
index: Math.floor(i / 2),
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
// Odd number - promote single node (or duplicate for complete binary tree)
|
|
119
|
+
// Using duplication for complete binary tree
|
|
120
|
+
const parentHash = await computeParentHash(left.hash, left.hash);
|
|
121
|
+
nextLevel.push({
|
|
122
|
+
hash: parentHash,
|
|
123
|
+
left,
|
|
124
|
+
right: left, // Duplicate
|
|
125
|
+
index: Math.floor(i / 2),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
currentLevel = nextLevel;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Return root
|
|
134
|
+
return currentLevel[0];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate Merkle Proof
|
|
139
|
+
* Creates proof path from leaf to root
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} tree - Merkle tree root
|
|
142
|
+
* @param {number} leafIndex - Index of leaf to prove
|
|
143
|
+
* @returns {Object} Merkle proof
|
|
144
|
+
* @throws {Error} If leaf index invalid
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* const proof = generateMerkleProof(tree, 1);
|
|
148
|
+
* console.log('Proof path length:', proof.proof.length);
|
|
149
|
+
*/
|
|
150
|
+
export function generateMerkleProof(tree, leafIndex) {
|
|
151
|
+
if (typeof leafIndex !== 'number' || leafIndex < 0) {
|
|
152
|
+
throw new TypeError('generateMerkleProof: leafIndex must be non-negative number');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const proof = [];
|
|
156
|
+
let currentNode = tree;
|
|
157
|
+
let currentIndex = leafIndex;
|
|
158
|
+
|
|
159
|
+
// Find leaf
|
|
160
|
+
let leaf = null;
|
|
161
|
+
function findLeaf(node) {
|
|
162
|
+
if (node.data !== undefined && node.index === leafIndex) {
|
|
163
|
+
leaf = node;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
if (node.left && findLeaf(node.left)) return true;
|
|
167
|
+
if (node.right && findLeaf(node.right)) return true;
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
findLeaf(tree);
|
|
172
|
+
|
|
173
|
+
if (!leaf) {
|
|
174
|
+
throw new Error(`generateMerkleProof: Leaf at index ${leafIndex} not found`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Build proof path
|
|
178
|
+
function buildProof(node, targetIndex) {
|
|
179
|
+
if (node.data !== undefined) {
|
|
180
|
+
// Reached leaf
|
|
181
|
+
return node.index === targetIndex;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const leftMatch = node.left && buildProof(node.left, targetIndex);
|
|
185
|
+
const rightMatch = node.right && buildProof(node.right, targetIndex);
|
|
186
|
+
|
|
187
|
+
if (leftMatch) {
|
|
188
|
+
// Target in left subtree - add right sibling to proof
|
|
189
|
+
if (node.right && node.right !== node.left) {
|
|
190
|
+
proof.push({
|
|
191
|
+
hash: node.right.hash,
|
|
192
|
+
position: 'right',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (rightMatch) {
|
|
199
|
+
// Target in right subtree - add left sibling to proof
|
|
200
|
+
if (node.left && node.left !== node.right) {
|
|
201
|
+
proof.push({
|
|
202
|
+
hash: node.left.hash,
|
|
203
|
+
position: 'left',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
buildProof(tree, leafIndex);
|
|
213
|
+
|
|
214
|
+
const result = {
|
|
215
|
+
leaf: leaf.hash,
|
|
216
|
+
index: leafIndex,
|
|
217
|
+
proof,
|
|
218
|
+
root: tree.hash,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Validate proof schema
|
|
222
|
+
MerkleProofSchema.parse(result);
|
|
223
|
+
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Verify Merkle Proof
|
|
229
|
+
* Verifies a Merkle proof against a root hash
|
|
230
|
+
*
|
|
231
|
+
* @param {Object} proof - Merkle proof
|
|
232
|
+
* @param {string} leafHash - Leaf hash to verify
|
|
233
|
+
* @returns {Promise<boolean>} True if proof is valid
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* const valid = await verifyMerkleProof(proof, leafHash);
|
|
237
|
+
* console.log('Proof valid:', valid);
|
|
238
|
+
*/
|
|
239
|
+
export async function verifyMerkleProof(proof, leafHash) {
|
|
240
|
+
// Validate proof schema
|
|
241
|
+
try {
|
|
242
|
+
MerkleProofSchema.parse(proof);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Verify leaf hash matches
|
|
248
|
+
if (proof.leaf !== leafHash) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Compute root by following proof path
|
|
253
|
+
let currentHash = leafHash;
|
|
254
|
+
|
|
255
|
+
for (const step of proof.proof) {
|
|
256
|
+
if (step.position === 'left') {
|
|
257
|
+
// Sibling is on left
|
|
258
|
+
currentHash = await computeParentHash(step.hash, currentHash);
|
|
259
|
+
} else {
|
|
260
|
+
// Sibling is on right
|
|
261
|
+
currentHash = await computeParentHash(currentHash, step.hash);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Compare computed root with proof root
|
|
266
|
+
return currentHash === proof.root;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Batch Operations with Merkle Tree
|
|
271
|
+
* Creates batch receipt with Merkle tree for verification
|
|
272
|
+
*
|
|
273
|
+
* @param {Array<Object>} operations - Array of operations
|
|
274
|
+
* @returns {Promise<Object>} Batch with Merkle root and tree
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* import { batchWithMerkleTree } from './merkle-batcher.mjs';
|
|
278
|
+
* const batch = await batchWithMerkleTree(operations);
|
|
279
|
+
* console.log('Merkle root:', batch.merkleRoot);
|
|
280
|
+
* console.log('Tree depth:', calculateDepth(batch.tree));
|
|
281
|
+
*/
|
|
282
|
+
export async function batchWithMerkleTree(operations) {
|
|
283
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
284
|
+
throw new TypeError('batchWithMerkleTree: operations must be non-empty array');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Build Merkle tree
|
|
288
|
+
const tree = await buildMerkleTree(operations);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
operations,
|
|
292
|
+
merkleRoot: tree.hash,
|
|
293
|
+
tree,
|
|
294
|
+
batchSize: operations.length,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Verify Operation in Batch
|
|
300
|
+
* Verifies single operation is included in batch using Merkle proof
|
|
301
|
+
*
|
|
302
|
+
* @param {Object} operation - Operation to verify
|
|
303
|
+
* @param {number} index - Index of operation in batch
|
|
304
|
+
* @param {Object} batch - Batch with Merkle tree
|
|
305
|
+
* @returns {Promise<boolean>} True if operation is valid member of batch
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* const valid = await verifyOperationInBatch(operation, 2, batch);
|
|
309
|
+
* console.log('Operation verified:', valid);
|
|
310
|
+
*/
|
|
311
|
+
export async function verifyOperationInBatch(operation, index, batch) {
|
|
312
|
+
if (!batch || !batch.tree) {
|
|
313
|
+
throw new TypeError('verifyOperationInBatch: batch must have tree');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Generate proof for this operation
|
|
317
|
+
const proof = generateMerkleProof(batch.tree, index);
|
|
318
|
+
|
|
319
|
+
// Compute leaf hash
|
|
320
|
+
const leafHash = await computeLeafHash(operation);
|
|
321
|
+
|
|
322
|
+
// Verify proof
|
|
323
|
+
return verifyMerkleProof(proof, leafHash);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get Merkle Root
|
|
328
|
+
* Extracts Merkle root hash from tree
|
|
329
|
+
*
|
|
330
|
+
* @param {Object} tree - Merkle tree
|
|
331
|
+
* @returns {string} Root hash
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* const root = getMerkleRoot(tree);
|
|
335
|
+
* console.assert(root.length === 64);
|
|
336
|
+
*/
|
|
337
|
+
export function getMerkleRoot(tree) {
|
|
338
|
+
if (!tree || !tree.hash) {
|
|
339
|
+
throw new TypeError('getMerkleRoot: tree must have hash property');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return tree.hash;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Calculate Tree Depth
|
|
347
|
+
* Computes depth of Merkle tree
|
|
348
|
+
*
|
|
349
|
+
* @param {Object} tree - Merkle tree
|
|
350
|
+
* @returns {number} Tree depth (0 for single leaf)
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* const depth = calculateTreeDepth(tree);
|
|
354
|
+
* console.log('Tree depth:', depth);
|
|
355
|
+
*/
|
|
356
|
+
export function calculateTreeDepth(tree) {
|
|
357
|
+
if (!tree) return 0;
|
|
358
|
+
|
|
359
|
+
if (tree.data !== undefined) {
|
|
360
|
+
// Leaf node
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const leftDepth = tree.left ? calculateTreeDepth(tree.left) : 0;
|
|
365
|
+
const rightDepth = tree.right ? calculateTreeDepth(tree.right) : 0;
|
|
366
|
+
|
|
367
|
+
return 1 + Math.max(leftDepth, rightDepth);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get Leaf Count
|
|
372
|
+
* Counts number of leaves in tree
|
|
373
|
+
*
|
|
374
|
+
* @param {Object} tree - Merkle tree
|
|
375
|
+
* @returns {number} Number of leaves
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* const count = getLeafCount(tree);
|
|
379
|
+
* console.log('Leaf count:', count);
|
|
380
|
+
*/
|
|
381
|
+
export function getLeafCount(tree) {
|
|
382
|
+
if (!tree) return 0;
|
|
383
|
+
|
|
384
|
+
if (tree.data !== undefined) {
|
|
385
|
+
// Leaf node
|
|
386
|
+
return 1;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const leftCount = tree.left ? getLeafCount(tree.left) : 0;
|
|
390
|
+
const rightCount = tree.right && tree.right !== tree.left
|
|
391
|
+
? getLeafCount(tree.right)
|
|
392
|
+
: 0;
|
|
393
|
+
|
|
394
|
+
return leftCount + rightCount;
|
|
395
|
+
}
|