@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.
@@ -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
+ }