diff --git a/.gitignore b/.gitignore index 0794134..7d49510 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build lib yarn-error.log .idea +.nyc_output diff --git a/.run/Template Mocha.run.xml b/.run/Template Mocha.run.xml new file mode 100644 index 0000000..72dd382 --- /dev/null +++ b/.run/Template Mocha.run.xml @@ -0,0 +1,14 @@ + + + project + + $PROJECT_DIR$/node_modules/ts-mocha + $PROJECT_DIR$ + true + bdd + + PATTERN + $PROJECT_DIR$/test/*.spec.ts + + + diff --git a/src/fixedMerkleTree.ts b/src/fixedMerkleTree.ts new file mode 100644 index 0000000..4c4c6f9 --- /dev/null +++ b/src/fixedMerkleTree.ts @@ -0,0 +1,238 @@ +import { + defaultHash, + Element, + HashFunction, + MerkleTreeOptions, + ProofPath, + SerializedTreeState, + TreeEdge, +} from './' + + +export default class MerkleTree { + get layers(): Array { + return this._layers.slice() + } + + set layers(value: Array) { + this._layers = value + } + + levels: number + capacity: number + private _hashFn: HashFunction + private zeroElement: Element + private _zeros: Element[] + private _layers: Array + + constructor(levels: number, elements: Element[] = [], { + hashFunction = defaultHash, + zeroElement = 0, + }: MerkleTreeOptions = {}) { + this.levels = levels + this.capacity = 2 ** levels + if (elements.length > this.capacity) { + throw new Error('Tree is full') + } + this._hashFn = hashFunction + this.zeroElement = zeroElement + + this._layers = [] + this._layers[0] = elements.slice() + this._buildZeros() + this._rebuild() + } + + private _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]) + } + } + + _rebuild() { + for (let level = 1; level <= this.levels; level++) { + this._layers[level] = [] + for (let i = 0; i < Math.ceil(this._layers[level - 1].length / 2); i++) { + this._layers[level][i] = this._hashFn( + this._layers[level - 1][i * 2], + i * 2 + 1 < this._layers[level - 1].length + ? this._layers[level - 1][i * 2 + 1] + : this._zeros[level - 1], + ) + } + } + } + + /** + * Get tree root + */ + root(): Element { + return this._layers[this.levels][0] ?? this._zeros[this.levels] + } + + /** + * Insert new element into the tree + * @param element Element to insert + */ + insert(element: 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: Element[]) { + 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]) + } + + /** + * Change an element in the tree + * @param {number} index Index of element to change + * @param element Updated element value + */ + update(index: number, element: 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 + for (let level = 1; level <= this.levels; level++) { + index >>= 1 + this._layers[level][index] = this._hashFn( + this._layers[level - 1][index * 2], + index * 2 + 1 < this._layers[level - 1].length + ? this._layers[level - 1][index * 2 + 1] + : this._zeros[level - 1], + ) + } + } + + /** + * 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: Element): ProofPath { + if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) { + throw new Error('Index out of bounds: ' + index) + } + let elIndex = +index + const pathElements: Element[] = [] + const pathIndices: number[] = [] + const pathPositions: number [] = [] + 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, + } + } + + /** + * 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) => R): number { + if (comparator) { + return this._layers[0].findIndex((el) => comparator(element, el)) + } else { + return this._layers[0].indexOf(element) + } + } + + getTreeEdge(edgeElement: Element, index?: number): TreeEdge { + if (edgeElement === 'undefined') { + throw new Error('element is required') + } + let edgeIndex: number + if (!Number.isInteger(index)) { + index = -1 + const leaves = this._layers[0] + index = leaves.indexOf(edgeElement) + edgeIndex = index + } + + if (index <= -1) { + return null + } + const edgePath = this.path(index) + return { edgePath, edgeElement, edgeIndex } + } + + /** + * Returns a copy of non-zero tree elements. + */ + get elements() { + return this._layers[0].slice() + } + + /** + * Returns a copy of n-th zero elements array + */ + get zeros() { + return this._zeros.slice() + } + + /** + * 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 { + 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: SerializedTreeState, hashFunction?: HashFunction): MerkleTree { + return new MerkleTree(data.levels, data._layers[0], { hashFunction, zeroElement: data._zeros[0] }) + } +} + diff --git a/src/index.ts b/src/index.ts index 3a7c7f1..b331226 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,260 +1,15 @@ -import simpleHash from './simpleHash' +import { simpleHash } from './simpleHash' -const defaultHash = (left: Element, right: Element): string => simpleHash([left, right]) +export { default as MerkleTree } from './fixedMerkleTree' +export { PartialMerkleTree } from './partialMerkleTree' +export { simpleHash } from './simpleHash' -export default class MerkleTree { - get layers(): Array { - return this._layers.slice() - } - - set layers(value: Array) { - this._layers = value - } - - levels: number - capacity: number - private _hashFn: HashFunction - private zeroElement: Element - private _zeros: Element[] - private _layers: Array - - constructor(levels: number, elements: Element[] = [], { - hashFunction = defaultHash, - zeroElement = 0, - }: MerkleTreeOptions = {}) { - this.levels = levels - this.capacity = 2 ** levels - if (elements.length > this.capacity) { - throw new Error('Tree is full') - } - this._hashFn = hashFunction - this.zeroElement = zeroElement - this._zeros = [] - this._zeros[0] = zeroElement - for (let i = 1; i <= levels; i++) { - this._zeros[i] = this._hashFn(this._zeros[i - 1], this._zeros[i - 1]) - } - this._layers = [] - this._layers[0] = elements.slice() - this._rebuild() - } - - _rebuild() { - for (let level = 1; level <= this.levels; level++) { - this._layers[level] = [] - for (let i = 0; i < Math.ceil(this._layers[level - 1].length / 2); i++) { - this._layers[level][i] = this._hashFn( - this._layers[level - 1][i * 2], - i * 2 + 1 < this._layers[level - 1].length - ? this._layers[level - 1][i * 2 + 1] - : this._zeros[level - 1], - ) - } - } - } - - /** - * Get tree root - */ - root(): string { - return `${this._layers[this.levels].length > 0 ? this._layers[this.levels][0] : this._zeros[this.levels]}` - } - - /** - * Insert new element into the tree - * @param element Element to insert - */ - insert(element: 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: Element[]) { - 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]) - } - - /** - * Change an element in the tree - * @param {number} index Index of element to change - * @param element Updated element value - */ - update(index: number, element: 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 - for (let level = 1; level <= this.levels; level++) { - index >>= 1 - this._layers[level][index] = this._hashFn( - this._layers[level - 1][index * 2], - index * 2 + 1 < this._layers[level - 1].length - ? this._layers[level - 1][index * 2 + 1] - : this._zeros[level - 1], - ) - } - } - - /** - * 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: Element): ProofPath { - if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) { - throw new Error('Index out of bounds: ' + index) - } - let elIndex = +index - const pathElements: Element[] = [] - const pathIndices: number[] = [] - const pathPositions: number [] = [] - 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, - } - } - - /** - * 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) => R): number { - if (comparator) { - return this._layers[0].findIndex((el) => comparator(element, el)) - } else { - return this._layers[0].indexOf(element) - } - } - - getTreeEdge(edgeElement: Element, index?: number) { - if (edgeElement === 'undefined') { - throw new Error('element is required') - } - let edgeIndex: number - if (!Number.isInteger(index)) { - index = -1 - const leaves = this._layers[0] - index = leaves.indexOf(edgeElement) - edgeIndex = index - } - - if (index <= -1) { - return [] - } - const edgePath = this.path(index) - return { edgePath, edgeElement, edgeIndex } - } - - /** - * Returns a copy of non-zero tree elements. - */ - get elements() { - return this._layers[0].slice() - } - - /** - * Returns a copy of n-th zero elements array - */ - get zeros() { - return this._zeros.slice() - } - - getLayersAsObject() { - const layers = this.layers - const objs = [] - for (let i = 0; i < this.levels; i++) { - const arr = [] - for (let j = 0; j < layers[i].length; j++) { - const obj = { [layers[i][j]]: null } - if (objs.length) { - obj[layers[i][j]] = {} - const a = objs.shift() - const akey = Object.keys(a)[0] - obj[layers[i][j]][akey] = a[akey] - if (objs.length) { - const b = objs.shift() - const bkey = Object.keys(b)[0] - obj[layers[i][j]][bkey] = b[bkey] - } - } - arr.push(obj) - } - objs.push(...arr) - } - return objs[0] - } - - /** - * 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 { - 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: SerializedTreeState, hashFunction?: HashFunction): MerkleTree { - return new MerkleTree(data.levels, data._layers[0], { hashFunction, zeroElement: data._zeros[0] }) - } +export type HashFunction = { + (left: T, right: T): string } -export type HashFunction = { - (left: string | number, right: string | number): string -} export type MerkleTreeOptions = { - hashFunction?: HashFunction + hashFunction?: HashFunction zeroElement?: Element } @@ -271,3 +26,9 @@ export type ProofPath = { pathIndices: number[], pathPositions: number[], } +export type TreeEdge = { + edgeElement: Element; + edgePath: ProofPath; + edgeIndex: number +} +export const defaultHash = (left: Element, right: Element): string => simpleHash([left, right]) diff --git a/src/partialMerkleTree.ts b/src/partialMerkleTree.ts index 4050b20..d5e4092 100644 --- a/src/partialMerkleTree.ts +++ b/src/partialMerkleTree.ts @@ -1,27 +1,31 @@ -import { Element, HashFunction, ProofPath } from './index' +import { defaultHash, Element, HashFunction, MerkleTreeOptions, ProofPath, TreeEdge } from './' type LeafWithIndex = { index: number, data: Element } export class PartialMerkleTree { levels: number - private _hash: HashFunction private zeroElement: Element private _zeros: Element[] private _layers: Array private _leaves: Element[] private _leavesAfterEdge: Element[] private _edgeLeaf: LeafWithIndex - private _root: string - private _hashFn: HashFunction + private _root: Element + private _hashFn: HashFunction private _edgeLeafProof: ProofPath - constructor(edgeLeafProof: ProofPath, edgeLeaf: LeafWithIndex, leaves: Element[], root: string, hashFn: HashFunction) { - this._edgeLeafProof = edgeLeafProof - this._edgeLeaf = edgeLeaf + constructor({ + edgePath, + edgeElement, + edgeIndex, + }: TreeEdge, leaves: Element[], root: Element, { hashFunction, zeroElement }: MerkleTreeOptions = {}) { + this._edgeLeafProof = edgePath + this.zeroElement = zeroElement ?? 0 + this._edgeLeaf = { data: edgeElement, index: edgeIndex } this._leavesAfterEdge = leaves this._root = root - this._hashFn = hashFn - + this._hashFn = hashFunction || defaultHash + this._buildTree() } get capacity() { @@ -32,15 +36,23 @@ export class PartialMerkleTree { const edgeLeafIndex = this._edgeLeaf.index this._leaves = [...Array.from({ length: edgeLeafIndex - 1 }, () => null), ...this._leavesAfterEdge] this._layers = [this._leaves] + this._buildZeros() this._rebuild() } + private _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]) + } + } + _rebuild() { for (let level = 1; level <= this.levels; level++) { this._layers[level] = [] for (let i = 0; i < Math.ceil(this._layers[level - 1].length / 2); i++) { - this._layers[level][i] = this._hash( + this._layers[level][i] = this._hashFn( this._layers[level - 1][i * 2], i * 2 + 1 < this._layers[level - 1].length ? this._layers[level - 1][i * 2 + 1] diff --git a/src/simpleHash.ts b/src/simpleHash.ts index 9368ed9..9c56848 100644 --- a/src/simpleHash.ts +++ b/src/simpleHash.ts @@ -5,7 +5,7 @@ * @param hashLength */ -function simpleHash(data: T[], seed?: number, hashLength = 40): string { +export function simpleHash(data: T[], seed?: number, hashLength = 40): string { const str = data.join('') let i, l, hval = seed ?? 0x811c9dcc5 @@ -17,4 +17,3 @@ function simpleHash(data: T[], seed?: number, hashLength = 40): string { return BigInt('0x' + hash.padEnd(hashLength - (hash.length - 1), '0')).toString(10) } -export default simpleHash diff --git a/test/merkleTree.spec.ts b/test/merkleTree.spec.ts index 2d03e8c..94f8975 100644 --- a/test/merkleTree.spec.ts +++ b/test/merkleTree.spec.ts @@ -1,4 +1,4 @@ -import MerkleTree from '../src' +import { MerkleTree } from '../src' import { assert, should } from 'chai' import { it } from 'mocha' @@ -74,7 +74,7 @@ describe('MerkleTree', () => { should().equal(tree.root(), '4066635800770511602067209448381558554624') }) - it('should give the same result as sequental inserts', () => { + it('should give the same result as sequential inserts', () => { const initialArray = [ [1], [1, 2], @@ -278,6 +278,24 @@ describe('MerkleTree', () => { const layers2 = tree.layers should().not.equal(layers1, layers2) }) + it('should return correct zeros array', () => { + const zeros = [ + 0, + '1390935134112885103361924701261056180224', + '3223901263414086620636498663535535980544', + '938972308169430750202858820582946897920', + '3743880566844110745576746962917825445888', + ] + const tree = new MerkleTree(4, []) + assert.deepEqual(tree.zeros, zeros, 'Not equal') + }) + it('should return copy of zeros array', () => { + const tree = new MerkleTree(4, []) + const zeros1 = tree.zeros + tree.insert(6) + const zeros2 = tree.zeros + should().not.equal(zeros1, zeros2) + }) }) describe('#serialize', () => { diff --git a/test/partialMerkleTree.spec.ts b/test/partialMerkleTree.spec.ts new file mode 100644 index 0000000..cbe9a67 --- /dev/null +++ b/test/partialMerkleTree.spec.ts @@ -0,0 +1,19 @@ +import { MerkleTree, PartialMerkleTree } from '../src' +import { assert, should } from 'chai' +import { it } from 'mocha' + +describe('PartialMerkleTree', () => { + + describe('#constructor', () => { + const leaves = [1, 2, 3, 4, 5] + const fullTree = new MerkleTree(4, leaves) + const root = fullTree.root() + const edge = fullTree.getTreeEdge(3) + const leavesAfterEdge = leaves.splice(edge.edgeIndex) + it('should initialize merkle tree', () => { + const partialTree = new PartialMerkleTree(edge, leavesAfterEdge, root) + console.log(partialTree) + return true + }) + }) +}) diff --git a/test/simpleHash.spec.ts b/test/simpleHash.spec.ts index 39fbb2a..2bd6ad1 100644 --- a/test/simpleHash.spec.ts +++ b/test/simpleHash.spec.ts @@ -1,4 +1,4 @@ -import simpleHash from '../src/simpleHash' +import { simpleHash } from '../src' import { it } from 'mocha' import { should } from 'chai'