/** * Merkle tree over SHA-256 leaf hex digests. * Only the root is sealed on the main Verae chain; leaves live on tree nodes. * @module merkle */ import { createHash } from 'node:crypto'; /** * @param {string|Buffer|Uint8Array} data * @returns {string} lowercase hex */ export function sha256Hex(data) { const buf = Buffer.isBuffer(data) ? data : Buffer.from(String(data), 'utf8'); return createHash('sha256').update(buf).digest('hex'); } /** * Parent of two 32-byte hex children (left || right). * Odd last child is duplicated (Bitcoin-style). * @param {string} left * @param {string} right */ export function parentHash(left, right) { return createHash('sha256') .update(Buffer.from(left, 'hex')) .update(Buffer.from(right, 'hex')) .digest('hex'); } /** * @param {string[]} leaves hex SHA-256 digests * @returns {{ root: string, layers: string[][], leaves: string[] }} */ export function buildMerkle(leaves) { if (!leaves.length) { throw new Error('at least one leaf is required'); } const layer0 = leaves.map((h) => String(h).toLowerCase()); const layers = [layer0]; let layer = layer0; while (layer.length > 1) { const next = []; for (let i = 0; i < layer.length; i += 2) { const left = layer[i]; const right = layer[i + 1] ?? layer[i]; next.push(parentHash(left, right)); } layer = next; layers.push(layer); } return { root: layer[0], layers, leaves: layer0 }; } /** * Inclusion proof for leaf index. * @param {string[][]} layers * @param {number} index * @returns {Array<{ sibling: string, position: 'left'|'right' }>} */ export function merkleProof(layers, index) { const proof = []; let i = index; for (let level = 0; level < layers.length - 1; level += 1) { const layer = layers[level]; const isRight = i % 2 === 1; const sib = isRight ? i - 1 : i + 1; const sibling = layer[sib] ?? layer[i]; proof.push({ sibling, position: isRight ? 'left' : 'right' }); i = Math.floor(i / 2); } return proof; } /** * @param {string} leaf * @param {Array<{ sibling: string, position: 'left'|'right' }>} proof * @param {string} root */ export function verifyProof(leaf, proof, root) { let h = String(leaf).toLowerCase(); for (const step of proof) { h = step.position === 'left' ? parentHash(step.sibling, h) : parentHash(h, step.sibling); } return h === String(root).toLowerCase(); } /** * Shard a leaf onto one of N tree nodes (others stay silent on bloom miss). * @param {string} sha256 * @param {number} n */ export function shardIndex(sha256, n) { const b = Buffer.from(String(sha256).slice(0, 8), 'hex'); return n <= 0 ? 0 : b.readUInt32BE(0) % n; }