From ec96f12cbf9342753d2ae0643a87b8343cd3cc60 Mon Sep 17 00:00:00 2001 From: cedoor Date: Tue, 18 Jan 2022 18:44:44 +0100 Subject: [PATCH] feat: add delete method and more tests Former-commit-id: c214da494298044b04414e137c7a52b14b36faa7 [formerly 3fe10d6d66231d4ffc6f105d23fdb496267781e8] [formerly af88f1b73915c377a7f1ff6c900ee4ba0b505bb8 [formerly 28a736bed822592c9df6189311e28427d420e272]] Former-commit-id: 67e3ac5620e6fae02fa0c948182d0768f7b86c4c [formerly 9387ebc52c3bd3db0eb090bc59e3b0ff1cc335f3] Former-commit-id: cb7a941e3c50664e0e91e17805d7e9d2a21463f7 --- .../src/incremental-merkle-tree.ts | 84 +++++++-- .../tests/index.test.ts | 168 ++++++++++++------ 2 files changed, 180 insertions(+), 72 deletions(-) diff --git a/packages/incremental-merkle-tree/src/incremental-merkle-tree.ts b/packages/incremental-merkle-tree/src/incremental-merkle-tree.ts index ab859cd..3a59c97 100644 --- a/packages/incremental-merkle-tree/src/incremental-merkle-tree.ts +++ b/packages/incremental-merkle-tree/src/incremental-merkle-tree.ts @@ -5,8 +5,8 @@ import { HashFunction, Proof, Node } from "./types" * A Merkle tree is a tree in which every leaf node is labelled with the cryptographic hash of a * data block, and every non-leaf node is labelled with the cryptographic hash of the labels of its child nodes. * It allows efficient and secure verification of the contents of large data structures. - * The MerkleTree class is a TypeScript implementation of Merkle tree and it provides all the functions to create - * efficient trees and to generate and verify proofs of membership. + * The IncrementalMerkleTree class is a TypeScript implementation of Incremental Merkle tree and it + * provides all the functions to create efficient trees and to generate and verify proofs of membership. */ export default class IncrementalMerkleTree { static readonly maxDepth = 32 @@ -19,7 +19,8 @@ export default class IncrementalMerkleTree { protected readonly _arity: number /** - * Initializes the Merkle tree with the hash function, the depth and the zero value to use for zeroes. + * Initializes the tree with the hash function, the depth, the zero value to use for zeroes + * and the arity (i.e. the number of children for each node). * @param hash Hash function. * @param depth Tree depth. * @param zeroValue Zero values for zeroes. @@ -90,8 +91,8 @@ export default class IncrementalMerkleTree { } /** - * Returns the zeroes nodes of the tree. - * @returns List of zeroes. + * Returns the number of children for each node. + * @returns Number of children per node. */ public get arity(): number { return this._arity @@ -128,10 +129,42 @@ export default class IncrementalMerkleTree { this.forEachLevel(this.leaves.length, (level, index, position) => { this._nodes[level][index] = node - const leftNeighbours: Array = this._nodes[level].slice(index - position, index) - const rightNeighbours: Array = Array(this._arity - 1 - position).fill(this.zeroes[level]) + let children = this._nodes[level].slice(index - position, index - position + this._arity) - node = this._hash(leftNeighbours.concat([node], rightNeighbours)) + if (children.length < this.arity) { + children = this.padArrayEnd(children, this.arity, this.zeroes[level]) + } + + node = this._hash(children) + }) + + this._root = node + } + + /** + * Deletes a leaf from the tree. It does not remove the leaf from + * the data structure. It set the leaf to be deleted to a zero value. + * @param index Index of the leaf to be deleted. + */ + public delete(index: number) { + checkParameter(index, "index", "number") + + if (index < 0 || index >= this.leaves.length) { + throw new Error("The leaf does not exist in this tree") + } + + let node = this._zeroes[0] + + this.forEachLevel(index, (level, index, position) => { + this._nodes[level][index] = node + + let children = this._nodes[level].slice(index - position, index - position + this._arity) + + if (children.length < this.arity) { + children = this.padArrayEnd(children, this.arity, this.zeroes[level]) + } + + node = this._hash(children) }) this._root = node @@ -150,21 +183,18 @@ export default class IncrementalMerkleTree { } const siblingNodes: Node[] = [] - const path: Array = [] + const path: number[] = [] this.forEachLevel(index, (level, index, position) => { path.push(position) - const leftNeighbours: Array = this._nodes[level].slice(index - position, index) - const lastRighIndex = index + this._arity - position - const numOfLeaves = this._nodes[level].length - let rightNeighbours: Array = this._nodes[level].slice(index + 1, Math.min(numOfLeaves, lastRighIndex)) + siblingNodes[level] = this._nodes[level].slice(index - position, index - position + this._arity) - if (numOfLeaves < lastRighIndex) { - rightNeighbours = rightNeighbours.concat(Array(lastRighIndex - numOfLeaves).fill(this.zeroes[level])) + if (siblingNodes[level].length < this.arity) { + siblingNodes[level] = this.padArrayEnd(siblingNodes[level], this.arity, this.zeroes[level]) } - siblingNodes[level] = leftNeighbours.concat(rightNeighbours) + siblingNodes[level].splice(position, 1) }) return { root: this._root, leaf: this.leaves[index], siblingNodes, path } @@ -186,17 +216,35 @@ export default class IncrementalMerkleTree { for (let i = 0; i < proof.siblingNodes.length; i += 1) { proof.siblingNodes[i].splice(proof.path[i], 0, node) + node = this._hash(proof.siblingNodes[i]) } return proof.root === node } + /** + * Provides a bottom-up tree traversal where for each level it calls a callback. + * @param index Index of the leaf. + * @param callback Callback with tree level, index of node in that level and position. + */ private forEachLevel(index: number, callback: (level: number, index: number, position: number) => void) { - for (let level = 0; level < this._depth; level += 1) { - callback(level, index, index % this._arity) + for (let level = 0; level < this.depth; level += 1) { + callback(level, index, index % this.arity) index = Math.floor(index / this.arity) } } + + /** + * Pads the array with a new value (multiple times, if needed) until the resulting + * array reaches the given length. + * @param array The array to pad. + * @param length The length of the resulting array. + * @param value The value to pad the array with. + * @returns An array of the specified length with the new values at the end. + */ + private padArrayEnd(array: any[], length: number, value: any): any[] { + return Array.from({ ...array, length }, (v) => v ?? value) + } } diff --git a/packages/incremental-merkle-tree/tests/index.test.ts b/packages/incremental-merkle-tree/tests/index.test.ts index 93b3e70..5ec99b2 100644 --- a/packages/incremental-merkle-tree/tests/index.test.ts +++ b/packages/incremental-merkle-tree/tests/index.test.ts @@ -1,62 +1,122 @@ import { poseidon } from "circomlibjs" import { IncrementalMerkleTree } from "../src" +import { IncrementalQuinTree } from "incrementalquintree" describe("Incremental Merkle Tree", () => { - const depth = 20 - const arity = 5 + const depth = 16 + const numberOfLeaves = 9 - let tree: IncrementalMerkleTree + for (const arity of [2, 5]) { + describe(`Intremental Merkle Tree (arity = ${arity})`, () => { + let tree: IncrementalMerkleTree + let oldTree: IncrementalQuinTree - describe("Merkle Tree class", () => { - beforeEach(() => { - tree = new IncrementalMerkleTree(poseidon, depth, BigInt(0), arity) + beforeEach(() => { + tree = new IncrementalMerkleTree(poseidon, depth, BigInt(0), arity) + oldTree = new IncrementalQuinTree(depth, BigInt(0), arity, poseidon) + }) + + it("Should not initialize a tree with wrong parameters", () => { + const fun1 = () => new IncrementalMerkleTree(undefined as any, 33, 0, arity) + const fun2 = () => new IncrementalMerkleTree(1 as any, 33, 0, arity) + + expect(fun1).toThrow("Parameter 'hash' is not defined") + expect(fun2).toThrow("Parameter 'hash' is none of these types: function") + }) + + it("Should not initialize a tree with depth > 32", () => { + const fun = () => new IncrementalMerkleTree(poseidon, 33, BigInt(0), arity) + + expect(fun).toThrow("The tree depth must be between 1 and 32") + }) + + it("Should initialize a tree", () => { + expect(tree.depth).toEqual(depth) + expect(tree.leaves).toHaveLength(0) + expect(tree.zeroes).toHaveLength(depth) + expect(tree.arity).toEqual(arity) + }) + + it("Should not insert a zero leaf", () => { + const fun = () => tree.insert(BigInt(0)) + + expect(fun).toThrow("The leaf cannot be a zero value") + }) + + it("Should not insert a leaf in a full tree", () => { + const fullTree = new IncrementalMerkleTree(poseidon, 1, BigInt(0), 3) + + fullTree.insert(BigInt(1)) + fullTree.insert(BigInt(2)) + fullTree.insert(BigInt(3)) + + const fun = () => fullTree.insert(BigInt(4)) + + expect(fun).toThrow("The tree is full") + }) + + it(`Should insert ${numberOfLeaves} leaves`, () => { + for (let i = 0; i < numberOfLeaves; i++) { + tree.insert(BigInt(1)) + oldTree.insert(BigInt(1)) + + const { root } = oldTree.genMerklePath(0) + + expect(tree.root).toEqual(root) + expect(tree.leaves).toHaveLength(i + 1) + } + }) + + it("Should not delete a leaf that does not exist", () => { + const fun = () => tree.delete(0) + + expect(fun).toThrow("The leaf does not exist in this tree") + }) + + it(`Should delete ${numberOfLeaves} leaves`, () => { + for (let i = 0; i < numberOfLeaves; i++) { + tree.insert(BigInt(1)) + oldTree.insert(BigInt(1)) + } + + for (let i = 0; i < numberOfLeaves; i++) { + tree.delete(i) + oldTree.update(i, BigInt(0)) + + const { root } = oldTree.genMerklePath(0) + + expect(tree.root).toEqual(root) + } + }) + + it("Should return the index of a leaf", () => { + tree.insert(BigInt(1)) + tree.insert(BigInt(2)) + + const index = tree.indexOf(BigInt(2)) + + expect(index).toEqual(1) + }) + + it("Should not create any proof if the leaf does not exist", () => { + tree.insert(BigInt(1)) + + const fun = () => tree.createProof(1) + + expect(fun).toThrow("The leaf does not exist in this tree") + }) + + it("Should create a valid proof", () => { + for (let i = 0; i < numberOfLeaves; i += 1) { + tree.insert(BigInt(i + 1)) + } + + for (let i = 0; i < numberOfLeaves; i += 1) { + const proof = tree.createProof(i) + + expect(tree.verifyProof(proof)).toBeTruthy() + } + }) }) - - it("Should not initialize a Merkle tree with wrong parameters", () => { - expect(() => new IncrementalMerkleTree(undefined as any, 33, 0, arity)).toThrow("Parameter 'hash' is not defined") - expect(() => new IncrementalMerkleTree(1 as any, 33, 0, arity)).toThrow( - "Parameter 'hash' is none of these types: function" - ) - }) - - it("Should not initialize a Merkle tree with depth > 32", () => { - expect(() => new IncrementalMerkleTree(poseidon, 33, BigInt(0), arity)).toThrow( - "The tree depth must be between 1 and 32" - ) - }) - - it("Should initialize a Merkle tree", () => { - expect(tree.depth).toEqual(depth) - expect(tree.leaves).toHaveLength(0) - expect(tree.zeroes).toHaveLength(depth) - expect(tree.arity).toEqual(arity) - }) - - it("Should not insert a zero leaf", () => { - expect(() => tree.insert(BigInt(0))).toThrow("The leaf cannot be a zero value") - }) - - it("Should not insert a leaf in a full tree", () => { - const fullTree = new IncrementalMerkleTree(poseidon, 1, BigInt(0), 3) - - fullTree.insert(BigInt(1)) - fullTree.insert(BigInt(2)) - fullTree.insert(BigInt(3)) - - expect(() => fullTree.insert(BigInt(4))).toThrow("The tree is full") - }) - - it("Should create a valid proof", () => { - const numberOfLeaves = 50 - - for (let i = 0; i < numberOfLeaves; i += 1) { - tree.insert(BigInt(i + 1)) - } - - for (let i = 0; i < numberOfLeaves; i += 1) { - const proof = tree.createProof(i) - expect(tree.verifyProof(proof)).toBeTruthy() - } - }) - }) + } })