feat: add delete method and more tests

Former-commit-id: c214da494298044b04414e137c7a52b14b36faa7 [formerly 3fe10d6d66231d4ffc6f105d23fdb496267781e8] [formerly af88f1b73915c377a7f1ff6c900ee4ba0b505bb8 [formerly 28a736bed8]]
Former-commit-id: 67e3ac5620e6fae02fa0c948182d0768f7b86c4c [formerly 9387ebc52c3bd3db0eb090bc59e3b0ff1cc335f3]
Former-commit-id: cb7a941e3c50664e0e91e17805d7e9d2a21463f7
This commit is contained in:
cedoor
2022-01-18 18:44:44 +01:00
parent 47be957403
commit ec96f12cbf
2 changed files with 180 additions and 72 deletions

View File

@@ -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<Node> = this._nodes[level].slice(index - position, index)
const rightNeighbours: Array<Node> = 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<number> = []
const path: number[] = []
this.forEachLevel(index, (level, index, position) => {
path.push(position)
const leftNeighbours: Array<Node> = this._nodes[level].slice(index - position, index)
const lastRighIndex = index + this._arity - position
const numOfLeaves = this._nodes[level].length
let rightNeighbours: Array<Node> = 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)
}
}

View File

@@ -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()
}
})
})
}
})