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'