@zk-kit/sparse-merkle-tree
Version:
Sparse Merkle tree implementation in TypeScript.
407 lines (402 loc) • 18.5 kB
JavaScript
/**
* @module @zk-kit/sparse-merkle-tree
* @version 0.2.0
* @file Sparse Merkle tree implementation in TypeScript.
* @copyright Omar Desogus 2022
* @license MIT
* @see [Github]{@link https://github.com/appliedzkp/zk-kit/tree/main/packages/sparse-merkle-tree}
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
/**
* Converts a hexadecimal number to a binary number.
* @param n A hexadecimal number.
* @returns The relative binary number.
*/
function hexToBin(n) {
var bin = Number("0x".concat(n[0])).toString(2);
for (var i = 1; i < n.length; i += 1) {
bin += Number("0x".concat(n[i])).toString(2).padStart(4, "0");
}
return bin;
}
/**
* Returns the binary representation of a key. For each key it is possibile
* to obtain an array of 256 padded bits.
* @param key The key of a tree entry.
* @returns The relative array of bits.
*/
function keyToPath(key) {
var bits = typeof key === "bigint" ? key.toString(2) : hexToBin(key);
return bits.padStart(256, "0").split("").reverse().map(Number);
}
/**
* Returns the index of the last non-zero element of an array.
* If there are only zero elements the function returns -1.
* @param array An array of hexadecimal or big numbers.
* @returns The index of the last non-zero element.
*/
function getIndexOfLastNonZeroElement(array) {
for (var i = array.length - 1; i >= 0; i -= 1) {
if (Number("0x".concat(array[i])) !== 0) {
return i;
}
}
return -1;
}
/**
* Returns the first common elements of two arrays.
* @param array1 The first array.
* @param array2 The second array.
* @returns The array of the first common elements.
*/
function getFirstCommonElements(array1, array2) {
var minArray = array1.length < array2.length ? array1 : array2;
for (var i = 0; i < minArray.length; i += 1) {
if (array1[i] !== array2[i]) {
return minArray.slice(0, i);
}
}
return minArray.slice();
}
/**
* Checks if a number is a hexadecimal number.
* @param n A hexadecimal number.
* @returns True if the number is a hexadecimal, false otherwise.
*/
function checkHex(n) {
return typeof n === "string" && /^[0-9A-Fa-f]{1,64}$/.test(n);
}
/**
* 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.
*/
var SparseMerkleTree = /** @class */ (function () {
/**
* Initializes the SparseMerkleTree attributes.
* @param hash Hash function used to hash the child nodes.
* @param bigNumbers BigInt type enabling.
*/
function SparseMerkleTree(hash, bigNumbers) {
if (bigNumbers === void 0) { 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"]))) {
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'.
*/
SparseMerkleTree.prototype.get = function (key) {
this.checkParameterType(key);
var entry = this.retrieveEntry(key).entry;
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.
*/
SparseMerkleTree.prototype.add = function (key, value) {
this.checkParameterType(key);
this.checkParameterType(value);
var _a = this.retrieveEntry(key), entry = _a.entry, matchingEntry = _a.matchingEntry, siblings = _a.siblings;
if (entry[1] !== undefined) {
throw new Error("Key \"".concat(key, "\" already exists"));
}
var 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.
var 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) {
var matchingPath = keyToPath(matchingEntry[0]);
for (var 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.
var 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.
*/
SparseMerkleTree.prototype.update = function (key, value) {
this.checkParameterType(key);
this.checkParameterType(value);
var _a = this.retrieveEntry(key), entry = _a.entry, siblings = _a.siblings;
if (entry[1] === undefined) {
throw new Error("Key \"".concat(key, "\" does not exist"));
}
var path = keyToPath(key);
// Deletes the old entry and all the nodes in its path.
var 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.
var 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.
*/
SparseMerkleTree.prototype.delete = function (key) {
this.checkParameterType(key);
var _a = this.retrieveEntry(key), entry = _a.entry, siblings = _a.siblings;
if (entry[1] === undefined) {
throw new Error("Key \"".concat(key, "\" does not exist"));
}
var path = keyToPath(key);
// Deletes the entry.
var 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 {
var firstSibling = siblings.pop();
var 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.
*/
SparseMerkleTree.prototype.createProof = function (key) {
this.checkParameterType(key);
var _a = this.retrieveEntry(key), entry = _a.entry, matchingEntry = _a.matchingEntry, siblings = _a.siblings;
// If the key exists the function returns a membership proof, otherwise it
// returns a non-membership proof with the matching entry.
return {
entry: entry,
matchingEntry: matchingEntry,
siblings: 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.
*/
SparseMerkleTree.prototype.verifyProof = function (merkleProof) {
// 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) {
var 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.
var node_1 = merkleProof.entry[1] !== undefined ? this.hash(merkleProof.entry) : this.zeroNode;
var root_1 = this.calculateRoot(node_1, path, merkleProof.siblings);
// If the obtained root is equal to the proof root, then the proof is valid.
return root_1 === 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.
var matchingPath = keyToPath(merkleProof.matchingEntry[0]);
var node = this.hash(merkleProof.matchingEntry);
var root = this.calculateRoot(node, matchingPath, merkleProof.siblings);
if (root === merkleProof.root) {
var path = keyToPath(merkleProof.entry[0]);
// Returns the first common bits of the two keys: the
// non-member key and the matching key.
var 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.
*/
SparseMerkleTree.prototype.retrieveEntry = function (key) {
var path = keyToPath(key);
var siblings = [];
// Starts from the root and goes down into the tree until it finds
// the entry, a zero node or a matching entry.
for (var i = 0, node = this.root; node !== this.zeroNode; i += 1) {
var childNodes = this.nodes.get(node);
var 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: 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: 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];
siblings.push(childNodes[Number(!direction)]);
}
// The path led to a zero node.
return { entry: [key], siblings: 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.
*/
SparseMerkleTree.prototype.calculateRoot = function (node, path, siblings) {
for (var i = siblings.length - 1; i >= 0; i -= 1) {
var 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.
*/
SparseMerkleTree.prototype.addNewNodes = function (node, path, siblings, i) {
if (i === void 0) { i = siblings.length - 1; }
for (; i >= 0; i -= 1) {
var 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.
*/
SparseMerkleTree.prototype.deleteOldNodes = function (node, path, siblings) {
for (var i = siblings.length - 1; i >= 0; i -= 1) {
var 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.
*/
SparseMerkleTree.prototype.isLeaf = function (node) {
var childNodes = this.nodes.get(node);
return !!(childNodes && childNodes[2]);
};
/**
* Checks the parameter type.
* @param parameter The parameter to check.
*/
SparseMerkleTree.prototype.checkParameterType = function (parameter) {
if (this.bigNumbers && typeof parameter !== "bigint") {
throw new Error("Parameter ".concat(parameter, " must be a big number"));
}
if (!this.bigNumbers && !checkHex(parameter)) {
throw new Error("Parameter ".concat(parameter, " must be a hexadecimal"));
}
};
return SparseMerkleTree;
}());
exports.SparseMerkleTree = SparseMerkleTree;
exports.checkHex = checkHex;
exports.getFirstCommonElements = getFirstCommonElements;
exports.getIndexOfLastNonZeroElement = getIndexOfLastNonZeroElement;
exports.hexToBin = hexToBin;
exports.keyToPath = keyToPath;