UNPKG

@iden3/js-merkletree

Version:

javascript sparse merkle tree library

1,413 lines (1,386 loc) 47.7 kB
import { Hex, poseidon } from '@iden3/js-crypto'; const HASH_BYTES_LENGTH = 32; const NODE_TYPE_MIDDLE = 0; // Leaf node.ts with a key and a value const NODE_TYPE_LEAF = 1; // empty node.ts const NODE_TYPE_EMPTY = 2; const NODE_VALUE_BYTE_ARR_LENGTH = 65; const EMPTY_NODE_VALUE = new Uint8Array(NODE_VALUE_BYTE_ARR_LENGTH); const EMPTY_NODE_STRING = 'empty'; const ELEM_BYTES_LEN = 32; const DATA_LEN = 8; const DATA_LEN_BYTES = DATA_LEN * ELEM_BYTES_LEN; const PROOF_FLAG_LEN = 2; const NOT_EMPTIES_LEN = ELEM_BYTES_LEN - PROOF_FLAG_LEN; const qString = '21888242871839275222246405745257275088548364400416034343698204186575808495617'; const FIELD_SIZE = BigInt(qString); const MAX_NUM_IN_FIELD = FIELD_SIZE - BigInt('1'); const checkBigIntInField = (bigNum) => { return bigNum < FIELD_SIZE; }; const bytesEqual = (b1, b2) => { return b1.every((ele, idx) => ele === b2[idx]); }; // TODO: might be make this generic over typed arrays? const swapEndianness = (bytes) => { return bytes.slice().reverse(); }; const bytes2BinaryString = (bytes) => { return '0b' + bytes.reduce((acc, i) => acc + i.toString(2).padStart(8, '0'), ''); }; const testBit = (bitMap, n) => { return (bitMap[parseInt((n / 8).toString())] & (1 << n % 8)) !== 0; }; const testBitBigEndian = (bitMap, n) => { return (bitMap[bitMap.length - parseInt(`${n / 8}`) - 1] & (1 << n % 8)) !== 0; }; // SetBitBigEndian sets the bit n in the bitmap to 1, in Big Endian. const setBitBigEndian = (bitMap, n) => { bitMap[bitMap.length - parseInt(`${n / 8}`) - 1] |= 1 << n % 8; }; const hexTable = '0123456789abcdef'; const bytes2Hex = (u) => { const arr = new Array(u.length * 2); let j = 0; u.forEach((v) => { arr[j] = hexTable[parseInt((v >> 4).toString(10))]; arr[j + 1] = hexTable[parseInt((v & 15).toString(10))]; j += 2; }); return arr.join(''); }; // NOTE: `bytes` should be big endian // bytes received from Hash.value getter are safe to use since their endianness is swapped, for the same reason the private Hash.bytes { stored in little endian } should never be used const newBigIntFromBytes = (bytes) => { if (bytes.length !== HASH_BYTES_LENGTH) { throw `Expected 32 bytes, found ${bytes.length} bytes`; } const bigNum = BigInt(bytes2BinaryString(bytes)); if (!checkBigIntInField(bigNum)) { throw 'NewBigIntFromHashBytes: Value not inside the Finite Field'; } return bigNum; }; const str2Bytes = (str) => new Uint8Array(str.length * 2).map((_, i) => str.charCodeAt(i)); // const siblingBytes = bs.slice(this.notEmpties.length + PROOF_FLAG_LEN); const getPath = (numLevels, k) => { const path = new Array(numLevels); for (let idx = 0; idx < numLevels; idx += 1) { path[idx] = testBit(k, idx); } return path; }; const siblings2Bytes = (siblings) => { const siblingBytesBuff = new ArrayBuffer(HASH_BYTES_LENGTH * siblings.length); const siblingBytes = new Uint8Array(siblingBytesBuff); siblings.forEach((v, i) => { siblingBytes.set(v.value, i * HASH_BYTES_LENGTH); }); return siblingBytes; }; const bigint2Array = (bigNum, radix) => { return bigNum .toString(radix ? radix : 10) .split('') .map((n) => { return parseInt(n); }); }; const bigIntToUINT8Array = (bigNum) => { const n256 = BigInt(256); const bytes = new Uint8Array(HASH_BYTES_LENGTH); let i = 0; while (bigNum > BigInt(0)) { bytes[HASH_BYTES_LENGTH - 1 - i] = Number(bigNum % n256); bigNum = bigNum / n256; i += 1; } return bytes; }; class Hash { // little endian bytes; constructor(_bytes) { if (_bytes?.length) { if (_bytes.length !== HASH_BYTES_LENGTH) { throw new Error(`Expected ${HASH_BYTES_LENGTH} bytes, found ${_bytes.length} bytes`); } this.bytes = _bytes; } else { this.bytes = new Uint8Array(HASH_BYTES_LENGTH); } } // returns a new copy, in little endian get value() { return this.bytes; } // bytes should be in big-endian set value(bytes) { if (bytes.length !== HASH_BYTES_LENGTH) { throw `Expected 32 bytes, found ${bytes.length} bytes`; } this.bytes = swapEndianness(bytes); } string() { return this.bigInt().toString(10); } hex() { return bytes2Hex(this.bytes); } equals(hash) { return bytesEqual(this.value, hash.value); } bigInt() { const bytes = swapEndianness(this.value); return BigInt(bytes2BinaryString(bytes)); } static fromString(s) { try { return Hash.fromBigInt(BigInt(s)); } catch (e) { const deserializedHash = JSON.parse(s); const bytes = Uint8Array.from(Object.values(deserializedHash.bytes)); return new Hash(bytes); } } static fromBigInt(i) { if (!checkBigIntInField(i)) { throw new Error('NewBigIntFromHashBytes: Value not inside the Finite Field'); } const bytes = bigIntToUINT8Array(i); return new Hash(swapEndianness(bytes)); } static fromHex(h) { if (!h) { return ZERO_HASH; } return new Hash(Hex.decodeString(h)); } toJSON() { return this.string(); } } const ZERO_HASH = new Hash(); /** * @deprecated The method should not be used and will be removed in the next major version, * please use Hash.fromBigInt instead */ const newHashFromBigInt = (bigNum) => { return Hash.fromBigInt(bigNum); }; /** * @deprecated The method should not be used and will be removed in the next major version, * please use Hash.fromBigInt instead */ const newHashFromHex = (h) => { return Hash.fromHex(h); }; /** * @deprecated The method should not be used and will be removed in the next major version, * please use Hash.fromBigString instead */ const newHashFromString = (decimalString) => { return Hash.fromString(decimalString); }; const hashElems = (e) => { const hashBigInt = poseidon.hash(e); return Hash.fromBigInt(hashBigInt); }; const hashElemsKey = (k, e) => { const hashBigInt = poseidon.hash([...e, k]); return Hash.fromBigInt(hashBigInt); }; const circomSiblingsFromSiblings = (siblings, levels) => { for (let i = siblings.length; i < levels; i += 1) { siblings.push(ZERO_HASH); } return siblings; }; // in Memory Database implementation class InMemoryDB { prefix; _kvMap; _currentRoot; constructor(_prefix) { this.prefix = _prefix; this._kvMap = {}; this._currentRoot = ZERO_HASH; } async get(k) { const kBytes = new Uint8Array([...this.prefix, ...k]); const val = this._kvMap[kBytes.toString()] ? this._kvMap[kBytes.toString()] : undefined; return val; } async put(k, n) { const kBytes = new Uint8Array([...this.prefix, ...k]); this._kvMap[kBytes.toString()] = n; } async getRoot() { return this._currentRoot; } async setRoot(r) { this._currentRoot = r; } } // LeafKey computes the key of a leaf node given the hIndex and hValue of the // entry of the leaf. const leafKey = async (k, v) => { return hashElemsKey(BigInt(1), [k.bigInt(), v.bigInt()]); }; const nodeValue = (type, a, b) => { const bytes = new Uint8Array(NODE_VALUE_BYTE_ARR_LENGTH); const kBytes = bigIntToUINT8Array(a.bigInt()); const vBytes = bigIntToUINT8Array(b.bigInt()); bytes[0] = type; for (let idx = 1; idx < 33; idx += 1) { bytes[idx] = kBytes[idx - 1]; } for (let idx = 33; idx <= NODE_VALUE_BYTE_ARR_LENGTH; idx += 1) { bytes[idx] = vBytes[idx - 33]; } return bytes; }; class NodeLeaf { type; entry; // cache used to avoid recalculating key _key; constructor(k, v) { this.type = NODE_TYPE_LEAF; this.entry = [k, v]; this._key = ZERO_HASH; } async getKey() { if (this._key === ZERO_HASH) { return await leafKey(this.entry[0], this.entry[1]); } return this._key; } get value() { return nodeValue(this.type, this.entry[0], this.entry[1]); } get string() { return `Leaf I:${this.entry[0]} D:${this.entry[1]}`; } } class NodeMiddle { type; childL; childR; _key; constructor(cL, cR) { this.type = NODE_TYPE_MIDDLE; this.childL = cL; this.childR = cR; this._key = ZERO_HASH; } async getKey() { if (this._key === ZERO_HASH) { return hashElems([this.childL.bigInt(), this.childR.bigInt()]); } return this._key; } get value() { return nodeValue(this.type, this.childL, this.childR); } get string() { return `Middle L:${this.childL} R:${this.childR}`; } } class NodeEmpty { type; _key; constructor() { this.type = NODE_TYPE_EMPTY; this._key = ZERO_HASH; } async getKey() { return ZERO_HASH; } get value() { return EMPTY_NODE_VALUE; } get string() { return EMPTY_NODE_STRING; } } /* eslint-disable no-case-declarations */ class LocalStorageDB { _prefix; _currentRoot; constructor(_prefix) { this._prefix = _prefix; const rootStr = localStorage.getItem(bytes2Hex(_prefix)); if (rootStr) { const bytes = JSON.parse(rootStr); this._currentRoot = new Hash(Uint8Array.from(bytes)); } else { this._currentRoot = ZERO_HASH; } } async get(k) { const kBytes = new Uint8Array([...this._prefix, ...k]); const key = bytes2Hex(kBytes); const val = localStorage.getItem(key); if (val === null) { return undefined; } const obj = JSON.parse(val); switch (obj.type) { case NODE_TYPE_EMPTY: return new NodeEmpty(); case NODE_TYPE_MIDDLE: const cL = new Hash(Uint8Array.from(obj.childL)); const cR = new Hash(Uint8Array.from(obj.childR)); return new NodeMiddle(cL, cR); case NODE_TYPE_LEAF: const k = new Hash(Uint8Array.from(obj.entry[0])); const v = new Hash(Uint8Array.from(obj.entry[1])); return new NodeLeaf(k, v); } throw `error: value found for key ${bytes2Hex(kBytes)} is not of type Node`; } async put(k, n) { const kBytes = new Uint8Array([...this._prefix, ...k]); const key = bytes2Hex(kBytes); const toSerialize = { type: n.type }; if (n instanceof NodeMiddle) { toSerialize.childL = Array.from(n.childL.bytes); toSerialize.childR = Array.from(n.childR.bytes); } else if (n instanceof NodeLeaf) { toSerialize.entry = [Array.from(n.entry[0].bytes), Array.from(n.entry[1].bytes)]; } const val = JSON.stringify(toSerialize); localStorage.setItem(key, val); } async getRoot() { return this._currentRoot; } async setRoot(r) { this._currentRoot = r; localStorage.setItem(bytes2Hex(this._prefix), JSON.stringify(Array.from(r.bytes))); } } function promisifyRequest(request) { return new Promise((resolve, reject) => { // @ts-ignore - file size hacks request.oncomplete = request.onsuccess = () => resolve(request.result); // @ts-ignore - file size hacks request.onabort = request.onerror = () => reject(request.error); }); } function createStore(dbName, storeName) { let dbp; const getDB = () => { if (dbp) return dbp; const request = indexedDB.open(dbName); request.onupgradeneeded = () => request.result.createObjectStore(storeName); dbp = promisifyRequest(request); dbp.then((db) => { // It seems like Safari sometimes likes to just close the connection. // It's supposed to fire this event when that happens. Let's hope it does! db.onclose = () => (dbp = undefined); }, () => { }); return dbp; }; return (txMode, callback) => getDB().then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName))); } let defaultGetStoreFunc; function defaultGetStore() { if (!defaultGetStoreFunc) { defaultGetStoreFunc = createStore('keyval-store', 'keyval'); } return defaultGetStoreFunc; } /** * Get a value by its key. * * @param key * @param customStore Method to get a custom store. Use with caution (see the docs). */ function get(key, customStore = defaultGetStore()) { return customStore('readonly', (store) => promisifyRequest(store.get(key))); } /** * Set a value with a key. * * @param key * @param value * @param customStore Method to get a custom store. Use with caution (see the docs). */ function set(key, value, customStore = defaultGetStore()) { return customStore('readwrite', (store) => { store.put(value, key); return promisifyRequest(store.transaction); }); } class IndexedDBStorage { _prefix; static storageName = 'merkle-tree'; _prefixHash; _store; _currentRoot; constructor(_prefix, databaseName) { this._prefix = _prefix; this._currentRoot = ZERO_HASH; this._prefixHash = bytes2Hex(_prefix); this._store = createStore(`${databaseName ?? IndexedDBStorage.storageName}-db`, IndexedDBStorage.storageName); } async get(k) { const kBytes = new Uint8Array([...this._prefix, ...k]); const key = bytes2Hex(kBytes); const obj = await get(key, this._store); if (obj === null || obj === undefined) { return undefined; } if (obj.type === NODE_TYPE_EMPTY) { return new NodeEmpty(); } if (obj.type === NODE_TYPE_MIDDLE) { const cL = new Hash(Uint8Array.from(obj.childL.bytes)); const cR = new Hash(Uint8Array.from(obj.childR.bytes)); return new NodeMiddle(cL, cR); } if (obj.type === NODE_TYPE_LEAF) { const k = new Hash(Uint8Array.from(obj.entry[0].bytes)); const v = new Hash(Uint8Array.from(obj.entry[1].bytes)); return new NodeLeaf(k, v); } throw new Error(`error: value found for key ${key} is not of type Node`); } async put(k, n) { const kBytes = new Uint8Array([...this._prefix, ...k]); const key = bytes2Hex(kBytes); await set(key, n, this._store); } async getRoot() { if (!this._currentRoot.equals(ZERO_HASH)) { return this._currentRoot; } const root = await get(this._prefixHash, this._store); if (!root) { this._currentRoot = ZERO_HASH; } else { this._currentRoot = new Hash(root.bytes); } return this._currentRoot; } async setRoot(r) { await set(this._prefixHash, r, this._store); this._currentRoot = r; } } class ElemBytes { // Little Endian _bytes; constructor() { this._bytes = new Uint8Array(ELEM_BYTES_LEN); } get value() { return this._bytes; } set value(b) { this._bytes = b; } bigInt() { return newBigIntFromBytes(swapEndianness(this._bytes)); } string() { const hexStr = bytes2Hex(this._bytes.slice(0, 4)); return `${hexStr}...`; } } class Data { _value; constructor() { this._value = new Array(DATA_LEN); } get value() { return this._value; } set value(_v) { if (_v.length !== DATA_LEN) { throw `expected bytes length to be ${DATA_LEN}, got ${_v.length}`; } this._value = _v; } bytes() { const b = new Uint8Array(DATA_LEN * ELEM_BYTES_LEN); for (let idx = 0; idx < DATA_LEN; idx += 1) { this._value[idx].value.forEach((v, _idx) => { b[idx * ELEM_BYTES_LEN + _idx] = v; }); } return b; } equal(d2) { return (bytesEqual(this._value[0].value, d2.value[0].value) && bytesEqual(this._value[1].value, d2.value[1].value) && bytesEqual(this._value[2].value, d2.value[2].value) && bytesEqual(this._value[3].value, d2.value[3].value)); } } const newDataFromBytes = (bytes) => { if (bytes.length !== DATA_LEN_BYTES) { throw `expected bytes length to be ${DATA_LEN_BYTES}, got ${bytes.length}`; } const d = new Data(); const arr = new Array(DATA_LEN_BYTES); for (let i = 0; i < DATA_LEN; i += 1) { const tmp = new ElemBytes(); tmp.value = bytes.slice(i * ELEM_BYTES_LEN, (i + 1) * DATA_LEN_BYTES); arr[i] = tmp; } d.value = arr; return d; }; class Entry { _data; _hIndex; _hValue; constructor(_data) { this._data = _data ? _data : new Data(); this._hIndex = ZERO_HASH; this._hValue = ZERO_HASH; } get data() { return this._data; } get index() { return this._data.value.slice(0, 4); } get value() { return this._data.value.slice(4, 8); } async hIndex() { if (this._hIndex === ZERO_HASH) { return hashElems(elemBytesToBigInts(this.index)); } return this._hIndex; } async hValue() { if (this._hValue === ZERO_HASH) { return hashElems(elemBytesToBigInts(this.value)); } return this._hValue; } hiHv() { return (async () => { const hi = await this.hIndex(); const hv = await this.hValue(); return { hi, hv }; })(); } bytes() { return this._data.value; } equal(e2) { return this._data.equal(e2.data); } clone() { return new Entry(this._data); } } const elemBytesToBigInts = (es) => { const bigInts = es.map((e) => { return e.bigInt(); }); return bigInts; }; const checkEntryInField = (e) => { const bigInts = elemBytesToBigInts(e.data.value); let flag = true; bigInts.forEach((b) => { if (!checkBigIntInField(b)) { flag = false; } }); return flag; }; const ErrNodeKeyAlreadyExists = 'key already exists'; // ErrKeyNotFound is used when a key is not found in the MerkleTree. const ErrKeyNotFound = 'Key not found in the MerkleTree'; // ErrNodeBytesBadSize is used when the data of a node has an incorrect // size and can't be parsed. const ErrNodeBytesBadSize = 'node data has incorrect size in the DB'; // ErrReachedMaxLevel is used when a traversal of the MT reaches the // maximum level. const ErrReachedMaxLevel = 'reached maximum level of the merkle tree'; // ErrInvalidNodeFound is used when an invalid node is found and can't // be parsed. const ErrInvalidNodeFound = 'found an invalid node in the DB'; // ErrInvalidProofBytes is used when a serialized proof is invalid. const ErrInvalidProofBytes = 'the serialized proof is invalid'; // ErrInvalidDBValue is used when a value in the key value DB is // invalid (for example, it doesn't contain a byte header and a []byte // body of at least len=1. const ErrInvalidDBValue = 'the value in the DB is invalid'; // ErrEntryIndexAlreadyExists is used when the entry index already // exists in the tree. const ErrEntryIndexAlreadyExists = 'the entry index already exists in the tree'; // ErrNotWritable is used when the MerkleTree is not writable and a // write function is called const ErrNotWritable = 'Merkle Tree not writable'; const ErrNotFound = 'key not found'; class CircomVerifierProof { root; siblings; oldKey; oldValue; isOld0; key; value; // 0: inclusion, 1: non inclusion fnc; constructor(_root = ZERO_HASH, _siblings = [], _oldKey = ZERO_HASH, _oldValue = ZERO_HASH, _isOld0 = false, _key = ZERO_HASH, _value = ZERO_HASH, _fnc = 0) { this.root = _root; this.siblings = _siblings; this.oldKey = _oldKey; this.oldValue = _oldValue; this.isOld0 = _isOld0; this.key = _key; this.value = _value; this.fnc = _fnc; } } class CircomProcessorProof { oldRoot; newRoot; siblings; oldKey; oldValue; newKey; newValue; isOld0; // 0: NOP, 1: Update, 2: Insert, 3: Delete fnc; constructor(_oldRoot = ZERO_HASH, _newRoot = ZERO_HASH, _siblings = [], _oldKey = ZERO_HASH, _oldValue = ZERO_HASH, _newKey = ZERO_HASH, _newValue = ZERO_HASH, _isOld0 = false, _fnc = 0) { this.oldRoot = _oldRoot; this.newRoot = _newRoot; this.siblings = _siblings; this.oldKey = _oldKey; this.oldValue = _oldValue; this.newKey = _newKey; this.newValue = _newValue; this.isOld0 = _isOld0; this.fnc = _fnc; } } const ErrNodeAuxNonExistAgainstHIndex = 'non-existence proof being checked against hIndex equal to nodeAux'; class Proof { existence; depth; // notEmpties is a bitmap of non-empty siblings found in siblings notEmpties; siblings; nodeAux; constructor(obj) { this.existence = obj?.existence ?? false; this.depth = 0; this.nodeAux = obj?.nodeAux; const { siblings, notEmpties } = this.reduceSiblings(obj?.siblings); this.siblings = siblings; this.notEmpties = notEmpties; } bytes() { let bsLen = PROOF_FLAG_LEN + this.notEmpties.length + ELEM_BYTES_LEN * this.siblings.length; if (typeof this.nodeAux !== 'undefined') { bsLen += 2 * ELEM_BYTES_LEN; } const arrBuff = new ArrayBuffer(bsLen); const bs = new Uint8Array(arrBuff); if (!this.existence) { bs[0] |= 1; } bs[1] = this.depth; bs.set(this.notEmpties, PROOF_FLAG_LEN); const siblingBytes = siblings2Bytes(this.siblings); bs.set(siblingBytes, this.notEmpties.length + PROOF_FLAG_LEN); if (typeof this.nodeAux !== 'undefined') { bs[0] |= 2; bs.set(this.nodeAux.key.value, bs.length - 2 * ELEM_BYTES_LEN); bs.set(this.nodeAux.value.value, bs.length - 1 * ELEM_BYTES_LEN); } return bs; } toJSON() { return { existence: this.existence, siblings: this.allSiblings().map((s) => s.toJSON()), node_aux: this.nodeAux ? { key: this.nodeAux.key.toJSON(), value: this.nodeAux.value.toJSON() } : undefined }; } reduceSiblings(siblings) { const reducedSiblings = []; const notEmpties = new Uint8Array(NOT_EMPTIES_LEN); if (!siblings) { return { siblings: reducedSiblings, notEmpties }; } for (let i = 0; i < siblings.length; i++) { const sibling = siblings[i]; if (JSON.stringify(siblings[i]) !== JSON.stringify(ZERO_HASH)) { setBitBigEndian(notEmpties, i); reducedSiblings.push(sibling); this.depth = i + 1; } } return { notEmpties, siblings: reducedSiblings }; } static fromJSON(obj) { let nodeAux = undefined; const nodeAuxJson = obj.node_aux ?? obj.nodeAux; // we keep backward compatibility and support both representations if (nodeAuxJson) { nodeAux = { key: Hash.fromString(nodeAuxJson.key), value: Hash.fromString(nodeAuxJson.value) }; } const existence = obj.existence ?? false; const siblings = obj.siblings.map((s) => Hash.fromString(s)); return new Proof({ existence, nodeAux, siblings }); } allSiblings() { return Proof.buildAllSiblings(this.depth, this.notEmpties, this.siblings); } static buildAllSiblings(depth, notEmpties, siblings) { let sibIdx = 0; const allSiblings = []; for (let i = 0; i < depth; i += 1) { if (testBitBigEndian(notEmpties, i)) { allSiblings.push(siblings[sibIdx]); sibIdx += 1; } else { allSiblings.push(ZERO_HASH); } } return allSiblings; } } /** * @deprecated The method should not be used and will be removed in the next major version, * please use proof.allSiblings instead */ // eslint-disable-next-line @cspell/spellchecker const siblignsFroomProof = (proof) => { return proof.allSiblings(); }; const verifyProof = async (rootKey, proof, k, v) => { try { const rFromProof = await rootFromProof(proof, k, v); return bytesEqual(rootKey.value, rFromProof.value); } catch (err) { if (err === ErrNodeAuxNonExistAgainstHIndex) { return false; } throw err; } }; const rootFromProof = async (proof, k, v) => { const kHash = Hash.fromBigInt(k); const vHash = Hash.fromBigInt(v); let midKey; if (proof.existence) { midKey = await leafKey(kHash, vHash); } else { if (typeof proof.nodeAux === 'undefined') { midKey = ZERO_HASH; } else { const nodeAux = proof.nodeAux; if (bytesEqual(kHash.value, nodeAux.key.value)) { throw ErrNodeAuxNonExistAgainstHIndex; } midKey = await leafKey(nodeAux.key, nodeAux.value); } } const siblings = proof.allSiblings(); const path = getPath(siblings.length, kHash.value); for (let i = siblings.length - 1; i >= 0; i -= 1) { if (path[i]) { midKey = await new NodeMiddle(siblings[i], midKey).getKey(); } else { midKey = await new NodeMiddle(midKey, siblings[i]).getKey(); } } return midKey; }; class Merkletree { _db; _root; _writable; _maxLevel; constructor(_db, _writable, _maxLevels) { this._db = _db; this._writable = _writable; this._maxLevel = _maxLevels; } async root() { if (!this._root) { this._root = await this._db.getRoot(); } return this._root; } get maxLevels() { return this._maxLevel; } async add(k, v) { 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) { 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) { 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) { 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, oldLeaf, lvl, pathNewLeaf, pathOldLeaf) { if (lvl > this._maxLevel - 2) { throw new Error(ErrReachedMaxLevel); } let newNodeMiddle; 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, key, lvl, path) { 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.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: { let newNodeMiddle; if (path[lvl]) { const nextKey = await this.addLeaf(newLeaf, n.childR, lvl + 1, path); newNodeMiddle = new NodeMiddle(n.childL, nextKey); } else { const nextKey = await this.addLeaf(newLeaf, n.childL, lvl + 1, path); newNodeMiddle = new NodeMiddle(nextKey, n.childR); } return this.addNode(newNodeMiddle); } default: { throw ErrInvalidNodeFound; } } } async get(k) { const kHash = Hash.fromBigInt(k); const path = getPath(this.maxLevels, kHash.value); let nextKey = await this.root(); const 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.entry[0].bigInt(), value: n.entry[1].bigInt(), siblings }; case NODE_TYPE_MIDDLE: if (path[i]) { nextKey = n.childR; siblings.push(n.childL); } else { nextKey = n.childL; siblings.push(n.childR); } break; default: throw ErrInvalidNodeFound; } } throw new Error(ErrReachedMaxLevel); } async update(k, v) { 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 = []; 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.entry[0].value)) { cp.oldValue = n.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.childR; siblings.push(n.childL); } else { nextKey = n.childL; siblings.push(n.childR); } break; default: throw ErrInvalidNodeFound; } } throw ErrKeyNotFound; } async getNode(k) { if (bytesEqual(k.value, ZERO_HASH.value)) { return new NodeEmpty(); } return await this._db.get(k.value); } async recalculatePathUntilRoot(path, node, siblings) { 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) { if (!this._writable) { throw ErrNotWritable; } const kHash = Hash.fromBigInt(k); const path = getPath(this.maxLevels, kHash.value); let nextKey = this._root; const 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.entry[0].value)) { await this.rmAndUpload(path, kHash, siblings); return; } throw ErrKeyNotFound; case NODE_TYPE_MIDDLE: if (path[i]) { nextKey = n.childR; siblings.push(n.childL); } else { nextKey = n.childL; siblings.push(n.childR); } break; default: throw ErrInvalidNodeFound; } } throw ErrKeyNotFound; } async rmAndUpload(path, kHash, siblings) { 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; 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; 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, f) { 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.childL, f); await this.recWalk(n.childR, f); break; default: throw ErrInvalidNodeFound; } } async walk(rootKey, f) { if (bytesEqual(rootKey.value, ZERO_HASH.value)) { rootKey = await this.root(); } await this.recWalk(rootKey, f); } async generateCircomVerifierProof(k, rootKey) { const cp = await this.generateSCVerifierProof(k, rootKey); cp.siblings = circomSiblingsFromSiblings(cp.siblings, this.maxLevels); return cp; } async generateSCVerifierProof(k, rootKey) { 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, rootKey) { let siblingKey; 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 = []; let nodeAux; 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.entry[0].value)) { existence = true; return { proof: new Proof({ existence, nodeAux, siblings }), value: n.entry[1].bigInt() }; } nodeAux = { key: n.entry[0], value: n.entry[1] }; return { proof: new Proof({ existence, nodeAux, siblings }), value: n.entry[1].bigInt() }; case NODE_TYPE_MIDDLE: if (path[depth]) { nextKey = n.childR; siblingKey = n.childL; } else { nextKey = n.childL; siblingKey = n.childR; } break; default: throw ErrInvalidNodeFound; } siblings.push(siblingKey); } throw ErrKeyNotFound; } async addAndGetCircomProof(k, v) { const cp = new CircomProcessorProof(); cp.fnc = 2; cp.oldRoot = await this.root(); let key = BigInt('0'); let value = BigInt('0'); let 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) { let cnt = 0; await this.walk(rootKey, async (n) => { const k = await n.getKey(); let lr; let emptyNodes; 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.childL.string(), n.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; } }); // eslint-disable-next-line no-console console.log(`}\n`); } async printGraphViz(rootKey) { 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`); } } export { CircomProcessorProof, CircomVerifierProof, DATA_LEN, DATA_LEN_BYTES, Data, ELEM_BYTES_LEN, EMPTY_NODE_STRING, EMPTY_NODE_VALUE, ElemBytes, Entry, ErrEntryIndexAlreadyExists, ErrInvalidDBValue, ErrInvalidNodeFound, ErrInvalidProofBytes, ErrKeyNotFound, ErrNodeBytesBadSize, ErrNodeKeyAlreadyExists, ErrNotFound, ErrNotWritable, ErrReachedMaxLevel, FIELD_SIZE, HASH_BYTES_LENGTH, Hash, InMemoryDB, IndexedDBStorage, LocalStorageDB, MAX_NUM_IN_FIELD, Merkletree, NODE_TYPE_EMPTY, NODE_TYPE_LEAF, NODE_TYPE_MIDDLE, NODE_VALUE_BYTE_ARR_LENGTH, NOT_EMPTIES_LEN, NodeEmpty, NodeLeaf, NodeMiddle, PROOF_FLAG_LEN, Proof, ZERO_HASH, bigIntToUINT8Array, bigint2Array, bytes2BinaryString, bytes2Hex, bytesEqual, checkBigIntInField, checkEntryInField, circomSiblingsFromSiblings, elemBytesToBigInts, getPath, hashElems, hashElemsKey, newBigIntFromBytes, newDataFromBytes, newHashFromBigInt, newHashFromHex, newHashFromString, rootFromProof, setBitBigEndian, siblignsFroomProof, siblings2Bytes, str2Bytes, swapEndianness, testBit, testBitBigEndian, verifyProof }; //# sourceMappingURL=index.js.map