mirror of
https://github.com/privacy-scaling-explorations/zk-kit.git
synced 2026-04-22 03:00:15 -04:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user