diff --git a/lib/BaseTree.d.ts b/lib/BaseTree.d.ts new file mode 100644 index 0000000..d79a711 --- /dev/null +++ b/lib/BaseTree.d.ts @@ -0,0 +1,42 @@ +import { Element, HashFunction, ProofPath } from './index'; +export declare class BaseTree { + levels: number; + protected _hashFn: HashFunction; + protected zeroElement: Element; + protected _zeros: Element[]; + protected _layers: Array; + get capacity(): number; + get layers(): Array; + get zeros(): Element[]; + get elements(): Element[]; + get root(): Element; + /** + * Find an element in the tree + * @param element An element to find + * @param comparator A function that checks leaf value equality + * @returns {number} Index if element is found, otherwise -1 + */ + indexOf(element: Element, comparator?: (arg0: T, arg1: T) => boolean): number; + /** + * Insert new element into the tree + * @param element Element to insert + */ + insert(element: Element): void; + bulkInsert(elements: Element[]): void; + /** + * Change an element in the tree + * @param {number} index Index of element to change + * @param element Updated element value + */ + update(index: number, element: Element): void; + proof(element: Element): ProofPath; + /** + * Get merkle path to a leaf + * @param {number} index Leaf index to generate path for + * @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index + */ + path(index: number): ProofPath; + protected _buildZeros(): void; + protected _processNodes(nodes: Element[], layerIndex: number): any[]; + protected _processUpdate(index: number): void; +} diff --git a/lib/BaseTree.js b/lib/BaseTree.js new file mode 100644 index 0000000..ab6407f --- /dev/null +++ b/lib/BaseTree.js @@ -0,0 +1,156 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BaseTree = void 0; +class BaseTree { + get capacity() { + return 2 ** this.levels; + } + get layers() { + return this._layers.slice(); + } + get zeros() { + return this._zeros.slice(); + } + get elements() { + return this._layers[0].slice(); + } + get root() { + var _a; + return (_a = this._layers[this.levels][0]) !== null && _a !== void 0 ? _a : this._zeros[this.levels]; + } + /** + * Find an element in the tree + * @param element An element to find + * @param comparator A function that checks leaf value equality + * @returns {number} Index if element is found, otherwise -1 + */ + indexOf(element, comparator) { + if (comparator) { + return this._layers[0].findIndex((el) => comparator(element, el)); + } + else { + return this._layers[0].indexOf(element); + } + } + /** + * Insert new element into the tree + * @param element Element to insert + */ + insert(element) { + if (this._layers[0].length >= this.capacity) { + throw new Error('Tree is full'); + } + this.update(this._layers[0].length, element); + } + /* + * Insert multiple elements into the tree. + * @param {Array} elements Elements to insert + */ + bulkInsert(elements) { + if (!elements.length) { + return; + } + if (this._layers[0].length + elements.length > this.capacity) { + throw new Error('Tree is full'); + } + // First we insert all elements except the last one + // updating only full subtree hashes (all layers where inserted element has odd index) + // the last element will update the full path to the root making the tree consistent again + for (let i = 0; i < elements.length - 1; i++) { + this._layers[0].push(elements[i]); + let level = 0; + let index = this._layers[0].length - 1; + while (index % 2 === 1) { + level++; + index >>= 1; + const left = this._layers[level - 1][index * 2]; + const right = this._layers[level - 1][index * 2 + 1]; + this._layers[level][index] = this._hashFn(left, right); + } + } + this.insert(elements[elements.length - 1]); + } + /** + * Change an element in the tree + * @param {number} index Index of element to change + * @param element Updated element value + */ + update(index, element) { + if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) { + throw new Error('Insert index out of bounds: ' + index); + } + this._layers[0][index] = element; + this._processUpdate(index); + } + proof(element) { + const index = this.indexOf(element); + return this.path(index); + } + /** + * Get merkle path to a leaf + * @param {number} index Leaf index to generate path for + * @returns {{pathElements: Object[], pathIndex: number[]}} An object containing adjacent elements and left-right index + */ + path(index) { + if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) { + throw new Error('Index out of bounds: ' + index); + } + let elIndex = +index; + const pathElements = []; + const pathIndices = []; + const pathPositions = []; + for (let level = 0; level < this.levels; level++) { + pathIndices[level] = elIndex % 2; + const leafIndex = elIndex ^ 1; + if (leafIndex < this._layers[level].length) { + pathElements[level] = this._layers[level][leafIndex]; + pathPositions[level] = leafIndex; + } + else { + pathElements[level] = this._zeros[level]; + pathPositions[level] = 0; + } + elIndex >>= 1; + } + return { + pathElements, + pathIndices, + pathPositions, + pathRoot: this.root, + }; + } + _buildZeros() { + this._zeros = [this.zeroElement]; + for (let i = 1; i <= this.levels; i++) { + this._zeros[i] = this._hashFn(this._zeros[i - 1], this._zeros[i - 1]); + } + } + _processNodes(nodes, layerIndex) { + const length = nodes.length; + let currentLength = Math.ceil(length / 2); + const currentLayer = new Array(currentLength); + currentLength--; + const starFrom = length - ((length % 2) ^ 1); + let j = 0; + for (let i = starFrom; i >= 0; i -= 2) { + if (nodes[i - 1] === undefined) + break; + const left = nodes[i - 1]; + const right = (i === starFrom && length % 2 === 1) ? this._zeros[layerIndex - 1] : nodes[i]; + currentLayer[currentLength - j] = this._hashFn(left, right); + j++; + } + return currentLayer; + } + _processUpdate(index) { + for (let level = 1; level <= this.levels; level++) { + index >>= 1; + const left = this._layers[level - 1][index * 2]; + const right = index * 2 + 1 < this._layers[level - 1].length + ? this._layers[level - 1][index * 2 + 1] + : this._zeros[level - 1]; + this._layers[level][index] = this._hashFn(left, right); + } + } +} +exports.BaseTree = BaseTree; diff --git a/lib/FixedMerkleTree.d.ts b/lib/FixedMerkleTree.d.ts new file mode 100644 index 0000000..2c43792 --- /dev/null +++ b/lib/FixedMerkleTree.d.ts @@ -0,0 +1,30 @@ +import { Element, HashFunction, MerkleTreeOptions, SerializedTreeState, TreeEdge, TreeSlice } from './'; +import { BaseTree } from './BaseTree'; +export default class MerkleTree extends BaseTree { + constructor(levels: number, elements?: Element[], { hashFunction, zeroElement, }?: MerkleTreeOptions); + private _buildHashes; + /** + * Insert multiple elements into the tree. + * @param {Array} elements Elements to insert + */ + bulkInsert(elements: Element[]): void; + getTreeEdge(edgeIndex: number): TreeEdge; + /** + * 🪓 + * @param count + */ + getTreeSlices(count?: number): TreeSlice[]; + /** + * Serialize entire tree state including intermediate layers into a plain object + * Deserializing it back will not require to recompute any hashes + * Elements are not converted to a plain type, this is responsibility of the caller + */ + serialize(): SerializedTreeState; + /** + * Deserialize data into a MerkleTree instance + * Make sure to provide the same hashFunction as was used in the source tree, + * otherwise the tree state will be invalid + */ + static deserialize(data: SerializedTreeState, hashFunction?: HashFunction): MerkleTree; + toString(): string; +} diff --git a/lib/FixedMerkleTree.js b/lib/FixedMerkleTree.js new file mode 100644 index 0000000..0f12a93 --- /dev/null +++ b/lib/FixedMerkleTree.js @@ -0,0 +1,104 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const simpleHash_1 = __importDefault(require("./simpleHash")); +const BaseTree_1 = require("./BaseTree"); +class MerkleTree extends BaseTree_1.BaseTree { + constructor(levels, elements = [], { hashFunction = simpleHash_1.default, zeroElement = 0, } = {}) { + super(); + this.levels = levels; + if (elements.length > this.capacity) { + throw new Error('Tree is full'); + } + this._hashFn = hashFunction; + this.zeroElement = zeroElement; + this._layers = []; + const leaves = elements.slice(); + this._layers = [leaves]; + this._buildZeros(); + this._buildHashes(); + } + _buildHashes() { + for (let layerIndex = 1; layerIndex <= this.levels; layerIndex++) { + const nodes = this._layers[layerIndex - 1]; + this._layers[layerIndex] = this._processNodes(nodes, layerIndex); + } + } + /** + * Insert multiple elements into the tree. + * @param {Array} elements Elements to insert + */ + bulkInsert(elements) { + if (!elements.length) { + return; + } + if (this._layers[0].length + elements.length > this.capacity) { + throw new Error('Tree is full'); + } + // First we insert all elements except the last one + // updating only full subtree hashes (all layers where inserted element has odd index) + // the last element will update the full path to the root making the tree consistent again + for (let i = 0; i < elements.length - 1; i++) { + this._layers[0].push(elements[i]); + let level = 0; + let index = this._layers[0].length - 1; + while (index % 2 === 1) { + level++; + index >>= 1; + this._layers[level][index] = this._hashFn(this._layers[level - 1][index * 2], this._layers[level - 1][index * 2 + 1]); + } + } + this.insert(elements[elements.length - 1]); + } + getTreeEdge(edgeIndex) { + const edgeElement = this._layers[0][edgeIndex]; + if (edgeElement === undefined) { + throw new Error('Element not found'); + } + const edgePath = this.path(edgeIndex); + return { edgePath, edgeElement, edgeIndex, edgeElementsCount: this._layers[0].length }; + } + /** + * 🪓 + * @param count + */ + getTreeSlices(count = 4) { + const length = this._layers[0].length; + let size = Math.ceil(length / count); + if (size % 2) + size++; + const slices = []; + for (let i = 0; i < length; i += size) { + const edgeLeft = i; + const edgeRight = i + size; + slices.push({ edge: this.getTreeEdge(edgeLeft), elements: this.elements.slice(edgeLeft, edgeRight) }); + } + return slices; + } + /** + * Serialize entire tree state including intermediate layers into a plain object + * Deserializing it back will not require to recompute any hashes + * Elements are not converted to a plain type, this is responsibility of the caller + */ + serialize() { + return { + levels: this.levels, + _zeros: this._zeros, + _layers: this._layers, + }; + } + /** + * Deserialize data into a MerkleTree instance + * Make sure to provide the same hashFunction as was used in the source tree, + * otherwise the tree state will be invalid + */ + static deserialize(data, hashFunction) { + return new MerkleTree(data.levels, data._layers[0], { hashFunction, zeroElement: data._zeros[0] }); + } + toString() { + return JSON.stringify(this.serialize()); + } +} +exports.default = MerkleTree; diff --git a/lib/PartialMerkleTree.d.ts b/lib/PartialMerkleTree.d.ts new file mode 100644 index 0000000..1f76fa9 --- /dev/null +++ b/lib/PartialMerkleTree.d.ts @@ -0,0 +1,33 @@ +import { Element, HashFunction, MerkleTreeOptions, ProofPath, SerializedPartialTreeState, TreeEdge } from './'; +import { BaseTree } from './BaseTree'; +export declare class PartialMerkleTree extends BaseTree { + private _leaves; + private _leavesAfterEdge; + private _edgeLeaf; + private _initialRoot; + private _edgeLeafProof; + private _proofMap; + constructor(levels: number, { edgePath, edgeElement, edgeIndex, edgeElementsCount, }: TreeEdge, leaves: Element[], { hashFunction, zeroElement }?: MerkleTreeOptions); + get edgeIndex(): number; + get edgeElement(): Element; + get edgeLeafProof(): ProofPath; + private _createProofMap; + private _buildTree; + private _buildHashes; + /** + * Change an element in the tree + * @param {number} index Index of element to change + * @param element Updated element value + */ + update(index: number, element: Element): void; + path(index: number): ProofPath; + /** + * Shifts edge of tree to left + * @param edge new TreeEdge below current edge + * @param elements leaves between old and new edge + */ + shiftEdge(edge: TreeEdge, elements: Element[]): void; + serialize(): SerializedPartialTreeState; + static deserialize(data: SerializedPartialTreeState, hashFunction?: HashFunction): PartialMerkleTree; + toString(): string; +} diff --git a/lib/PartialMerkleTree.js b/lib/PartialMerkleTree.js new file mode 100644 index 0000000..f475ba7 --- /dev/null +++ b/lib/PartialMerkleTree.js @@ -0,0 +1,155 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PartialMerkleTree = void 0; +const simpleHash_1 = __importDefault(require("./simpleHash")); +const BaseTree_1 = require("./BaseTree"); +class PartialMerkleTree extends BaseTree_1.BaseTree { + constructor(levels, { edgePath, edgeElement, edgeIndex, edgeElementsCount, }, leaves, { hashFunction, zeroElement } = {}) { + super(); + if (edgeIndex + leaves.length !== edgeElementsCount) + throw new Error('Invalid number of elements'); + this._edgeLeafProof = edgePath; + this._initialRoot = edgePath.pathRoot; + this.zeroElement = zeroElement !== null && zeroElement !== void 0 ? zeroElement : 0; + this._edgeLeaf = { data: edgeElement, index: edgeIndex }; + this._leavesAfterEdge = leaves; + this.levels = levels; + this._hashFn = hashFunction || simpleHash_1.default; + this._createProofMap(); + this._buildTree(); + } + get edgeIndex() { + return this._edgeLeaf.index; + } + get edgeElement() { + return this._edgeLeaf.data; + } + get edgeLeafProof() { + return this._edgeLeafProof; + } + _createProofMap() { + this._proofMap = this.edgeLeafProof.pathPositions.reduce((p, c, i) => { + p.set(i, [c, this.edgeLeafProof.pathElements[i]]); + return p; + }, new Map()); + this._proofMap.set(this.levels, [0, this.edgeLeafProof.pathRoot]); + } + _buildTree() { + const edgeLeafIndex = this._edgeLeaf.index; + this._leaves = Array(edgeLeafIndex).concat(this._leavesAfterEdge); + if (this._proofMap.has(0)) { + const [proofPos, proofEl] = this._proofMap.get(0); + this._leaves[proofPos] = proofEl; + } + this._layers = [this._leaves]; + this._buildZeros(); + this._buildHashes(); + } + _buildHashes() { + for (let layerIndex = 1; layerIndex <= this.levels; layerIndex++) { + const nodes = this._layers[layerIndex - 1]; + const currentLayer = this._processNodes(nodes, layerIndex); + if (this._proofMap.has(layerIndex)) { + const [proofPos, proofEl] = this._proofMap.get(layerIndex); + if (!currentLayer[proofPos]) + currentLayer[proofPos] = proofEl; + } + this._layers[layerIndex] = currentLayer; + } + } + /** + * Change an element in the tree + * @param {number} index Index of element to change + * @param element Updated element value + */ + update(index, element) { + if (isNaN(Number(index)) || index < 0 || index > this._layers[0].length || index >= this.capacity) { + throw new Error('Insert index out of bounds: ' + index); + } + if (index < this._edgeLeaf.index) { + throw new Error(`Index ${index} is below the edge: ${this._edgeLeaf.index}`); + } + this._layers[0][index] = element; + this._processUpdate(index); + } + path(index) { + if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) { + throw new Error('Index out of bounds: ' + index); + } + if (index < this._edgeLeaf.index) { + throw new Error(`Index ${index} is below the edge: ${this._edgeLeaf.index}`); + } + let elIndex = Number(index); + const pathElements = []; + const pathIndices = []; + const pathPositions = []; + for (let level = 0; level < this.levels; level++) { + pathIndices[level] = elIndex % 2; + const leafIndex = elIndex ^ 1; + if (leafIndex < this._layers[level].length) { + const [proofPos, proofEl] = this._proofMap.get(level); + pathElements[level] = proofPos === leafIndex ? proofEl : this._layers[level][leafIndex]; + pathPositions[level] = leafIndex; + } + else { + pathElements[level] = this._zeros[level]; + pathPositions[level] = 0; + } + elIndex >>= 1; + } + return { + pathElements, + pathIndices, + pathPositions, + pathRoot: this.root, + }; + } + /** + * Shifts edge of tree to left + * @param edge new TreeEdge below current edge + * @param elements leaves between old and new edge + */ + shiftEdge(edge, elements) { + if (this._edgeLeaf.index <= edge.edgeIndex) { + throw new Error(`New edgeIndex should be smaller then ${this._edgeLeaf.index}`); + } + if (elements.length !== (this._edgeLeaf.index - edge.edgeIndex)) { + throw new Error(`Elements length should be ${this._edgeLeaf.index - edge.edgeIndex}`); + } + this._edgeLeafProof = edge.edgePath; + this._edgeLeaf = { index: edge.edgeIndex, data: edge.edgeElement }; + this._leavesAfterEdge = [...elements, ...this._leavesAfterEdge]; + this._createProofMap(); + this._buildTree(); + } + serialize() { + const leaves = this.layers[0].slice(this._edgeLeaf.index); + return { + _edgeLeafProof: this._edgeLeafProof, + _edgeLeaf: this._edgeLeaf, + _edgeElementsCount: this._layers[0].length, + levels: this.levels, + leaves, + _zeros: this._zeros, + }; + } + static deserialize(data, hashFunction) { + const edge = { + edgePath: data._edgeLeafProof, + edgeElement: data._edgeLeaf.data, + edgeIndex: data._edgeLeaf.index, + edgeElementsCount: data._edgeElementsCount, + }; + return new PartialMerkleTree(data.levels, edge, data.leaves, { + hashFunction, + zeroElement: data._zeros[0], + }); + } + toString() { + return JSON.stringify(this.serialize()); + } +} +exports.PartialMerkleTree = PartialMerkleTree; diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..74bc3a8 --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,44 @@ +export { default as MerkleTree } from './FixedMerkleTree'; +export { PartialMerkleTree } from './PartialMerkleTree'; +export { simpleHash } from './simpleHash'; +export declare type HashFunction = { + (left: T, right: T): string; +}; +export declare type MerkleTreeOptions = { + hashFunction?: HashFunction; + zeroElement?: Element; +}; +export declare type Element = string | number; +export declare type SerializedTreeState = { + levels: number; + _zeros: Array; + _layers: Array; +}; +export declare type SerializedPartialTreeState = { + levels: number; + leaves: Element[]; + _edgeElementsCount: number; + _zeros: Array; + _edgeLeafProof: ProofPath; + _edgeLeaf: LeafWithIndex; +}; +export declare type ProofPath = { + pathElements: Element[]; + pathIndices: number[]; + pathPositions: number[]; + pathRoot: Element; +}; +export declare type TreeEdge = { + edgeElement: Element; + edgePath: ProofPath; + edgeIndex: number; + edgeElementsCount: number; +}; +export declare type TreeSlice = { + edge: TreeEdge; + elements: Element[]; +}; +export declare type LeafWithIndex = { + index: number; + data: Element; +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..1a2eb38 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,12 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.simpleHash = exports.PartialMerkleTree = exports.MerkleTree = void 0; +var FixedMerkleTree_1 = require("./FixedMerkleTree"); +Object.defineProperty(exports, "MerkleTree", { enumerable: true, get: function () { return __importDefault(FixedMerkleTree_1).default; } }); +var PartialMerkleTree_1 = require("./PartialMerkleTree"); +Object.defineProperty(exports, "PartialMerkleTree", { enumerable: true, get: function () { return PartialMerkleTree_1.PartialMerkleTree; } }); +var simpleHash_1 = require("./simpleHash"); +Object.defineProperty(exports, "simpleHash", { enumerable: true, get: function () { return simpleHash_1.simpleHash; } }); diff --git a/lib/simpleHash.d.ts b/lib/simpleHash.d.ts new file mode 100644 index 0000000..e5c6c87 --- /dev/null +++ b/lib/simpleHash.d.ts @@ -0,0 +1,10 @@ +import { Element } from './index'; +/*** + * This is insecure hash function, just for example only + * @param data + * @param seed + * @param hashLength + */ +export declare function simpleHash(data: T[], seed?: number, hashLength?: number): string; +declare const _default: (left: Element, right: Element) => string; +export default _default; diff --git a/lib/simpleHash.js b/lib/simpleHash.js new file mode 100644 index 0000000..20e3794 --- /dev/null +++ b/lib/simpleHash.js @@ -0,0 +1,21 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.simpleHash = void 0; +/*** + * This is insecure hash function, just for example only + * @param data + * @param seed + * @param hashLength + */ +function simpleHash(data, seed, hashLength = 40) { + const str = data.join(''); + let i, l, hval = seed !== null && seed !== void 0 ? seed : 0x811c9dcc5; + for (i = 0, l = str.length; i < l; i++) { + hval ^= str.charCodeAt(i); + hval += (hval << 1) + (hval << 4) + (hval << 6) + (hval << 8) + (hval << 24); + } + const hash = (hval >>> 0).toString(16); + return BigInt('0x' + hash.padEnd(hashLength - (hash.length - 1), '0')).toString(10); +} +exports.simpleHash = simpleHash; +exports.default = (left, right) => simpleHash([left, right]);