mirror of
https://github.com/privacy-scaling-explorations/zk-kit.git
synced 2026-04-22 03:00:15 -04:00
feat(sparse-merkle-tree): create @zk-kit/sparse-merkle-tree pkg
This commit is contained in:
184
packages/sparse-merkle-tree/README.md
Normal file
184
packages/sparse-merkle-tree/README.md
Normal file
@@ -0,0 +1,184 @@
|
||||
<p align="center">
|
||||
<h1 align="center">
|
||||
Sparse Merkle tree
|
||||
</h1>
|
||||
<p align="center">Sparse Merkle tree implementation in TypeScript.</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/appliedzkp/zk-kit">
|
||||
<img src="https://img.shields.io/badge/project-zk--kit-blue.svg?style=flat-square">
|
||||
</a>
|
||||
<a href="https://github.com/appliedzkp/zk-kit/blob/main/LICENSE">
|
||||
<img alt="Github license" src="https://img.shields.io/github/license/appliedzkp/zk-kit.svg?style=flat-square">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/@zk-kit/sparse-merkle-tree">
|
||||
<img alt="NPM version" src="https://img.shields.io/npm/v/@zk-kit/sparse-merkle-tree?style=flat-square" />
|
||||
</a>
|
||||
<a href="https://npmjs.org/package/@zk-kit/sparse-merkle-tree">
|
||||
<img alt="Downloads" src="https://img.shields.io/npm/dm/@zk-kit/sparse-merkle-tree.svg?style=flat-square" />
|
||||
</a>
|
||||
<a href="https://bundlephobia.com/package/@zk-kit/sparse-merkle-tree">
|
||||
<img alt="npm bundle size (scoped)" src="https://img.shields.io/bundlephobia/minzip/@zk-kit/sparse-merkle-tree" />
|
||||
</a>
|
||||
<a href="https://eslint.org/">
|
||||
<img alt="Linter eslint" src="https://img.shields.io/badge/linter-eslint-8080f2?style=flat-square&logo=eslint" />
|
||||
</a>
|
||||
<a href="https://prettier.io/">
|
||||
<img alt="Code style prettier" src="https://img.shields.io/badge/code%20style-prettier-f8bc45?style=flat-square&logo=prettier" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
A sparse Merkle tree is a data structure useful for storing a key/value map where every leaf node of the tree contains the cryptographic hash of a key/value pair and every non leaf node contains the concatenated hashes of its child nodes. Sparse Merkle trees provides a secure and efficient verification of large data sets and they are often used in peer-to-peer technologies. This implementation is an optimized version of the traditional sparse Merkle tree and it is based on the concepts expressed in the papers and resources below.
|
||||
|
||||
**Notice**: this library is still not stable and therefore it must be used with care.
|
||||
|
||||
## References
|
||||
|
||||
1. Rasmus Dahlberg, Tobias Pulls and Roel Peeters. _Efficient Sparse Merkle Trees: Caching Strategies and Secure (Non-)Membership Proofs_. Cryptology ePrint Archive: Report 2016/683, 2016. https://eprint.iacr.org/2016/683.
|
||||
2. Faraz Haider. _Compact sparse merkle trees_. Cryptology ePrint Archive: Report 2018/955, 2018. https://eprint.iacr.org/2018/955.
|
||||
3. Jordi Baylina and Marta Bellés. _Sparse Merkle Trees_. https://docs.iden3.io/publications/pdfs/Merkle-Tree.pdf.
|
||||
4. Vitalik Buterin Fichter. _Optimizing sparse Merkle trees_. https://ethresear.ch/t/optimizing-sparse-merkle-trees/3751.
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
### npm or yarn
|
||||
|
||||
You can install `@zk-kit/sparse-merkle-tree` package with npm:
|
||||
|
||||
```bash
|
||||
npm i @zk-kit/sparse-merkle-tree --save
|
||||
```
|
||||
|
||||
or yarn:
|
||||
|
||||
```bash
|
||||
yarn add @zk-kit/sparse-merkle-tree
|
||||
```
|
||||
|
||||
### CDN
|
||||
|
||||
You can also load it using a `script` tag using [unpkg](https://unpkg.com/):
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/@zk-kit/sparse-merkle-tree/"></script>
|
||||
```
|
||||
|
||||
or [JSDelivr](https://www.jsdelivr.com/):
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/@zk-kit/sparse-merkle-tree/"></script>
|
||||
```
|
||||
|
||||
## 📜 Usage
|
||||
|
||||
\# **new SparseMerkleTree**(hash: _HashFunction_, bigNumbers?: _boolean_): _SparseMerkleTree_
|
||||
|
||||
```typescript
|
||||
import { SparseMerkleTree } from "@zk-kit/sparse-merkle-tree"
|
||||
import { sha256 } from "js-sha256"
|
||||
import { poseidon } from "circomlibjs"
|
||||
|
||||
// Hexadecimal hashes.
|
||||
const hash = (childNodes: ChildNodes) => sha256(childNodes.join(""))
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
// Big number hashes.
|
||||
const hash2 = (childNodes: ChildNodes) => poseidon(childNodes)
|
||||
const tree2 = new SparseMerkleTree(hash2, true)
|
||||
|
||||
console.log(tree.root) // 0
|
||||
console.log(tree2.root) // 0n
|
||||
```
|
||||
|
||||
\# **add**(key: _string_ | _number_, value: _string_ | _number_): _void_
|
||||
|
||||
```typescript
|
||||
tree.add("2b", "44") // Hexadecimal key/value.
|
||||
tree.add("16", "78")
|
||||
tree.add("d", "e7")
|
||||
tree.add("10", "141")
|
||||
tree.add("20", "340")
|
||||
|
||||
console.log(tree.root) // 31ee2a59741c9c32a32d8c7fafe461cca1ccaf5986c2d592586e3e6482a48645
|
||||
```
|
||||
|
||||
\# **get**(key: _string_ | _number_): _undefined_ | _string_
|
||||
|
||||
```typescript
|
||||
const value = tree.get("16")
|
||||
|
||||
console.log(value) // 78
|
||||
```
|
||||
|
||||
\# **update**(key: _string_ | _number_, value: _string_ | _number_): _void_
|
||||
|
||||
```typescript
|
||||
tree.update("16", "79")
|
||||
|
||||
const value = tree.get("16")
|
||||
|
||||
console.log(value) // 79
|
||||
```
|
||||
|
||||
\# **delete**(key: _string_ | _number_): _void_
|
||||
|
||||
```typescript
|
||||
tree.delete("16")
|
||||
|
||||
const value = tree.get("16")
|
||||
|
||||
console.log(value) // undefined
|
||||
```
|
||||
|
||||
\# **createProof**(key: _string_ | _number_): _Proof_
|
||||
|
||||
```typescript
|
||||
const membershipProof = tree.createProof("2b")
|
||||
const nonMembershipProof = tree.createProof("16") // This key has been deleted.
|
||||
|
||||
console.log(membershipProof)
|
||||
/*
|
||||
{
|
||||
entry: [ '2b', '44', '1' ],
|
||||
matchingEntry: undefined,
|
||||
sidenodes: [
|
||||
'006a0ab15a212e0e0126b81e056b11576628b1ad80792403dbb3a90be2e71d64',
|
||||
'f786ce5a843614d7da216d95c0087c1eb29244927feeeeeb658aa60cf124cd5e'
|
||||
],
|
||||
root: 'c3c023c84afc0a7bab1dbebcef5f7beaf3d6af4af98e8f481620dec052be7d0d',
|
||||
membership: true
|
||||
}
|
||||
*/
|
||||
|
||||
console.log(nonMembershipProof)
|
||||
/*
|
||||
{
|
||||
entry: [ '16' ],
|
||||
matchingEntry: undefined,
|
||||
sidenodes: [
|
||||
'960f23d9fbb44241be53efb7c4d69ac129bb1cb9482dcb6789d3cc7e6de2de2b',
|
||||
'2a1aef839e68d1bdf43c1b3b1ed9ef16c27162e8a175898c9ac64a679b0fc825'
|
||||
],
|
||||
root: 'c3c023c84afc0a7bab1dbebcef5f7beaf3d6af4af98e8f481620dec052be7d0d',
|
||||
membership: false
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
\# **verifyProof**(proof: _Proof_): _boolean_
|
||||
|
||||
```typescript
|
||||
console.log(tree.verifyProof(membershipProof)) // true
|
||||
console.log(tree.verifyProof(nonMembershipProof)) // true
|
||||
```
|
||||
|
||||
## Contacts
|
||||
|
||||
### Developers
|
||||
|
||||
- e-mail : me@cedoor.dev
|
||||
- github : [@cedoor](https://github.com/cedoor)
|
||||
- website : https://cedoor.dev
|
||||
7
packages/sparse-merkle-tree/build.tsconfig.json
Normal file
7
packages/sparse-merkle-tree/build.tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declarationDir": "types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
6
packages/sparse-merkle-tree/jest.config.json
Normal file
6
packages/sparse-merkle-tree/jest.config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"displayName": "sparse-merkle-tree",
|
||||
"transform": {
|
||||
"\\.ts$": "ts-jest"
|
||||
}
|
||||
}
|
||||
9131
packages/sparse-merkle-tree/package-lock.json
generated
Normal file
9131
packages/sparse-merkle-tree/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
packages/sparse-merkle-tree/package.json
Normal file
49
packages/sparse-merkle-tree/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@zk-kit/sparse-merkle-tree",
|
||||
"version": "0.1.0",
|
||||
"description": "Sparse Merkle tree implementation in TypeScript.",
|
||||
"keywords": [
|
||||
"typescript",
|
||||
"sparse-merkle-tree",
|
||||
"merkle-tree"
|
||||
],
|
||||
"iife": "dist/index.js",
|
||||
"unpkg": "dist/index.min.js",
|
||||
"jsdelivr": "dist/index.min.js",
|
||||
"main": "dist/index.node.js",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.node.js"
|
||||
},
|
||||
"types": "dist/types/index.d.ts",
|
||||
"files": [
|
||||
"dist/",
|
||||
"src/",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"repository": "git@github.com:appliedzkp/zk-kit.git",
|
||||
"homepage": "https://github.com/appliedzkp/zk-kit/tree/main/packages/sparse-merkle-tree",
|
||||
"author": {
|
||||
"name": "Omar Desogus",
|
||||
"email": "me@cedoor.dev",
|
||||
"url": "https://cedoor.dev"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build:watch": "rollup -c rollup.config.ts -w --configPlugin typescript",
|
||||
"build": "rimraf dist && rollup -c rollup.config.ts --configPlugin typescript",
|
||||
"prepublishOnly": "yarn build",
|
||||
"docs": "rimraf docs && typedoc src/index.ts"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^8.3.0",
|
||||
"circomlibjs": "^0.0.8",
|
||||
"js-sha256": "^0.9.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typedoc": "^0.20.32"
|
||||
}
|
||||
}
|
||||
35
packages/sparse-merkle-tree/rollup.config.ts
Normal file
35
packages/sparse-merkle-tree/rollup.config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import typescript from "@rollup/plugin-typescript"
|
||||
import fs from "fs"
|
||||
import { terser } from "rollup-plugin-terser"
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"))
|
||||
const banner = `/**
|
||||
* @module ${pkg.name}
|
||||
* @version ${pkg.version}
|
||||
* @file ${pkg.description}
|
||||
* @copyright ${pkg.author.name} ${new Date().getFullYear()}
|
||||
* @license ${pkg.license}
|
||||
* @see [Github]{@link ${pkg.homepage}}
|
||||
*/`
|
||||
const name = pkg.name.substr(1).replace(/[-/]./g, (x: string) => x.toUpperCase()[1])
|
||||
|
||||
export default {
|
||||
input: "src/index.ts",
|
||||
output: [
|
||||
{
|
||||
file: pkg.iife,
|
||||
name,
|
||||
format: "iife",
|
||||
banner
|
||||
},
|
||||
{
|
||||
file: pkg.unpkg,
|
||||
name,
|
||||
format: "iife",
|
||||
plugins: [terser({ output: { preamble: banner } })]
|
||||
},
|
||||
{ file: pkg.exports.require, format: "cjs", banner },
|
||||
{ file: pkg.exports.import, format: "es", banner }
|
||||
],
|
||||
plugins: [typescript({ tsconfig: "./build.tsconfig.json" })]
|
||||
}
|
||||
4
packages/sparse-merkle-tree/src/index.ts
Normal file
4
packages/sparse-merkle-tree/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import SparseMerkleTree from "./sparse-merkle-tree"
|
||||
|
||||
export { SparseMerkleTree }
|
||||
export * from "./utils"
|
||||
418
packages/sparse-merkle-tree/src/sparse-merkle-tree.ts
Normal file
418
packages/sparse-merkle-tree/src/sparse-merkle-tree.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import { checkHex, getFirstCommonElements, getIndexOfLastNonZeroElement, keyToPath } from "../src/utils"
|
||||
|
||||
/**
|
||||
* 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`;
|
||||
* * **side node**: if you take one of the two child nodes, the other one is its side node;
|
||||
* * **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<Node, ChildNodes>
|
||||
|
||||
// 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, sidenodes } = 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 side nodes it deletes all the nodes of the path.
|
||||
// These nodes will be re-created below with the new hashes.
|
||||
if (sidenodes.length > 0) {
|
||||
this.deleteOldNodes(node, path, sidenodes)
|
||||
}
|
||||
|
||||
// If there is a matching entry, further N zero side nodes are added
|
||||
// in the `sidenodes` 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 = sidenodes.length; matchingPath[i] === path[i]; i++) {
|
||||
sidenodes.push(this.zeroNode)
|
||||
}
|
||||
|
||||
sidenodes.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, sidenodes)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, sidenodes } = 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, sidenodes)
|
||||
|
||||
// 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, sidenodes)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, sidenodes } = 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 side nodes it deletes the nodes of the path and
|
||||
// re-creates them with the new hashes.
|
||||
if (sidenodes.length > 0) {
|
||||
this.deleteOldNodes(node, path, sidenodes)
|
||||
|
||||
// If the last side node 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 side node from the `sidenodes`
|
||||
// array and it starts from it by skipping the last zero nodes.
|
||||
if (!this.isLeaf(sidenodes[sidenodes.length - 1])) {
|
||||
this.root = this.addNewNodes(this.zeroNode, path, sidenodes)
|
||||
} else {
|
||||
const firstSidenode = sidenodes.pop() as Node
|
||||
const i = getIndexOfLastNonZeroElement(sidenodes)
|
||||
|
||||
this.root = this.addNewNodes(firstSidenode, path, sidenodes, 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): Proof {
|
||||
this.checkParameterType(key)
|
||||
|
||||
const { entry, matchingEntry, sidenodes } = 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,
|
||||
sidenodes,
|
||||
root: this.root,
|
||||
membership: !!entry[1]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a membership or a non-membership proof.
|
||||
* @param proof The proof to verify.
|
||||
* @returns True if the proof is valid, false otherwise.
|
||||
*/
|
||||
verifyProof(proof: Proof): boolean {
|
||||
// If there is not a matching entry it simply obtains the root
|
||||
// hash by using the side nodes and the path of the key.
|
||||
if (!proof.matchingEntry) {
|
||||
const path = keyToPath(proof.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 = proof.entry[1] !== undefined ? this.hash(proof.entry) : this.zeroNode
|
||||
const root = this.calculateRoot(node, path, proof.sidenodes)
|
||||
|
||||
// If the obtained root is equal to the proof root, then the proof is valid.
|
||||
return root === proof.root
|
||||
} else {
|
||||
// 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 side nodes.
|
||||
const matchingPath = keyToPath(proof.matchingEntry[0])
|
||||
const node = this.hash(proof.matchingEntry)
|
||||
const root = this.calculateRoot(node, matchingPath, proof.sidenodes)
|
||||
|
||||
if (root === proof.root) {
|
||||
const path = keyToPath(proof.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 side nodes.
|
||||
return proof.sidenodes.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 side nodes 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 sidenodes: SideNodes = []
|
||||
|
||||
// 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++) {
|
||||
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 side nodes.
|
||||
return { entry: childNodes, sidenodes }
|
||||
}
|
||||
// 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, sidenodes }
|
||||
}
|
||||
|
||||
// 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 side node.
|
||||
node = childNodes[direction] as Node
|
||||
sidenodes.push(childNodes[Number(!direction)] as Node)
|
||||
}
|
||||
|
||||
// The path led to a zero node.
|
||||
return { entry: [key], sidenodes }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 sidenodes The side nodes of the path.
|
||||
* @returns The root node.
|
||||
*/
|
||||
private calculateRoot(node: Node, path: number[], sidenodes: SideNodes): Node {
|
||||
for (let i = sidenodes.length - 1; i >= 0; i--) {
|
||||
const childNodes: ChildNodes = path[i] ? [sidenodes[i], node] : [node, sidenodes[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 sidenodes The side nodes of the path.
|
||||
* @param i The index to start from.
|
||||
* @returns The root node.
|
||||
*/
|
||||
private addNewNodes(node: Node, path: number[], sidenodes: SideNodes, i = sidenodes.length - 1): Node {
|
||||
for (; i >= 0; i--) {
|
||||
const childNodes: ChildNodes = path[i] ? [sidenodes[i], node] : [node, sidenodes[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 sidenodes The side nodes of the path.
|
||||
* @param i The index to start from.
|
||||
*/
|
||||
private deleteOldNodes(node: Node, path: number[], sidenodes: SideNodes) {
|
||||
for (let i = sidenodes.length - 1; i >= 0; i--) {
|
||||
const childNodes: ChildNodes = path[i] ? [sidenodes[i], node] : [node, sidenodes[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`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Node = string | bigint
|
||||
export type Key = Node
|
||||
export type Value = Node
|
||||
export type EntryMark = Node
|
||||
|
||||
export type Entry = [Key, Value, EntryMark]
|
||||
export type ChildNodes = Node[]
|
||||
export type SideNodes = Node[]
|
||||
|
||||
export type HashFunction = (childNodes: ChildNodes) => Node
|
||||
|
||||
export interface EntryResponse {
|
||||
entry: Entry | Node[]
|
||||
matchingEntry?: Entry | Node[]
|
||||
sidenodes: SideNodes
|
||||
}
|
||||
|
||||
export interface Proof extends EntryResponse {
|
||||
root: Node
|
||||
membership: boolean
|
||||
}
|
||||
69
packages/sparse-merkle-tree/src/utils.ts
Normal file
69
packages/sparse-merkle-tree/src/utils.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function keyToPath(key: string | bigint): number[] {
|
||||
const bits = typeof key === "bigint" ? key.toString(2) : hexToBin(key as string)
|
||||
|
||||
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.
|
||||
*/
|
||||
export function getIndexOfLastNonZeroElement(array: any[]): number {
|
||||
for (let i = array.length - 1; i >= 0; i--) {
|
||||
if (Number(`0x${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.
|
||||
*/
|
||||
export function getFirstCommonElements(array1: any[], array2: any[]): any[] {
|
||||
const minArray = array1.length < array2.length ? array1 : array2
|
||||
|
||||
for (let i = 0; i < minArray.length; i++) {
|
||||
if (array1[i] !== array2[i]) {
|
||||
return minArray.slice(0, i)
|
||||
}
|
||||
}
|
||||
|
||||
return minArray.slice()
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a hexadecimal number to a binary number.
|
||||
* @param n A hexadecimal number.
|
||||
* @returns The relative binary number.
|
||||
*/
|
||||
export function hexToBin(n: string): string {
|
||||
let bin = Number(`0x${n[0]}`).toString(2)
|
||||
|
||||
for (let i = 1; i < n.length; i++) {
|
||||
bin += Number(`0x${n[i]}`).toString(2).padStart(4, "0")
|
||||
}
|
||||
|
||||
return bin
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a number is a hexadecimal number.
|
||||
* @param n A hexadecimal number.
|
||||
* @returns True if the number is a hexadecimal, false otherwise.
|
||||
*/
|
||||
export function checkHex(n: string): boolean {
|
||||
return typeof n === "string" && /^[0-9A-Fa-f]{1,64}$/.test(n)
|
||||
}
|
||||
238
packages/sparse-merkle-tree/tests/index.test.ts
Normal file
238
packages/sparse-merkle-tree/tests/index.test.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { SparseMerkleTree } from "../src"
|
||||
import { ChildNodes } from "../src/sparse-merkle-tree"
|
||||
import { sha256 } from "js-sha256"
|
||||
import { poseidon, smt } from "circomlibjs"
|
||||
|
||||
describe("Sparse Merkle tree", () => {
|
||||
const hash = (childNodes: ChildNodes) => sha256(childNodes.join(""))
|
||||
const testKeys = ["a", "3", "2b", "20", "9", "17"]
|
||||
|
||||
describe("Create hexadecimal trees", () => {
|
||||
it("Should create an empty sparse Merkle tree", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
expect(tree.root).toEqual("0")
|
||||
})
|
||||
|
||||
it("Should not create a hexadecimal tree if the hash function does not return a hexadecimal", () => {
|
||||
const hash = (childNodes: ChildNodes) => poseidon(childNodes)
|
||||
|
||||
const fun = () => new SparseMerkleTree(hash)
|
||||
|
||||
expect(fun).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Add new entries (key/value) in the tree", () => {
|
||||
it("Should add a new entry", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
const oldRoot = tree.root
|
||||
|
||||
tree.add("2", "a")
|
||||
|
||||
expect(tree.root).not.toEqual(oldRoot)
|
||||
})
|
||||
|
||||
it("Should not add a new non-hexadecimal entry", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
const fun = () => tree.add(BigInt(2), BigInt(4))
|
||||
|
||||
expect(fun).toThrow()
|
||||
})
|
||||
|
||||
it("Should not add a new entry with an existing key", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
tree.add("2", "a")
|
||||
const fun = () => tree.add("2", "a")
|
||||
|
||||
expect(fun).toThrow()
|
||||
})
|
||||
|
||||
it("Should add 6 new entries and create the correct root hash", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
for (const key of testKeys) {
|
||||
tree.add(key, key)
|
||||
}
|
||||
|
||||
expect(tree.root).toEqual("40770450d00520bdab58e115dd4439c20cd39028252f3973e81fb15b02eb28f7")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Get values from the tree", () => {
|
||||
it("Should get a value from the tree using an existing key", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
tree.add("2", "a")
|
||||
const value = tree.get("2")
|
||||
|
||||
expect(value).toEqual("a")
|
||||
})
|
||||
|
||||
it("Should not get a value from the tree using a non-existing key", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
tree.add("2", "a")
|
||||
const value = tree.get("1")
|
||||
|
||||
expect(value).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Update values in the tree", () => {
|
||||
it("Should update a value of an existing key", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
tree.add("2", "a")
|
||||
tree.update("2", "5")
|
||||
|
||||
expect(tree.root).toEqual("c75d3f1f5bcd6914d0331ce5ec17c0db8f2070a2d4285f8e3ff11c6ca19168ff")
|
||||
})
|
||||
|
||||
it("Should not update a value with a non-existing key", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
const fun = () => tree.update("1", "5")
|
||||
|
||||
expect(fun).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Delete entries from the tree", () => {
|
||||
it("Should delete an entry with an existing key", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
tree.add("2", "a")
|
||||
tree.delete("2")
|
||||
|
||||
expect(tree.root).toEqual("0")
|
||||
})
|
||||
|
||||
it("Should delete 3 entries and create the correct root hash", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
for (const key of testKeys) {
|
||||
tree.add(key, key)
|
||||
}
|
||||
|
||||
tree.delete(testKeys[1])
|
||||
tree.delete(testKeys[3])
|
||||
tree.delete(testKeys[4])
|
||||
|
||||
expect(tree.root).toEqual("5d2bfda7c24d9e9e59fe89a271f7d0a3435892c98bc1121b9b590d800deeca10")
|
||||
})
|
||||
|
||||
it("Should not delete an entry with a non-existing key", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
const fun = () => tree.delete("1")
|
||||
|
||||
expect(fun).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Create Merkle proofs and verify them", () => {
|
||||
it("Should create some Merkle proofs and verify them", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
for (const key of testKeys) {
|
||||
tree.add(key, key)
|
||||
}
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const randomKey = Math.floor(Math.random() * 100).toString(16)
|
||||
const proof = tree.createProof(randomKey)
|
||||
|
||||
expect(tree.verifyProof(proof)).toBeTruthy()
|
||||
}
|
||||
|
||||
tree.add("12", "1")
|
||||
|
||||
const proof = tree.createProof("6")
|
||||
expect(tree.verifyProof(proof)).toBeTruthy()
|
||||
})
|
||||
|
||||
it("Should not verify a wrong Merkle proof", () => {
|
||||
const tree = new SparseMerkleTree(hash)
|
||||
|
||||
for (const key of testKeys) {
|
||||
tree.add(key, key)
|
||||
}
|
||||
|
||||
const proof = tree.createProof("19")
|
||||
proof.matchingEntry = ["20", "a"]
|
||||
|
||||
expect(tree.verifyProof(proof)).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Create big number trees", () => {
|
||||
const hash = (childNodes: ChildNodes) => poseidon(childNodes)
|
||||
|
||||
it("Should create a big number tree", () => {
|
||||
const tree = new SparseMerkleTree(hash, true)
|
||||
|
||||
expect(tree.root).toEqual(BigInt(0))
|
||||
})
|
||||
|
||||
it("Should not create a big number tree if the hash function does not return a big number", () => {
|
||||
const hash = (childNodes: ChildNodes) => sha256(childNodes.join(""))
|
||||
|
||||
const fun = () => new SparseMerkleTree(hash, true)
|
||||
|
||||
expect(fun).toThrow()
|
||||
})
|
||||
|
||||
it("Should add a big number new entry", () => {
|
||||
const tree = new SparseMerkleTree(hash, true)
|
||||
const oldRoot = tree.root
|
||||
|
||||
tree.add(BigInt(2), BigInt(4))
|
||||
|
||||
expect(tree.root).not.toEqual(oldRoot)
|
||||
})
|
||||
|
||||
it("Should not add a new non-big number entry", () => {
|
||||
const tree = new SparseMerkleTree(hash, true)
|
||||
|
||||
const fun = () => tree.add("2", "a")
|
||||
|
||||
expect(fun).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Matching with Circomlib smt implementation", () => {
|
||||
it("Should create two trees with different implementations and match their root nodes", async () => {
|
||||
const hash = (childNodes: ChildNodes) => poseidon(childNodes)
|
||||
const tree = new SparseMerkleTree(hash, true)
|
||||
const tree2 = await smt.newMemEmptyTrie()
|
||||
const entries: any = [
|
||||
[
|
||||
BigInt("20438969296305830531522370305156029982566273432331621236661483041446048135547"),
|
||||
BigInt("17150136040889237739751319962368206600863150289695545292530539263327413090784")
|
||||
],
|
||||
[
|
||||
BigInt("8459688297517826598613412977307486050019239051864711035321718508109192087854"),
|
||||
BigInt("8510347201346963732943571140849185725417245763047403804445415726302354045170")
|
||||
],
|
||||
[
|
||||
BigInt("18746990989203767017840856832962652635369613415011636432610873672704085238844"),
|
||||
BigInt("10223238458026721676606706894638558676629446348345239719814856822628482567791")
|
||||
],
|
||||
[
|
||||
BigInt("13924553918840562069536446401916499801909138643922241340476956069386532478098"),
|
||||
BigInt("13761779908325789083343687318102407319424329800042729673292939195255502025802")
|
||||
]
|
||||
]
|
||||
|
||||
for (const entry of entries) {
|
||||
tree.add(entry[0], entry[1])
|
||||
await tree2.insert(entry[0], entry[1])
|
||||
}
|
||||
|
||||
expect(tree.root).toEqual(tree2.root)
|
||||
})
|
||||
})
|
||||
})
|
||||
68
packages/sparse-merkle-tree/tests/utils.test.ts
Normal file
68
packages/sparse-merkle-tree/tests/utils.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getFirstCommonElements, getIndexOfLastNonZeroElement, hexToBin, keyToPath, checkHex } from "../src/utils"
|
||||
|
||||
describe("Utility functions", () => {
|
||||
describe("Convert SMT keys in 256-bit paths", () => {
|
||||
it("Should convert a key in an array of 256 bits", () => {
|
||||
const path = keyToPath("17")
|
||||
|
||||
expect(path.length).toEqual(256)
|
||||
expect(path.every((b) => b === 0 || b === 1)).toBeTruthy()
|
||||
})
|
||||
|
||||
it("Should create a path in the correct order", () => {
|
||||
const path = keyToPath("17")
|
||||
|
||||
expect(path.slice(0, 5)).toEqual([1, 1, 1, 0, 1])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Get index of the last non-zero element", () => {
|
||||
it("Should return the correct index of the last non-zero element", () => {
|
||||
const index = getIndexOfLastNonZeroElement(["0", "17", "3", "0", "3", "0", "3", "2", "0", "0"])
|
||||
|
||||
expect(index).toEqual(7)
|
||||
})
|
||||
|
||||
it("Should return -1 if there are not non-zero elements", () => {
|
||||
const index = getIndexOfLastNonZeroElement(["0", "0", "0", "0"])
|
||||
|
||||
expect(index).toEqual(-1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Get first matching elements", () => {
|
||||
it("Should return the first matching elements of two arrays", () => {
|
||||
const array1 = [1, 4, 3, 8, 2, 9]
|
||||
const array2 = [1, 4, 2, 7, 2]
|
||||
|
||||
const matchingArray = getFirstCommonElements(array1, array2)
|
||||
|
||||
expect(matchingArray).toEqual([1, 4])
|
||||
})
|
||||
|
||||
it("Should return the smallest array if all its elements are the first elements of the other array", () => {
|
||||
const array1 = [1, 4, 3, 8, 2]
|
||||
const array2 = [1, 4, 3, 8, 2, 32, 23]
|
||||
|
||||
const matchingArray = getFirstCommonElements(array1, array2)
|
||||
|
||||
expect(matchingArray).toEqual(array1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Check hexadecimal", () => {
|
||||
it("Should return true if the number is a hexadecimal", () => {
|
||||
expect(checkHex("be12")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("Should return false if the number is not a hexadecimal", () => {
|
||||
expect(checkHex("gbe12")).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Convert hexadecimal to binary", () => {
|
||||
it("Should convert a hexadecimal number to a binary number", () => {
|
||||
expect(hexToBin("12")).toEqual("10010")
|
||||
})
|
||||
})
|
||||
})
|
||||
4
packages/sparse-merkle-tree/tsconfig.json
Normal file
4
packages/sparse-merkle-tree/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "tests", "rollup.config.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user