UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

383 lines 16.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const utils_js_1 = require("../primitives/utils.js"); const Hash_js_1 = require("../primitives/Hash.js"); /** * Represents a Merkle Path, which is used to provide a compact proof of inclusion for a * transaction in a block. This class encapsulates all the details required for creating * and verifying Merkle Proofs. * * @class MerklePath * @property {number} blockHeight - The height of the block in which the transaction is included. * @property {Array<Array<{offset: number, hash?: string, txid?: boolean, duplicate?: boolean}>>} path - * A tree structure representing the Merkle Path, with each level containing information * about the nodes involved in constructing the proof. * * @example * // Creating and verifying a Merkle Path * const merklePath = MerklePath.fromHex('...'); * const isValid = merklePath.verify(txid, chainTracker); * * @description * The MerklePath class is useful for verifying transactions in a lightweight and efficient manner without * needing the entire block data. This class offers functionalities for creating, converting, * and verifying these proofs. */ class MerklePath { /** * Creates a MerklePath instance from a hexadecimal string. * * @static * @param {string} hex - The hexadecimal string representation of the Merkle Path. * @returns {MerklePath} - A new MerklePath instance. */ static fromHex(hex) { return MerklePath.fromBinary((0, utils_js_1.toArray)(hex, 'hex')); } static fromReader(reader, legalOffsetsOnly = true) { const blockHeight = reader.readVarIntNum(); const treeHeight = reader.readUInt8(); // Explicitly define the type of path as an array of arrays of leaf objects const path = Array(treeHeight) .fill(null) .map(() => []); let flags, offset, nLeavesAtThisHeight; for (let level = 0; level < treeHeight; level++) { nLeavesAtThisHeight = reader.readVarIntNum(); while (nLeavesAtThisHeight > 0) { offset = reader.readVarIntNum(); flags = reader.readUInt8(); const leaf = { offset }; if ((flags & 1) !== 0) { leaf.duplicate = true; } else { if ((flags & 2) !== 0) { leaf.txid = true; } leaf.hash = (0, utils_js_1.toHex)(reader.read(32).reverse()); } // Ensure path[level] exists before pushing if (!Array.isArray(path[level]) || path[level].length === 0) { path[level] = []; } path[level].push(leaf); nLeavesAtThisHeight--; } // Sort the array based on the offset property path[level].sort((a, b) => a.offset - b.offset); } return new MerklePath(blockHeight, path, legalOffsetsOnly); } /** * Creates a MerklePath instance from a binary array. * * @static * @param {number[]} bump - The binary array representation of the Merkle Path. * @returns {MerklePath} - A new MerklePath instance. */ static fromBinary(bump) { const reader = new utils_js_1.Reader(bump); return MerklePath.fromReader(reader); } /** * * @static fromCoinbaseTxid * * Creates a MerklePath instance for a coinbase transaction in an empty block. * This edge case is difficult to retrieve from standard APIs. * * @param {string} txid - The coinbase txid. * @param {number} height - The height of the block. * @returns {MerklePath} - A new MerklePath instance which assumes the tx is in a block with no other transactions. */ static fromCoinbaseTxidAndHeight(txid, height) { return new MerklePath(height, [[{ offset: 0, hash: txid, txid: true }]]); } constructor(blockHeight, path, legalOffsetsOnly = true) { this.blockHeight = blockHeight; this.path = path; // store all of the legal offsets which we expect given the txid indices. const legalOffsets = Array(this.path.length) .fill(0) .map(() => new Set()); this.path.forEach((leaves, height) => { if (leaves.length === 0 && height === 0) { throw new Error(`Empty level at height: ${height}`); } const offsetsAtThisHeight = new Set(); leaves.forEach((leaf) => { if (offsetsAtThisHeight.has(leaf.offset)) { throw new Error(`Duplicate offset: ${leaf.offset}, at height: ${height}`); } offsetsAtThisHeight.add(leaf.offset); if (height === 0) { if (leaf.duplicate !== true) { for (let h = 1; h < this.path.length; h++) { legalOffsets[h].add((leaf.offset >> h) ^ 1); } } } else { if (legalOffsetsOnly && !legalOffsets[height].has(leaf.offset)) { throw new Error(`Invalid offset: ${leaf.offset}, at height: ${height}, with legal offsets: ${Array.from(legalOffsets[height]).join(', ')}`); } } }); }); // every txid must calculate to the same root. let root; this.path[0].forEach((leaf, idx) => { if (idx === 0) root = this.computeRoot(leaf.hash); if (root !== this.computeRoot(leaf.hash)) { throw new Error('Mismatched roots'); } }); } /** * Converts the MerklePath to a binary array format. * * @returns {number[]} - The binary array representation of the Merkle Path. */ toBinary() { const writer = new utils_js_1.Writer(); writer.writeVarIntNum(this.blockHeight); const treeHeight = this.path.length; writer.writeUInt8(treeHeight); for (let level = 0; level < treeHeight; level++) { const nLeaves = Object.keys(this.path[level]).length; writer.writeVarIntNum(nLeaves); for (const leaf of this.path[level]) { writer.writeVarIntNum(leaf.offset); let flags = 0; if (leaf?.duplicate === true) { flags |= 1; } if (leaf?.txid !== undefined && leaf.txid !== null) { flags |= 2; } writer.writeUInt8(flags); if ((flags & 1) === 0) { writer.write((0, utils_js_1.toArray)(leaf.hash, 'hex').reverse()); } } } return writer.toArray(); } /** * Converts the MerklePath to a hexadecimal string format. * * @returns {string} - The hexadecimal string representation of the Merkle Path. */ toHex() { return (0, utils_js_1.toHex)(this.toBinary()); } // indexOf(txid) { const leaf = this.path[0].find((l) => l.hash === txid); if (leaf === null || leaf === undefined) { throw new Error(`Transaction ID ${txid} not found in the Merkle Path`); } return leaf.offset; } /** * Computes the Merkle root from the provided transaction ID. * * @param {string} txid - The transaction ID to compute the Merkle root for. If not provided, the root will be computed from an unspecified branch, and not all branches will be validated! * @returns {string} - The computed Merkle root as a hexadecimal string. * @throws {Error} - If the transaction ID is not part of the Merkle Path. */ computeRoot(txid) { if (typeof txid !== 'string') { const foundLeaf = this.path[0].find((leaf) => Boolean(leaf?.hash)); if (foundLeaf === null || foundLeaf === undefined) { throw new Error('No valid leaf found in the Merkle Path'); } txid = foundLeaf.hash; } // Find the index of the txid at the lowest level of the Merkle tree if (typeof txid !== 'string') { throw new Error('Transaction ID is undefined'); } const index = this.indexOf(txid); if (typeof index !== 'number') { throw new Error(`This proof does not contain the txid: ${txid ?? 'undefined'}`); } // Calculate the root using the index as a way to determine which direction to concatenate. const hash = (m) => (0, utils_js_1.toHex)((0, Hash_js_1.hash256)((0, utils_js_1.toArray)(m, 'hex').reverse()).reverse()); let workingHash = txid; // special case for blocks with only one transaction if (this.path.length === 1 && this.path[0].length === 1) return workingHash; for (let height = 0; height < this.path.length; height++) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const leaves = this.path[height]; const offset = (index >> height) ^ 1; const leaf = this.findOrComputeLeaf(height, offset); if (typeof leaf !== 'object') { throw new Error(`Missing hash for index ${index} at height ${height}`); } if (leaf.duplicate === true) { workingHash = hash((workingHash ?? '') + (workingHash ?? '')); } else if (offset % 2 !== 0) { workingHash = hash((leaf.hash ?? '') + (workingHash ?? '')); } else { workingHash = hash((workingHash ?? '') + (leaf.hash ?? '')); } } return workingHash; } /** * Find leaf with `offset` at `height` or compute from level below, recursively. * * Does not add computed leaves to path. * * @param height * @param offset */ findOrComputeLeaf(height, offset) { const hash = (m) => (0, utils_js_1.toHex)((0, Hash_js_1.hash256)((0, utils_js_1.toArray)(m, 'hex').reverse()).reverse()); let leaf = this.path[height].find((l) => l.offset === offset); if (leaf != null) return leaf; if (height === 0) return undefined; const h = height - 1; const l = offset << 1; const leaf0 = this.findOrComputeLeaf(h, l); if (leaf0 == null || leaf0.hash == null || leaf0.hash === '') return undefined; const leaf1 = this.findOrComputeLeaf(h, l + 1); if (leaf1 == null) return undefined; let workinghash; if (leaf1.duplicate === true) { workinghash = hash(leaf0.hash + leaf0.hash); } else { workinghash = hash((leaf1.hash ?? '') + (leaf0.hash ?? '')); } leaf = { offset, hash: workinghash }; return leaf; } /** * Verifies if the given transaction ID is part of the Merkle tree at the specified block height. * * @param {string} txid - The transaction ID to verify. * @param {ChainTracker} chainTracker - The ChainTracker instance used to verify the Merkle root. * @returns {boolean} - True if the transaction ID is valid within the Merkle Path at the specified block height. */ async verify(txid, chainTracker) { const root = this.computeRoot(txid); if (this.indexOf(txid) === 0) { // Coinbase transaction outputs can only be spent once they're 100 blocks deep. const height = await chainTracker.currentHeight(); if (this.blockHeight + 100 < height) { return false; } } // Use the chain tracker to determine whether this is a valid merkle root at the given block height return await chainTracker.isValidRootForHeight(root, this.blockHeight); } /** * Combines this MerklePath with another to create a compound proof. * * @param {MerklePath} other - Another MerklePath to combine with this path. * @throws {Error} - If the paths have different block heights or roots. */ combine(other) { if (this.blockHeight !== other.blockHeight) { throw new Error('You cannot combine paths which do not have the same block height.'); } const root1 = this.computeRoot(); const root2 = other.computeRoot(); if (root1 !== root2) { throw new Error('You cannot combine paths which do not have the same root.'); } const combinedPath = []; for (let h = 0; h < this.path.length; h++) { combinedPath.push([]); for (let l = 0; l < this.path[h].length; l++) { combinedPath[h].push(this.path[h][l]); } for (let l = 0; l < other.path[h].length; l++) { if (combinedPath[h].find((leaf) => leaf.offset === other.path[h][l].offset) === undefined) { combinedPath[h].push(other.path[h][l]); } else { // Ensure that any elements which appear in both are not downgraded to a non txid. if (other.path[h][l]?.txid !== undefined && other.path[h][l]?.txid !== null) { const target = combinedPath[h].find((leaf) => leaf.offset === other.path[h][l].offset); if (target !== null && target !== undefined) { target.txid = true; } } } } } this.path = combinedPath; this.trim(); } /** * Remove all internal nodes that are not required by level zero txid nodes. * Assumes that at least all required nodes are present. * Leaves all levels sorted by increasing offset. */ trim() { const pushIfNew = (v, a) => { if (a.length === 0 || a.slice(-1)[0] !== v) { a.push(v); } }; const dropOffsetsFromLevel = (dropOffsets, level) => { for (let i = dropOffsets.length; i >= 0; i--) { const l = this.path[level].findIndex((n) => n.offset === dropOffsets[i]); if (l >= 0) { this.path[level].splice(l, 1); } } }; const nextComputedOffsets = (cos) => { const ncos = []; for (const o of cos) { pushIfNew(o >> 1, ncos); } return ncos; }; let computedOffsets = []; // in next level let dropOffsets = []; for (let h = 0; h < this.path.length; h++) { // Sort each level by increasing offset order this.path[h].sort((a, b) => a.offset - b.offset); } for (let l = 0; l < this.path[0].length; l++) { const n = this.path[0][l]; if (n.txid === true) { // level 0 must enable computing level 1 for txid nodes pushIfNew(n.offset >> 1, computedOffsets); } else { const isOdd = n.offset % 2 === 1; const peer = this.path[0][l + (isOdd ? -1 : 1)]; if (peer.txid === undefined || peer.txid === null || !peer.txid) { // drop non-txid level 0 nodes without a txid peer pushIfNew(peer.offset, dropOffsets); } } } dropOffsetsFromLevel(dropOffsets, 0); for (let h = 1; h < this.path.length; h++) { dropOffsets = computedOffsets; computedOffsets = nextComputedOffsets(computedOffsets); dropOffsetsFromLevel(dropOffsets, h); } } } exports.default = MerklePath; //# sourceMappingURL=MerklePath.js.map