nary incrementaly merkle tree

Former-commit-id: 812614c0cc1f79817b314a46491d6b61710e4942 [formerly 522c3bd30389b7ce4175c2830c79ccf22a4404d9] [formerly cc13abf762f896a1959190f8767eeba8a4c8028c [formerly 312e3ef206]]
Former-commit-id: 0a9e445f0ec532bc892a540c4027de886ab6f188 [formerly e0fa3503670f7130116a766f4af21996450d715f]
Former-commit-id: ea5a71189761e75320b173d25664d3f72ae0d74d
This commit is contained in:
Andrija
2022-01-17 19:24:40 +01:00
parent d6faef4598
commit eeac8008ca
11 changed files with 25359 additions and 16612 deletions

File diff suppressed because it is too large Load Diff

8563
packages/merkle-tree/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
import checkParameter from "./checkParameter"
import { HashFunction, Node } from "./types"
export default class IncrementalTree {
static readonly maxDepth = 32 // 5**32
protected _root: Node
protected readonly _nodes: Node[][]
protected readonly _zeroes: Node[]
protected readonly _hash: HashFunction
protected readonly _depth: number
protected readonly _arity: number
constructor(hash: HashFunction, depth: number, zeroValue: Node, arity: number) {
checkParameter(hash, "hash", "function")
checkParameter(depth, "depth", "number")
checkParameter(zeroValue, "zeroValue", "number", "string", "bigint")
if (depth < 1 || depth > IncrementalTree.maxDepth) {
throw new Error("The tree depth must be between 1 and 32")
}
// Initialize the attributes.
this._hash = hash
this._depth = depth
this._zeroes = []
this._nodes = []
this._arity = arity;
for (let i = 0; i < depth; i += 1) {
this._zeroes.push(zeroValue)
this._nodes[i] = []
// There must be a zero value for each tree level (except the root).
zeroValue = hash(Array(this._arity).fill(zeroValue))
}
// The default root is the last zero value.
this._root = zeroValue
// Freeze the array objects. It prevents unintentional changes.
Object.freeze(this._zeroes)
Object.freeze(this._nodes)
}
/**
* Returns the root hash of the tree.
* @returns Root hash.
*/
public get root(): Node {
return this._root
}
/**
* Returns the depth of the tree.
* @returns Tree depth.
*/
public get depth(): number {
return this._depth
}
/**
* Returns the leaves of the tree.
* @returns List of leaves.
*/
public get leaves(): Node[] {
return this._nodes[0].slice()
}
/**
* Returns the zeroes nodes of the tree.
* @returns List of zeroes.
*/
public get zeroes(): Node[] {
return this._zeroes
}
/**
* Returns the zeroes nodes of the tree.
* @returns List of zeroes.
*/
public get arity(): number {
return this._arity
}
/**
* Returns the index of a leaf. If the leaf does not exist it returns -1.
* @param leaf Tree leaf.
* @returns Index of the leaf.
*/
public indexOf(leaf: Node): number {
checkParameter(leaf, "leaf", "number", "string", "bigint")
return this.leaves.indexOf(leaf)
}
}

View File

@@ -1,4 +1,5 @@
import MerkleTree from "./merkle-tree"
import NAryIncrementalTree from "./nary-incremental-tree"
export { MerkleTree }
export { MerkleTree, NAryIncrementalTree }
export * from "./types"

View File

