96 lines
2.6 KiB
JavaScript
96 lines
2.6 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|