import { checkHex, getFirstCommonElements, getIndexOfLastNonZeroElement, keyToPath } from "./utils" import { ChildNodes, EntryMark, HashFunction, Key, Value, Node, Siblings, EntryResponse, MerkleProof } from "./types" /** * SparseMerkleTree class provides all the functions to create a sparse Merkle tree * and to take advantage of its features: {@linkcode SparseMerkleTree.add}, {@linkcode SparseMerkleTree.get}, * {@linkcode SparseMerkleTree.update}, {@linkcode SparseMerkleTree.delete}, {@linkcode SparseMerkleTree.createProof}, * {@linkcode SparseMerkleTree.verifyProof}. * To better understand the code below it may be useful to describe the terminology used: * * **nodes**: every node in the tree is the hash of the two child nodes (`H(x, y)`); * * **root node**: the root node is the top hash and since it represents the whole data * structure it can be used to certify its integrity; * * **leaf nodes**: every leaf node is the hash of a key/value pair and an additional * value to mark the node as leaf node (`H(x, y, 1)`); * * **entry**: a tree entry is a key/value pair used to create the leaf nodes; * * **zero nodes**: a zero node is an hash of zeros and in this implementation `H(0,0) = 0`; * * **siblings**: the children of a parent node are siblings; * * **path**: every entry key is a number < 2^256 that can be converted in a binary number, * and this binary number is the path used to place the entry in the tree (1 or 0 define the * child node to choose); * * **matching node**: when an entry is not found and the path leads to another existing entry, * this entry is a matching entry and it has some of the first bits in common with the entry not found; * * **depth**: the depth of a node is the length of the path to its root. */ export default class SparseMerkleTree { // Hash function used to hash the child nodes. private hash: HashFunction // Value for zero nodes. private zeroNode: Node // Additional entry value to mark the leaf nodes. private entryMark: EntryMark // If true it sets `BigInt` type as default type of the tree hashes. private bigNumbers: boolean // Key/value map in which the key is a node of the tree and // the value is an array of child nodes. When the node is // a leaf node the child nodes are an entry (key/value) of the tree. private nodes: Map // The root node of the tree. root: Node /** * Initializes the SparseMerkleTree attributes. * @param hash Hash function used to hash the child nodes. * @param bigNumbers BigInt type enabling. */ constructor(hash: HashFunction, bigNumbers = false) { if (bigNumbers) { /* istanbul ignore next */ if (typeof BigInt !== "function") { throw new Error("Big numbers are not supported") } if (typeof hash([BigInt(1), BigInt(1)]) !== "bigint") { throw new Error("The hash function must return a big number") } } else if (!checkHex(hash(["1", "1"]) as string)) { throw new Error("The hash function must return a hexadecimal") } this.hash = hash this.bigNumbers = bigNumbers this.zeroNode = bigNumbers ? BigInt(0) : "0" this.entryMark = bigNumbers ? BigInt(1) : "1" this.nodes = new Map() this.root = this.zeroNode // The root node is initially a zero node. } /** * Gets a key and if the key exists in the tree the function returns the * value, otherwise it returns 'undefined'. * @param key A key of a tree entry. * @returns A value of a tree entry or 'undefined'. */ get(key: Key): Value | undefined { this.checkParameterType(key) const { entry } = this.retrieveEntry(key) return entry[1] } /** * Adds a new entry in the tree. It retrieves a matching entry * or a zero node with a top-down approach and then it updates all the * hashes of the nodes in the path of the new entry with a bottom-up approach. * @param key The key of the new entry. * @param value The value of the new entry. */ add(key: Key, value: Value) { this.checkParameterType(key) this.checkParameterType(value) const { entry, matchingEntry, siblings } = this.retrieveEntry(key) if (entry[1] !== undefined) { throw new Error(`Key "${key}" already exists`) } const path = keyToPath(key) // If there is a matching entry its node is saved, otherwise // the node is a zero node. This node is used below as the first node // (starting from the bottom of the tree) to obtain the new nodes // up to the root. const node = matchingEntry ? this.hash(matchingEntry) : this.zeroNode // If there are siblings it deletes all the nodes of the path. // These nodes will be re-created below with the new hashes. if (siblings.length > 0) { this.deleteOldNodes(node, path, siblings) } // If there is a matching entry, further N zero siblings are added // in the `siblings` array, followed by the matching node itself. // N is the number of the first matching bits of the paths. // This is helpful in the non-membership proof verification // as explained in the function below. if (matchingEntry) { const matchingPath = keyToPath(matchingEntry[0]) for (let i = siblings.length; matchingPath[i] === path[i]; i += 1) { siblings.push(this.zeroNode) } siblings.push(node) } // Adds the new entry and re-creates the nodes of the path with the new hashes // with a bottom-up approach. The `addNewNodes` function returns the last node // added, which is the root node. const newNode = this.hash([key, value, this.entryMark]) this.nodes.set(newNode, [key, value, this.entryMark]) this.root = this.addNewNodes(newNode, path, siblings) } /** * Updates a value of an entry in the tree. Also in this case * all the hashes of the nodes in the path of the entry are updated * with a bottom-up approach. * @param key The key of the entry. * @param value The value of the entry. */ update(key: Key, value: Value) { this.checkParameterType(key) this.checkParameterType(value) const { entry, siblings } = this.retrieveEntry(key) if (entry[1] === undefined) { throw new Error(`Key "${key}" does not exist`) } const path = keyToPath(key) // Deletes the old entry and all the nodes in its path. const oldNode = this.hash(entry) this.nodes.delete(oldNode) this.deleteOldNodes(oldNode, path, siblings) // Adds the new entry and re-creates the nodes of the path // with the new hashes. const newNode = this.hash([key, value, this.entryMark]) this.nodes.set(newNode, [key, value, this.entryMark]) this.root = this.addNewNodes(newNode, path, siblings) } /** * Deletes an entry in the tree. Also in this case all the hashes of * the nodes in the path of the entry are updated with a bottom-up approach. * @param key The key of the entry. */ delete(key: Key) { this.checkParameterType(key) const { entry, siblings } = this.retrieveEntry(key) if (entry[1] === undefined) { throw new Error(`Key "${key}" does not exist`) } const path = keyToPath(key) // Deletes the entry. const node = this.hash(entry) this.nodes.delete(node) this.root = this.zeroNode // If there are siblings it deletes the nodes of the path and // re-creates them with the new hashes. if (siblings.length > 0) { this.deleteOldNodes(node, path, siblings) // If the last siblings is not a leaf node, it adds all the // nodes of the path starting from a zero node, otherwise // it removes the last non-zero siblings from the `siblings` // array and it starts from it by skipping the last zero nodes. if (!this.isLeaf(siblings[siblings.length - 1])) { this.root = this.addNewNodes(this.zeroNode, path, siblings) } else { const firstSibling = siblings.pop() as Node const i = getIndexOfLastNonZeroElement(siblings) this.root = this.addNewNodes(firstSibling, path, siblings, i) } } } /** * Creates a proof to prove the membership or the non-membership * of a tree entry. * @param key A key of an existing or a non-existing entry. * @returns The membership or the non-membership proof. */ createProof(key: Key): MerkleProof { this.checkParameterType(key) const { entry, matchingEntry, siblings } = this.retrieveEntry(key) // If the key exists the function returns a membership proof, otherwise it // returns a non-membership proof with the matching entry. return { entry, matchingEntry, siblings, root: this.root, membership: !!entry[1] } } /** * Verifies a membership or a non-membership proof. * @param merkleProof The proof to verify. * @returns True if the proof is valid, false otherwise. */ verifyProof(merkleProof: MerkleProof): boolean { // If there is not a matching entry it simply obtains the root // hash by using the siblings and the path of the key. if (!merkleProof.matchingEntry) { const path = keyToPath(merkleProof.entry[0]) // If there is not an entry value the proof is a non-membership proof, // and in this case, since there is not a matching entry, the node // is a zero node. If there is an entry value the proof is a // membership proof and the node is the hash of the entry. const node = merkleProof.entry[1] !== undefined ? this.hash(merkleProof.entry) : this.zeroNode const root = this.calculateRoot(node, path, merkleProof.siblings) // If the obtained root is equal to the proof root, then the proof is valid. return root === merkleProof.root } // If there is a matching entry the proof is definitely a non-membership // proof. In this case it checks if the matching node belongs to the tree // and then it checks if the number of the first matching bits of the keys // is greater than or equal to the number of the siblings. const matchingPath = keyToPath(merkleProof.matchingEntry[0]) const node = this.hash(merkleProof.matchingEntry) const root = this.calculateRoot(node, matchingPath, merkleProof.siblings) if (root === merkleProof.root) { const path = keyToPath(merkleProof.entry[0]) // Returns the first common bits of the two keys: the // non-member key and the matching key. const firstMatchingBits = getFirstCommonElements(path, matchingPath) // If the non-member key was a key of a tree entry, the depth of the // matching node should be greater than the number of the first common // bits of the keys. The depth of a node can be defined by the number // of its siblings. return merkleProof.siblings.length <= firstMatchingBits.length } return false } /** * Searches for an entry in the tree. If the key passed as parameter exists in * the tree, the function returns the entry, otherwise it returns the entry * with only the key, and when there is another existing entry * in the same path it returns also this entry as 'matching entry'. * In any case the function returns the siblings of the path. * @param key The key of the entry to search for. * @returns The entry response. */ private retrieveEntry(key: Key): EntryResponse { const path = keyToPath(key) const siblings: Siblings = [] // Starts from the root and goes down into the tree until it finds // the entry, a zero node or a matching entry. for (let i = 0, node = this.root; node !== this.zeroNode; i += 1) { const childNodes = this.nodes.get(node) as ChildNodes const direction = path[i] // If the third position of the array is not empty the child nodes // are an entry of the tree. if (childNodes[2]) { if (childNodes[0] === key) { // An entry with the same key was found and // it returns it with the siblings. return { entry: childNodes, siblings } } // The entry found does not have the same key. But the key of this // particular entry matches the first 'i' bits of the key passed // as parameter and it can be useful in several functions. return { entry: [key], matchingEntry: childNodes, siblings } } // When it goes down into the tree and follows the path, in every step // a node is chosen between the left and the right child nodes, and the // opposite node is saved as siblings. node = childNodes[direction] as Node siblings.push(childNodes[Number(!direction)] as Node) } // The path led to a zero node. return { entry: [key], siblings } } /** * Calculates nodes with a bottom-up approach until it reaches the root node. * @param node The node to start from. * @param path The path of the key. * @param siblings The siblings of the path. * @returns The root node. */ private calculateRoot(node: Node, path: number[], siblings: Siblings): Node { for (let i = siblings.length - 1; i >= 0; i -= 1) { const childNodes: ChildNodes = path[i] ? [siblings[i], node] : [node, siblings[i]] node = this.hash(childNodes) } return node } /** * Adds new nodes in the tree with a bottom-up approach until it reaches the root node. * @param node The node to start from. * @param path The path of the key. * @param siblings The siblings of the path. * @param i The index to start from. * @returns The root node. */ private addNewNodes(node: Node, path: number[], siblings: Siblings, i = siblings.length - 1): Node { for (; i >= 0; i -= 1) { const childNodes: ChildNodes = path[i] ? [siblings[i], node] : [node, siblings[i]] node = this.hash(childNodes) this.nodes.set(node, childNodes) } return node } /** * Deletes nodes in the tree with a bottom-up approach until it reaches the root node. * @param node The node to start from. * @param path The path of the key. * @param siblings The siblings of the path. * @param i The index to start from. */ private deleteOldNodes(node: Node, path: number[], siblings: Siblings) { for (let i = siblings.length - 1; i >= 0; i -= 1) { const childNodes: ChildNodes = path[i] ? [siblings[i], node] : [node, siblings[i]] node = this.hash(childNodes) this.nodes.delete(node) } } /** * Checks if a node is a leaf node. * @param node A node of the tree. * @returns True if the node is a leaf, false otherwise. */ private isLeaf(node: Node): boolean { const childNodes = this.nodes.get(node) return !!(childNodes && childNodes[2]) } /** * Checks the parameter type. * @param parameter The parameter to check. */ private checkParameterType(parameter: Key | Value) { if (this.bigNumbers && typeof parameter !== "bigint") { throw new Error(`Parameter ${parameter} must be a big number`) } if (!this.bigNumbers && !checkHex(parameter as string)) { throw new Error(`Parameter ${parameter} must be a hexadecimal`) } } }