@@ -1,5 +1,6 @@
import checkParameter from "./checkParameter"
import { HashFunction, Proof, Node } from "./types"
import IncrementalTree from "./incremental-tree"
/**
* A Merkle tree is a tree in which every leaf node is labelled with the cryptographic hash of a
@@ -8,15 +9,7 @@ import { HashFunction, Proof, Node } from "./types"
* 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.
*/
export default class MerkleTree {
static readonly maxDepth = 32 // 2**32 = 4294967296 possible leaves.
private _root: Node
private readonly _nodes: Node[][]
private readonly _zeroes: Node[]
private readonly _hash: HashFunction
private readonly _depth: number
export default class MerkleTree extends IncrementalTree {
/**
* Initializes the Merkle tree with the hash function, the depth and the zero value to use for zeroes.
* @param hash Hash function.
@@ -24,65 +17,7 @@ export default class MerkleTree {
* @param zeroValue Zero values for zeroes.
*/
constructor(hash: HashFunction, depth: number, zeroValue: Node) {
checkParameter(hash, "hash", "function")
checkParameter(depth, "depth", "number")
checkParameter(zeroValue, "zeroValue", "number", "string", "bigint")
if (depth < 1 || depth > MerkleTree.maxDepth) {
throw new Error("The tree depth must be between 1 and 32")
}
// Initialize the attributes.
this._hash = hash
this._depth = depth
this._zeroes = []
this._nodes = []
for (let i = 0; i < depth; i += 1) {
this._zeroes.push(zeroValue)
this._nodes[i] = []
// There must be a zero value for each tree level (except the root).
zeroValue = hash([zeroValue, zeroValue])
}
// The default root is the last zero value.
this._root = zeroValue
// Freeze the array objects. It prevents unintentional changes.
Object.freeze(this._zeroes)
Object.freeze(this._nodes)
}
/**
* Returns the root hash of the tree.
* @returns Root hash.
*/
public get root(): Node {
return this._root
}
/**
* Returns the depth of the tree.
* @returns Tree depth.
*/
public get depth(): number {
return this._depth
}
/**
* Returns the leaves of the tree.
* @returns List of leaves.
*/
public get leaves(): Node[] {
return this._nodes[0].slice()
}
/**
* Returns the zeroes nodes of the tree.
* @returns List of zeroes.
*/
public get zeroes(): Node[] {
return this._zeroes
super(hash, depth, zeroValue, 2)
}
/**
@@ -195,17 +130,6 @@ export default class MerkleTree {
return proof.root === node
}
/**
* Returns the index of a leaf. If the leaf does not exist it returns -1.
* @param leaf Tree leaf.
* @returns Index of the leaf.
*/
public indexOf(leaf: Node): number {
checkParameter(leaf, "leaf", "number", "string", "bigint")
return this.leaves.indexOf(leaf)
}
/**
* Provides a bottom-up tree traversal where for each level it calls a callback.
* @param index Index of the leaf.

View File

@@ -0,0 +1,103 @@
import checkParameter from "./checkParameter"
import { HashFunction, Proof, Node } from "./types"
import IncrementalTree from "./incremental-tree"
export default class NAryIncrementalTree extends IncrementalTree {
constructor(hash: HashFunction, depth: number, zeroValue: Node, arity: number) {
super(hash, depth, zeroValue, arity);
}
/**
* Inserts a new leaf in the tree.
* @param leaf New leaf.
*/
public insert(leaf: Node) {
checkParameter(leaf, "leaf", "number", "string", "bigint")
if (leaf === this.zeroes[0]) {
throw new Error("The leaf cannot be a zero value")
}
if (this.leaves.length >= this.arity ** this.depth) {
throw new Error("The tree is full")
}
let node = leaf
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])
node = this._hash(leftNeighbours.concat([node], rightNeighbours))
})
this._root = node
}
/**
* Creates a proof of membership.
* @param index Index of the proof's leaf.
* @returns Proof object.
*/
public createProof(index: number): Proof {
checkParameter(index, "index", "number")
if (index < 0 || index >= this.leaves.length) {
throw new Error("The leaf does not exist in this tree")
}
const siblingNodes: Node[] = []
const path: Array<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))
if (numOfLeaves < lastRighIndex) {
rightNeighbours = rightNeighbours.concat(Array(lastRighIndex - numOfLeaves).fill(this.zeroes[level]))
}
siblingNodes[level] = leftNeighbours.concat(rightNeighbours)
})
return { root: this._root, leaf: this.leaves[index], siblingNodes, path }
}
/**
* Verifies a proof and return true or false.
* @param proof Proof to be verified.
* @returns True or false.
*/
public verifyProof(proof: Proof): boolean {
checkParameter(proof, "proof", "object")
checkParameter(proof.root, "proof.root", "number", "string", "bigint")
checkParameter(proof.leaf, "proof.leaf", "number", "string", "bigint")
checkParameter(proof.siblingNodes, "proof.siblingNodes", "object")
checkParameter(proof.path, "proof.path", "object")
let node = proof.leaf
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
}
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)
index = Math.floor(index / this.arity)
}
}
}

View File

@@ -6,5 +6,5 @@ export type Proof = {
root: Node
leaf: Node
siblingNodes: Node[]
path: (0 | 1)[]
path: Array<number>
}

View File

@@ -0,0 +1,59 @@
import { poseidon } from "circomlibjs"
import { NAryIncrementalTree } from "../src"
describe("Nary Merkle Tree", () => {
const depth = 20;
const arity = 5;
let tree: NAryIncrementalTree;
describe("Merkle Tree class", () => {
beforeEach(() => {
tree = new NAryIncrementalTree(poseidon, depth, BigInt(0), 5)
})
it("Should not initialize a Merkle tree with wrong parameters", () => {
expect(() => new NAryIncrementalTree(undefined as any, 33, 0, arity)).toThrow("Parameter 'hash' is not defined")
expect(() => new NAryIncrementalTree(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 NAryIncrementalTree(poseidon, 33, BigInt(0), arity)).toThrow("The tree depth must be between 1 and 32")
})
it("Should initialize a Merkle tree", () => {
// console.log(tree.root)
expect(tree.depth).toEqual(depth)
// expect(tree.root).toEqual(BigInt("3315762791558236426429898223445373782079540514426385620818139644150484427120n"))
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 NAryIncrementalTree(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()
}
})
})

View File

@@ -1 +1 @@
8799a1b1047496f5091e6e0b5321f1cadfd7404a
4c454c6f5e66fdbf5b136bd6fddb262b6ea20646

File diff suppressed because it is too large Load Diff