UNPKG

merkletreejs

Version:
437 lines (436 loc) 15.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MerkleMountainRange = void 0; const buffer_1 = require("buffer"); const sha256_1 = __importDefault(require("crypto-js/sha256")); const Base_1 = __importDefault(require("./Base")); // @credit: https://github.com/wanseob/solidity-mmr /** * @desc The index of this MMR implementation starts from 1 not 0. */ class MerkleMountainRange extends Base_1.default { constructor(hashFn = sha256_1.default, leaves = [], hashLeafFn, peakBaggingFn, hashBranchFn) { super(); this.root = buffer_1.Buffer.alloc(0); this.size = 0; this.width = 0; this.hashes = {}; this.data = {}; leaves = leaves.map(this.bufferify); this.hashFn = this.bufferifyFn(hashFn); this.hashLeafFn = hashLeafFn; this.peakBaggingFn = peakBaggingFn; this.hashBranchFn = hashBranchFn; for (const leaf of leaves) { this.append(leaf); } } /** * @desc This only stores the hashed value of the leaf. * If you need to retrieve the detail data later, use a map to store them. */ append(data) { data = this.bufferify(data); const dataHash = this.hashFn(data); const dataHashHex = this.bufferToHex(dataHash); if (!this.data[dataHashHex] || this.bufferToHex(this.hashFn(this.data[dataHashHex])) !== dataHashHex) { this.data[dataHashHex] = data; } const leaf = this.hashLeaf(this.size + 1, dataHash); this.hashes[this.size + 1] = leaf; this.width += 1; // find peaks for enlarged tree const peakIndexes = this.getPeakIndexes(this.width); // the right most peak's value is the new size of the updated tree this.size = this.getSize(this.width); // starting from the left-most peak, get all peak hashes const peaks = []; for (let i = 0; i < peakIndexes.length; i++) { peaks[i] = this._getOrCreateNode(peakIndexes[i]); } // update the tree root hash this.root = this.peakBagging(this.width, peaks); } /** * @desc It returns the hash of a leaf node with hash(M | DATA ) * M is the index of the node. */ hashLeaf(index, dataHash) { dataHash = this.bufferify(dataHash); if (this.hashLeafFn) { return this.bufferify(this.hashLeafFn(index, dataHash)); } return this.hashFn(buffer_1.Buffer.concat([this.bufferify(index), dataHash])); } /** * @desc It returns the hash a parent node with hash(M | Left child | Right child) * M is the index of the node. */ hashBranch(index, left, right) { if (this.hashBranchFn) { return this.bufferify(this.hashBranchFn(index, left, right)); } return this.hashFn(buffer_1.Buffer.concat([this.bufferify(index), this.bufferify(left), this.bufferify(right)])); } getPeaks() { const peakIndexes = this.getPeakIndexes(this.width); const peaks = []; for (let i = 0; i < peakIndexes.length; i++) { peaks[i] = this.hashes[peakIndexes[i]]; } return peaks; } getLeafIndex(width) { if (width % 2 === 1) { return this.getSize(width); } return this.getSize(width - 1) + 1; } /** * @desc It returns all peaks of the smallest merkle mountain range tree which includes * the given index(size). */ getPeakIndexes(width) { const numPeaks = this.numOfPeaks(width); const peakIndexes = []; let count = 0; let size = 0; for (let i = 255; i > 0; i--) { if ((width & (1 << (i - 1))) !== 0) { // peak exists size = size + (1 << i) - 1; peakIndexes[count++] = size; if (peakIndexes.length >= numPeaks) { break; } } } if (count !== peakIndexes.length) { throw new Error('invalid bit calculation'); } return peakIndexes; } numOfPeaks(width) { let bits = width; let num = 0; while (bits > 0) { if (bits % 2 === 1) { num++; } bits = bits >> 1; } return num; } peakBagging(width, peaks) { const size = this.getSize(width); if (this.numOfPeaks(width) !== peaks.length) { throw new Error('received invalid number of peaks'); } if (width === 0 && !peaks.length) { return buffer_1.Buffer.alloc(0); } if (this.peakBaggingFn) { return this.bufferify(this.peakBaggingFn(size, peaks)); } return this.hashFn(buffer_1.Buffer.concat([this.bufferify(size), ...peaks.map(this.bufferify)])); } /** * @desc It returns the size of the tree. */ getSize(width) { return (width << 1) - this.numOfPeaks(width); } /** * @desc It returns the root value of the tree. */ getRoot() { return this.root; } getHexRoot() { return this.bufferToHex(this.getRoot()); } /** * @dev It returns the hash value of a node for the given position. Note that the index starts from 1. */ getNode(index) { return this.hashes[index]; } /** * @desc It returns the height of the highest peak. */ mountainHeight(size) { let height = 1; while (1 << height <= size + height) { height++; } return height - 1; } /** * @desc It returns the height of the index. */ heightAt(index) { let reducedIndex = index; let peakIndex = 0; let height = 0; // if an index has a left mountain then subtract the mountain while (reducedIndex > peakIndex) { reducedIndex -= (1 << height) - 1; height = this.mountainHeight(reducedIndex); peakIndex = (1 << height) - 1; } // index is on the right slope return height - (peakIndex - reducedIndex); } /** * @desc It returns whether the index is the leaf node or not */ isLeaf(index) { return this.heightAt(index) === 1; } /** * @desc It returns the children when it is a parent node. */ getChildren(index) { const left = index - (1 << (this.heightAt(index) - 1)); const right = index - 1; if (left === right) { throw new Error('not a parent'); } return [left, right]; } /** * @desc It returns a merkle proof for a leaf. Note that the index starts from 1. */ getMerkleProof(index) { if (index > this.size) { throw new Error('out of range'); } if (!this.isLeaf(index)) { throw new Error('not a leaf'); } const root = this.root; const width = this.width; // find all peaks for bagging const peaks = this.getPeakIndexes(this.width); const peakBagging = []; let cursor = 0; for (let i = 0; i < peaks.length; i++) { // collect the hash of all peaks peakBagging[i] = this.hashes[peaks[i]]; // find the peak which includes the target index if (peaks[i] >= index && cursor === 0) { cursor = peaks[i]; } } let left = 0; let right = 0; // get hashes of the siblings in the mountain which the index belgons to. // it moves the cursor from the summit of the mountain down to the target index let height = this.heightAt(cursor); const siblings = []; while (cursor !== index) { height--; ([left, right] = this.getChildren(cursor)); // move the cursor down to the left size or right size cursor = index <= left ? left : right; // remaining node is the sibling siblings[height - 1] = this.hashes[index <= left ? right : left]; } return { root, width, peakBagging, siblings }; } /** * @desc It returns true when the given params verifies that the given value exists in the tree or reverts the transaction. */ verify(root, width, index, value, peaks, siblings) { value = this.bufferify(value); const size = this.getSize(width); if (size < index) { throw new Error('index is out of range'); } // check the root equals the peak bagging hash if (!root.equals(this.peakBagging(width, peaks))) { throw new Error('invalid root hash from the peaks'); } // find the mountain where the target index belongs to let cursor = 0; let targetPeak; const peakIndexes = this.getPeakIndexes(width); for (let i = 0; i < peakIndexes.length; i++) { if (peakIndexes[i] >= index) { targetPeak = peaks[i]; cursor = peakIndexes[i]; break; } } if (!targetPeak) { throw new Error('target not found'); } // find the path climbing down let height = siblings.length + 1; const path = new Array(height); let left = 0; let right = 0; while (height > 0) { // record the current cursor and climb down path[--height] = cursor; if (cursor === index) { // on the leaf node. Stop climbing down break; } else { // on the parent node. Go left or right ([left, right] = this.getChildren(cursor)); cursor = index > left ? right : left; continue; } } // calculate the summit hash climbing up again let node; while (height < path.length) { // move cursor cursor = path[height]; if (height === 0) { // cusor is on the leaf node = this.hashLeaf(cursor, this.hashFn(value)); } else if (cursor - 1 === path[height - 1]) { // cursor is on a parent and a siblings is on the left node = this.hashBranch(cursor, siblings[height - 1], node); } else { // cursor is on a parent and a siblings is on the right node = this.hashBranch(cursor, node, siblings[height - 1]); } // climb up height++; } // computed hash value of the summit should equal to the target peak hash if (!node.equals(targetPeak)) { throw new Error('hashed peak is invalid'); } return true; } peaksToPeakMap(width, peaks) { const peakMap = {}; let bitIndex = 0; let peakRef = 0; let count = peaks.length; for (let height = 1; height <= 32; height++) { // index starts from the right most bit bitIndex = 32 - height; peakRef = 1 << (height - 1); if ((width & peakRef) !== 0) { peakMap[bitIndex] = peaks[--count]; } else { peakMap[bitIndex] = 0; } } if (count !== 0) { throw new Error('invalid number of peaks'); } return peakMap; } peakMapToPeaks(width, peakMap) { const arrLength = this.numOfPeaks(width); const peaks = new Array(arrLength); let count = 0; for (let i = 0; i < 32; i++) { if (peakMap[i] !== 0) { peaks[count++] = peakMap[i]; } } if (count !== arrLength) { throw new Error('invalid number of peaks'); } return peaks; } peakUpdate(width, prevPeakMap, itemHash) { const nextPeakMap = {}; const newWidth = width + 1; let cursorIndex = this.getLeafIndex(newWidth); let cursorNode = this.hashLeaf(cursorIndex, itemHash); let bitIndex = 0; let peakRef = 0; let prevPeakExist = false; let nextPeakExist = false; let obtained = false; for (let height = 1; height <= 32; height++) { // index starts from the right most bit bitIndex = 32 - height; if (obtained) { nextPeakMap[bitIndex] = prevPeakMap[bitIndex]; } else { peakRef = 1 << (height - 1); prevPeakExist = (width & peakRef) !== 0; nextPeakExist = (newWidth & peakRef) !== 0; // get new cursor node with hashing the peak and the current cursor cursorIndex++; if (prevPeakExist) { cursorNode = this.hashBranch(cursorIndex, prevPeakMap[bitIndex], cursorNode); } // if new peak exists for the bit index if (nextPeakExist) { // if prev peak exists for the bit index if (prevPeakExist) { nextPeakMap[bitIndex] = prevPeakMap[bitIndex]; } else { nextPeakMap[bitIndex] = cursorNode; } obtained = true; } else { nextPeakMap[bitIndex] = 0; } } } return nextPeakMap; } rollUp(root, width, peaks, itemHashes) { // check the root equals the peak bagging hash if (!root.equals(this.peakBagging(width, peaks))) { throw new Error('invalid root hash from the peaks'); } let tmpWidth = width; let tmpPeakMap = this.peaksToPeakMap(width, peaks); for (let i = 0; i < itemHashes.length; i++) { tmpPeakMap = this.peakUpdate(tmpWidth, tmpPeakMap, itemHashes[i]); tmpWidth++; } return this.peakBagging(tmpWidth, this.peakMapToPeaks(tmpWidth, tmpPeakMap)); } /** * @desc It returns the hash value of the node for the index. * If the hash already exists it simply returns the stored value. On the other hand, * it computes hashes recursively downward. * Only appending an item calls this function. */ _getOrCreateNode(index) { if (index > this.size) { throw new Error('out of range'); } if (!this.hashes[index]) { const [leftIndex, rightIndex] = this.getChildren(index); const leftHash = this._getOrCreateNode(leftIndex); const rightHash = this._getOrCreateNode(rightIndex); this.hashes[index] = this.hashBranch(index, leftHash, rightHash); } return this.hashes[index]; } } exports.MerkleMountainRange = MerkleMountainRange; if (typeof window !== 'undefined') { ; window.MerkleMountainRange = MerkleMountainRange; } exports.default = MerkleMountainRange;