UNPKG

@iden3/js-merkletree

Version:

javascript sparse merkle tree library

680 lines (587 loc) 19.4 kB
import { ITreeStorage } from '../../types/storage'; import { Hash, ZERO_HASH, circomSiblingsFromSiblings } from '../hash/hash'; import { Node } from '../../types'; import { NODE_TYPE_EMPTY, NODE_TYPE_LEAF, NODE_TYPE_MIDDLE } from '../../constants'; import { NodeEmpty, NodeLeaf, NodeMiddle } from '../node/node'; import { bytesEqual, getPath } from '../utils'; import { NodeAux, Siblings } from '../../types/merkletree'; import { checkBigIntInField } from '../utils/crypto'; import { CircomProcessorProof, CircomVerifierProof } from './circom'; import { ErrEntryIndexAlreadyExists, ErrInvalidNodeFound, ErrKeyNotFound, ErrNotFound, ErrNotWritable, ErrReachedMaxLevel } from '../errors'; import { Proof } from './proof'; import { Entry, checkEntryInField } from '../entry'; export class Merkletree { private _db: ITreeStorage; private _root!: Hash; private _writable: boolean; private _maxLevel: number; constructor(_db: ITreeStorage, _writable: boolean, _maxLevels: number) { this._db = _db; this._writable = _writable; this._maxLevel = _maxLevels; } async root(): Promise<Hash> { if (!this._root) { this._root = await this._db.getRoot(); } return this._root; } get maxLevels(): number { return this._maxLevel; } async add(k: bigint, v: bigint): Promise<void> { if (!this._writable) { throw ErrNotWritable; } this._root = await this.root(); const kHash = Hash.fromBigInt(k); const vHash = Hash.fromBigInt(v); const newNodeLeaf = new NodeLeaf(kHash, vHash); const path = getPath(this.maxLevels, kHash.value); const newRootKey = await this.addLeaf(newNodeLeaf, this._root, 0, path); this._root = newRootKey; await this._db.setRoot(this._root); } async updateNode(n: Node): Promise<Hash> { if (!this._writable) { throw ErrNotWritable; } if (n.type === NODE_TYPE_EMPTY) { return await n.getKey(); } const k = await n.getKey(); await this._db.put(k.value, n); return k; } async addNode(n: Node): Promise<Hash> { if (!this._writable) { throw ErrNotWritable; } if (n.type === NODE_TYPE_EMPTY) { return await n.getKey(); } const k = await n.getKey(); // if (typeof this.#db.get(k.value) !== 'undefined') { // throw ErrNodeKeyAlreadyExists; // } await this._db.put(k.value, n); return k; } async addEntry(e: Entry): Promise<void> { if (!this._writable) { throw ErrNotWritable; } if (!checkEntryInField(e)) { throw 'elements not inside the finite field over r'; } this._root = await this._db.getRoot(); const hIndex = await e.hIndex(); const hValue = await e.hValue(); const newNodeLeaf = new NodeLeaf(hIndex, hValue); const path = getPath(this.maxLevels, hIndex.value); const newRootKey = await this.addLeaf(newNodeLeaf, this._root, 0, path); this._root = newRootKey; await this._db.setRoot(this._root); } async pushLeaf( newLeaf: Node, oldLeaf: Node, lvl: number, pathNewLeaf: Array<boolean>, pathOldLeaf: Array<boolean> ): Promise<Hash> { if (lvl > this._maxLevel - 2) { throw new Error(ErrReachedMaxLevel); } let newNodeMiddle: NodeMiddle; if (pathNewLeaf[lvl] === pathOldLeaf[lvl]) { const nextKey = await this.pushLeaf(newLeaf, oldLeaf, lvl + 1, pathNewLeaf, pathOldLeaf); if (pathNewLeaf[lvl]) { newNodeMiddle = new NodeMiddle(new Hash(), nextKey); } else { newNodeMiddle = new NodeMiddle(nextKey, new Hash()); } return await this.addNode(newNodeMiddle); } const oldLeafKey = await oldLeaf.getKey(); const newLeafKey = await newLeaf.getKey(); if (pathNewLeaf[lvl]) { newNodeMiddle = new NodeMiddle(oldLeafKey, newLeafKey); } else { newNodeMiddle = new NodeMiddle(newLeafKey, oldLeafKey); } await this.addNode(newLeaf); return await this.addNode(newNodeMiddle); } async addLeaf(newLeaf: NodeLeaf, key: Hash, lvl: number, path: Array<boolean>): Promise<Hash> { if (lvl > this._maxLevel - 1) { throw new Error(ErrReachedMaxLevel); } const n = await this.getNode(key); if (typeof n === 'undefined') { throw ErrNotFound; } switch (n.type) { case NODE_TYPE_EMPTY: return this.addNode(newLeaf); case NODE_TYPE_LEAF: { const nKey = (n as NodeLeaf).entry[0]; const newLeafKey = newLeaf.entry[0]; if (bytesEqual(nKey.value, newLeafKey.value)) { throw ErrEntryIndexAlreadyExists; } const pathOldLeaf = getPath(this.maxLevels, nKey.value); return this.pushLeaf(newLeaf, n, lvl, path, pathOldLeaf); } case NODE_TYPE_MIDDLE: { n as NodeMiddle; let newNodeMiddle: NodeMiddle; if (path[lvl]) { const nextKey = await this.addLeaf(newLeaf, (n as NodeMiddle).childR, lvl + 1, path); newNodeMiddle = new NodeMiddle((n as NodeMiddle).childL, nextKey); } else { const nextKey = await this.addLeaf(newLeaf, (n as NodeMiddle).childL, lvl + 1, path); newNodeMiddle = new NodeMiddle(nextKey, (n as NodeMiddle).childR); } return this.addNode(newNodeMiddle); } default: { throw ErrInvalidNodeFound; } } } async get(k: bigint): Promise<{ key: bigint; value: bigint; siblings: Siblings }> { const kHash = Hash.fromBigInt(k); const path = getPath(this.maxLevels, kHash.value); let nextKey = await this.root(); const siblings: Siblings = []; for (let i = 0; i < this.maxLevels; i++) { const n = await this.getNode(nextKey); if (typeof n === 'undefined') { throw ErrKeyNotFound; } switch (n.type) { case NODE_TYPE_EMPTY: return { key: BigInt('0'), value: BigInt('0'), siblings }; case NODE_TYPE_LEAF: // if (bytesEqual(kHash.value, (n as NodeLeaf).entry[0].value)) { // return { // key: (n as NodeLeaf).entry[0].BigInt(), // value: (n as NodeLeaf).entry[1].BigInt(), // siblings, // }; // } return { key: (n as NodeLeaf).entry[0].bigInt(), value: (n as NodeLeaf).entry[1].bigInt(), siblings }; case NODE_TYPE_MIDDLE: if (path[i]) { nextKey = (n as NodeMiddle).childR; siblings.push((n as NodeMiddle).childL); } else { nextKey = (n as NodeMiddle).childL; siblings.push((n as NodeMiddle).childR); } break; default: throw ErrInvalidNodeFound; } } throw new Error(ErrReachedMaxLevel); } async update(k: bigint, v: bigint): Promise<CircomProcessorProof> { if (!this._writable) { throw ErrNotWritable; } if (!checkBigIntInField(k)) { throw 'key not inside the finite field'; } if (!checkBigIntInField(v)) { throw 'key not inside the finite field'; } const kHash = Hash.fromBigInt(k); const vHash = Hash.fromBigInt(v); const path = getPath(this.maxLevels, kHash.value); const cp = new CircomProcessorProof(); cp.fnc = 1; cp.oldRoot = await this.root(); cp.oldKey = kHash; cp.newKey = kHash; cp.newValue = vHash; let nextKey = await this.root(); const siblings: Siblings = []; for (let i = 0; i < this.maxLevels; i += 1) { const n = await this.getNode(nextKey); if (typeof n === 'undefined') { throw ErrNotFound; } switch (n.type) { case NODE_TYPE_EMPTY: throw ErrKeyNotFound; case NODE_TYPE_LEAF: if (bytesEqual(kHash.value, (n as NodeLeaf).entry[0].value)) { cp.oldValue = (n as NodeLeaf).entry[1]; cp.siblings = circomSiblingsFromSiblings([...siblings], this.maxLevels); const newNodeLeaf = new NodeLeaf(kHash, vHash); await this.updateNode(newNodeLeaf); const newRootKey = await this.recalculatePathUntilRoot(path, newNodeLeaf, siblings); this._root = newRootKey; await this._db.setRoot(newRootKey); cp.newRoot = newRootKey; return cp; } break; case NODE_TYPE_MIDDLE: if (path[i]) { nextKey = (n as NodeMiddle).childR; siblings.push((n as NodeMiddle).childL); } else { nextKey = (n as NodeMiddle).childL; siblings.push((n as NodeMiddle).childR); } break; default: throw ErrInvalidNodeFound; } } throw ErrKeyNotFound; } async getNode(k: Hash): Promise<Node | undefined> { if (bytesEqual(k.value, ZERO_HASH.value)) { return new NodeEmpty(); } return await this._db.get(k.value); } async recalculatePathUntilRoot( path: Array<boolean>, node: Node, siblings: Siblings ): Promise<Hash> { for (let i = siblings.length - 1; i >= 0; i -= 1) { const nodeKey = await node.getKey(); if (path[i]) { node = new NodeMiddle(siblings[i], nodeKey); } else { node = new NodeMiddle(nodeKey, siblings[i]); } await this.addNode(node); } const nodeKey = await node.getKey(); return nodeKey; } // Delete removes the specified Key from the MerkleTree and updates the path // from the deleted key to the Root with the new values. This method removes // the key from the MerkleTree, but does not remove the old nodes from the // key-value database; this means that if the tree is accessed by an old Root // where the key was not deleted yet, the key will still exist. If is desired // to remove the key-values from the database that are not under the current // Root, an option could be to dump all the leaves (using mt.DumpLeafs) and // import them in a new MerkleTree in a new database (using // mt.ImportDumpedLeafs), but this will loose all the Root history of the // MerkleTree async delete(k: bigint): Promise<void> { if (!this._writable) { throw ErrNotWritable; } const kHash = Hash.fromBigInt(k); const path = getPath(this.maxLevels, kHash.value); let nextKey = this._root; const siblings: Siblings = []; for (let i = 0; i < this._maxLevel; i += 1) { const n = await this.getNode(nextKey); if (typeof n === 'undefined') { throw ErrNotFound; } switch (n.type) { case NODE_TYPE_EMPTY: throw ErrKeyNotFound; case NODE_TYPE_LEAF: if (bytesEqual(kHash.bytes, (n as NodeLeaf).entry[0].value)) { await this.rmAndUpload(path, kHash, siblings); return; } throw ErrKeyNotFound; case NODE_TYPE_MIDDLE: if (path[i]) { nextKey = (n as NodeMiddle).childR; siblings.push((n as NodeMiddle).childL); } else { nextKey = (n as NodeMiddle).childL; siblings.push((n as NodeMiddle).childR); } break; default: throw ErrInvalidNodeFound; } } throw ErrKeyNotFound; } async rmAndUpload(path: Array<boolean>, kHash: Hash, siblings: Siblings): Promise<void> { if (siblings.length === 0) { this._root = ZERO_HASH; await this._db.setRoot(this._root); return; } const toUpload = siblings[siblings.length - 1]; if (siblings.length < 2) { this._root = siblings[0]; await this._db.setRoot(this._root); } const nearestSibling = await this._db.get(toUpload.bytes); if (nearestSibling?.type === NODE_TYPE_MIDDLE) { let newNode: Node; if (path[siblings.length - 1]) { newNode = new NodeMiddle(toUpload, ZERO_HASH); } else { newNode = new NodeMiddle(ZERO_HASH, toUpload); } await this.addNode(newNode); const newRootKey = await this.recalculatePathUntilRoot( path, newNode, siblings.slice(0, siblings.length - 1) ); this._root = newRootKey; await this._db.setRoot(this._root); return; } for (let i = siblings.length - 2; i >= 0; i -= 1) { if (!bytesEqual(siblings[i].value, ZERO_HASH.value)) { let newNode: Node; if (path[i]) { newNode = new NodeMiddle(siblings[i], toUpload); } else { newNode = new NodeMiddle(toUpload, siblings[i]); } await this.addNode(newNode); const newRootKey = await this.recalculatePathUntilRoot(path, newNode, siblings.slice(0, i)); this._root = newRootKey; await this._db.setRoot(this._root); break; } if (i === 0) { this._root = toUpload; await this._db.setRoot(this._root); break; } } } async recWalk(key: Hash, f: (n: Node) => Promise<void>): Promise<void> { const n = await this.getNode(key); if (typeof n === 'undefined') { throw ErrNotFound; } switch (n.type) { case NODE_TYPE_EMPTY: await f(n); break; case NODE_TYPE_LEAF: await f(n); break; case NODE_TYPE_MIDDLE: await f(n); await this.recWalk((n as NodeMiddle).childL, f); await this.recWalk((n as NodeMiddle).childR, f); break; default: throw ErrInvalidNodeFound; } } async walk(rootKey: Hash, f: (n: Node) => Promise<void>): Promise<void> { if (bytesEqual(rootKey.value, ZERO_HASH.value)) { rootKey = await this.root(); } await this.recWalk(rootKey, f); } async generateCircomVerifierProof(k: bigint, rootKey: Hash): Promise<CircomVerifierProof> { const cp = await this.generateSCVerifierProof(k, rootKey); cp.siblings = circomSiblingsFromSiblings(cp.siblings, this.maxLevels); return cp; } async generateSCVerifierProof(k: bigint, rootKey: Hash): Promise<CircomVerifierProof> { if (bytesEqual(rootKey.value, ZERO_HASH.value)) { rootKey = await this.root(); } const { proof, value } = await this.generateProof(k, rootKey); const cp = new CircomVerifierProof(); cp.root = rootKey; cp.siblings = proof.allSiblings(); if (typeof proof.nodeAux !== 'undefined') { cp.oldKey = proof.nodeAux.key; cp.oldValue = proof.nodeAux.value; } else { cp.oldKey = ZERO_HASH; cp.oldValue = ZERO_HASH; } cp.key = Hash.fromBigInt(k); cp.value = Hash.fromBigInt(value); if (proof.existence) { cp.fnc = 0; } else { cp.fnc = 1; } return cp; } async generateProof(k: bigint, rootKey?: Hash): Promise<{ proof: Proof; value: bigint }> { let siblingKey: Hash; const kHash = Hash.fromBigInt(k); const path = getPath(this.maxLevels, kHash.value); if (!rootKey) { rootKey = await this.root(); } let nextKey = rootKey; let depth = 0; let existence = false; const siblings: Siblings = []; let nodeAux: NodeAux | undefined; for (depth = 0; depth < this.maxLevels; depth += 1) { const n = await this.getNode(nextKey); if (typeof n === 'undefined') { throw ErrNotFound; } switch (n.type) { case NODE_TYPE_EMPTY: return { proof: new Proof({ existence, nodeAux, siblings }), value: BigInt('0') }; case NODE_TYPE_LEAF: if (bytesEqual(kHash.value, (n as NodeLeaf).entry[0].value)) { existence = true; return { proof: new Proof({ existence, nodeAux, siblings }), value: (n as NodeLeaf).entry[1].bigInt() }; } nodeAux = { key: (n as NodeLeaf).entry[0], value: (n as NodeLeaf).entry[1] }; return { proof: new Proof({ existence, nodeAux, siblings }), value: (n as NodeLeaf).entry[1].bigInt() }; case NODE_TYPE_MIDDLE: if (path[depth]) { nextKey = (n as NodeMiddle).childR; siblingKey = (n as NodeMiddle).childL; } else { nextKey = (n as NodeMiddle).childL; siblingKey = (n as NodeMiddle).childR; } break; default: throw ErrInvalidNodeFound; } siblings.push(siblingKey); } throw ErrKeyNotFound; } async addAndGetCircomProof(k: bigint, v: bigint): Promise<CircomProcessorProof> { const cp = new CircomProcessorProof(); cp.fnc = 2; cp.oldRoot = await this.root(); let key = BigInt('0'); let value = BigInt('0'); let siblings: Siblings = []; try { const res = await this.get(k); key = res.key; value = res.value; siblings = res.siblings; } catch (err) { if (err !== ErrKeyNotFound) { throw err; } } if (typeof key === 'undefined' || typeof value === 'undefined') { throw 'key/value undefined'; } cp.oldKey = Hash.fromBigInt(key); cp.oldValue = Hash.fromBigInt(value); if (bytesEqual(cp.oldKey.value, ZERO_HASH.value)) { cp.isOld0 = true; } cp.siblings = circomSiblingsFromSiblings(siblings, this.maxLevels); await this.add(k, v); cp.newKey = Hash.fromBigInt(k); cp.newValue = Hash.fromBigInt(v); cp.newRoot = await this.root(); return cp; } // NOTE: for now it only prints to console, will be updated in future async graphViz(rootKey: Hash): Promise<void> { let cnt = 0; await this.walk(rootKey, async (n: Node) => { const k = await n.getKey(); let lr: [string, string]; let emptyNodes: string; switch (n.type) { case NODE_TYPE_EMPTY: break; case NODE_TYPE_LEAF: // eslint-disable-next-line no-console console.log(`"${k.string()}" [style=filled]`); break; case NODE_TYPE_MIDDLE: lr = [(n as NodeMiddle).childL.string(), (n as NodeMiddle).childR.string()]; emptyNodes = ''; lr.forEach((s, i) => { if (s === '0') { lr[i] = `empty${cnt}`; emptyNodes += `"${lr[i]}" [style=dashed,label=0];\n`; cnt += 1; } }); // eslint-disable-next-line no-console console.log(`"${k.string()}" -> {"${lr[1]}"}`); // eslint-disable-next-line no-console console.log(emptyNodes); break; default: break; } }); // eslint-disable-next-line no-console console.log(`}\n`); } async printGraphViz(rootKey: Hash): Promise<void> { if (bytesEqual(rootKey.value, ZERO_HASH.value)) { rootKey = await this.root(); } // eslint-disable-next-line no-console console.log( `--------\nGraphViz of the MerkleTree with RootKey ${rootKey.bigInt().toString(10)}\n` ); await this.graphViz(ZERO_HASH); // eslint-disable-next-line no-console console.log( `End of GraphViz of the MerkleTree with RootKey ${rootKey.bigInt().toString(10)}\n--------\n` ); } }