├── .editorconfig
├── .eslintrc.json
├── .github
└── workflows
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── code-of-conduct.md
├── package.json
├── rollup.config.ts
├── src
├── index.ts
├── smt.ts
└── utils.ts
├── test
├── smt.test.ts
└── utils.test.ts
├── tsconfig.json
├── types
└── circomlib.d.ts
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | #root = true
2 |
3 | [*]
4 | indent_style = space
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | max_line_length = 120
10 | indent_size = 4
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true
4 | },
5 | "extends": [
6 | "eslint:recommended",
7 | "plugin:@typescript-eslint/recommended",
8 | "prettier"
9 | ],
10 | "parser": "@typescript-eslint/parser",
11 | "parserOptions": {
12 | "ecmaVersion": 6,
13 | "sourceType": "module"
14 | },
15 | "plugins": ["@typescript-eslint"],
16 | "rules": {
17 | "@typescript-eslint/no-explicit-any": "off",
18 | "@typescript-eslint/explicit-module-boundary-types": "off",
19 | "comma-dangle": "warn",
20 | "comma-spacing": "warn",
21 | "comma-style": "warn",
22 | "func-call-spacing": "warn",
23 | "no-whitespace-before-property": "warn",
24 | "no-multi-spaces": "warn",
25 | "space-in-parens": "warn",
26 | "spaced-comment": "warn",
27 | "arrow-parens": "warn",
28 | "no-var": "error",
29 | "prefer-const": "warn",
30 | "prefer-destructuring": "warn",
31 | "prefer-template": "warn",
32 | "prefer-rest-params": "warn",
33 | "rest-spread-spacing": "warn",
34 | "template-curly-spacing": "warn",
35 | "prefer-arrow-callback": "warn",
36 | "object-shorthand": "warn",
37 | "no-useless-rename": "warn",
38 | "no-useless-computed-key": "warn",
39 | "no-duplicate-imports": "warn",
40 | "no-duplicate-case": "warn",
41 | "block-spacing": "warn",
42 | "brace-style": "warn",
43 | "camelcase": "warn",
44 | "computed-property-spacing": "warn",
45 | "eol-last": "warn",
46 | "function-call-argument-newline": ["warn", "consistent"]
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | package:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Use Node.js 14.x
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: 14.x
18 | registry-url: "https://registry.npmjs.org"
19 |
20 | - run: yarn --frozen-lockfile
21 | - run: yarn build
22 | - run: yarn publish --access public
23 | env:
24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
25 |
26 | docs:
27 | runs-on: ubuntu-latest
28 |
29 | steps:
30 | - uses: actions/checkout@v2
31 |
32 | - name: Use Node.js 14.x
33 | uses: actions/setup-node@v1
34 | with:
35 | node-version: 14.x
36 |
37 | - run: yarn --frozen-lockfile
38 | - run: yarn docs
39 |
40 | - name: Github Pages
41 | uses: crazy-max/ghaction-github-pages@v2.2.0
42 | with:
43 | build_dir: docs
44 | commit_message: "docs: update documentation website"
45 | fqdn: smt.cedoor.dev
46 | jekyll: false
47 | env:
48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Use Node.js 14.x
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: 14.x
20 |
21 | - run: yarn --frozen-lockfile
22 | - run: yarn test:prod
23 |
24 | - name: Coveralls
25 | uses: coverallsapp/github-action@master
26 | with:
27 | github-token: ${{ secrets.GITHUB_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDE
2 | .vscode
3 | .idea
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 | *.lcov
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Dependency directories
21 | node_modules/
22 |
23 | # TypeScript cache
24 | *.tsbuildinfo
25 |
26 | # Optional eslint cache
27 | .eslintcache
28 |
29 | # Microbundle cache
30 | .rpt2_cache/
31 | .rts2_cache_cjs/
32 | .rts2_cache_es/
33 | .rts2_cache_umd/
34 |
35 | # Yarn Integrity file
36 | .yarn-integrity
37 |
38 | # Generate output
39 | dist
40 | docs
41 |
42 | # dotenv environment variables file
43 | .env
44 | .env.test
45 |
46 | # Optional npm cache directory
47 | .npm
48 |
49 | # yarn v2
50 | .yarn/cache
51 | .yarn/unplugged
52 | .yarn/build-state.yml
53 | .yarn/install-state.gz
54 | .pnp.*
55 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | We're really glad you're reading this, because we need volunteer developers to help this project come to fruition. 👏
2 |
3 | ## Instructions
4 |
5 | These steps will guide you through contributing to this project:
6 |
7 | - Fork the repository
8 | - Clone it and install dependencies
9 |
10 | git clone https://github.com/YOUR-USERNAME/sparse-merkle-tree
11 | npm install
12 |
13 | Keep in mind that after running `npm install` the git repo is reset. So a good way to cope with this is to have a copy of the folder to push the changes, and the other to try them.
14 |
15 | Make and commit your changes. Make sure the commands npm run build and npm run test:prod are working.
16 |
17 | Finally send a [GitHub Pull Request](https://github.com/cedoor/sparse-merkle-tree/compare?expand=1) with a clear list of what you've done (read more [about pull requests](https://help.github.com/articles/about-pull-requests/)). Make sure all of your commits are atomic (one feature per commit).
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Omar Desogus
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Sparse Merkle tree
4 |
5 | Sparse Merkle tree implementation in TypeScript.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | A sparse Merkle tree (SMT) 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. SMTs 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.
32 |
33 | **Notice**: this library is still not stable and therefore it must be used with care.
34 |
35 | ## References
36 |
37 | 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.
38 | 2. Faraz Haider. _Compact sparse merkle trees_. Cryptology ePrint Archive: Report 2018/955, 2018. https://eprint.iacr.org/2018/955.
39 | 3. Jordi Baylina and Marta Bellés. _Sparse Merkle Trees_. https://docs.iden3.io/publications/pdfs/Merkle-Tree.pdf.
40 | 4. Vitalik Buterin Fichter. _Optimizing sparse Merkle trees_. https://ethresear.ch/t/optimizing-sparse-merkle-trees/3751.
41 |
42 | ---
43 |
44 | ## Table of Contents
45 |
46 | - 🛠 [Install](#install)
47 | - 📜 [API reference](#api-reference)
48 | - 🔬 [Development](#development)
49 | - [Rules](#scroll-rules)
50 | - [Commits](https://github.com/cedoor/cedoor/tree/main/git#commits-rules)
51 | - [Branches](https://github.com/cedoor/cedoor/tree/main/git#branch-rules)
52 | - 🧾 [MIT License](https://github.com/cedoor/sparse-merkle-tree/blob/master/LICENSE)
53 | - ☎️ [Contacts](#contacts)
54 | - [Developers](#developers)
55 |
56 | ## Install
57 |
58 | ### npm or yarn
59 |
60 | You can install utils package with npm:
61 |
62 | ```bash
63 | npm i @cedoor/smt --save
64 | ```
65 |
66 | or with yarn:
67 |
68 | ```bash
69 | yarn add @cedoor/smt
70 | ```
71 |
72 | ### CDN
73 |
74 | You can also load it using a `script` tap using [unpkg](https://unpkg.com/):
75 |
76 | ```html
77 |
78 | ```
79 |
80 | or [JSDelivr](https://www.jsdelivr.com/):
81 |
82 | ```html
83 |
84 | ```
85 |
86 | ## API reference
87 |
88 | - [Creating trees](#smt-new)
89 | - [Adding entries](#smt-add)
90 | - [Getting values](#smt-get)
91 | - [Updating values](#smt-update)
92 | - [Deleting entries](#smt-delete)
93 | - [Creating proofs](#smt-create-proof)
94 | - [Verifying proofs](#smt-verify-proof)
95 |
96 | # **new SMT**(hash: _HashFunction_, bigNumbers?: _boolean_): _SMT_
97 |
98 | ```typescript
99 | import { SMT, hexToDec } from "@cedoor/smt"
100 | import { sha256 } from "js-sha256"
101 | import { poseidon } from "circomlib"
102 |
103 | // Hexadecimal hashes.
104 | const hash = (childNodes: ChildNodes) => sha256(childNodes.join(""))
105 | const tree = new SMT(hash)
106 |
107 | // Big number hashes.
108 | const hash2 = (childNodes: ChildNodes) => poseidon(childNodes)
109 | const tree2 = new SMT(hash2, true)
110 |
111 | console.log(tree.root) // 0
112 | console.log(tree2.root) // 0n
113 | ```
114 |
115 | # **add**(key: _string_ | _number_, value: _string_ | _number_): _void_
116 |
117 | ```typescript
118 | tree.add("2b", "44") // Hexadecimal key/value.
119 | tree.add("16", "78")
120 | tree.add("d", "e7")
121 | tree.add("10", "141")
122 | tree.add("20", "340")
123 |
124 | console.log(tree.root) // 31ee2a59741c9c32a32d8c7fafe461cca1ccaf5986c2d592586e3e6482a48645
125 | ```
126 |
127 | # **get**(key: _string_ | _number_): _undefined_ | _string_
128 |
129 | ```typescript
130 | const value = tree.get("16")
131 |
132 | console.log(value) // 78
133 | ```
134 |
135 | # **update**(key: _string_ | _number_, value: _string_ | _number_): _void_
136 |
137 | ```typescript
138 | tree.update("16", "79")
139 |
140 | const value = tree.get("16")
141 |
142 | console.log(value) // 79
143 | ```
144 |
145 | # **delete**(key: _string_ | _number_): _void_
146 |
147 | ```typescript
148 | tree.delete("16")
149 |
150 | const value = tree.get("16")
151 |
152 | console.log(value) // undefined
153 | ```
154 |
155 | # **createProof**(key: _string_ | _number_): _Proof_
156 |
157 | ```typescript
158 | const membershipProof = tree.createProof("2b")
159 | const nonMembershipProof = tree.createProof("16") // This key has been deleted.
160 |
161 | console.log(membershipProof)
162 | /*
163 | {
164 | entry: [ '2b', '44', '1' ],
165 | matchingEntry: undefined,
166 | sidenodes: [
167 | '006a0ab15a212e0e0126b81e056b11576628b1ad80792403dbb3a90be2e71d64',
168 | 'f786ce5a843614d7da216d95c0087c1eb29244927feeeeeb658aa60cf124cd5e'
169 | ],
170 | root: 'c3c023c84afc0a7bab1dbebcef5f7beaf3d6af4af98e8f481620dec052be7d0d',
171 | membership: true
172 | }
173 | */
174 |
175 | console.log(nonMembershipProof)
176 | /*
177 | {
178 | entry: [ '16' ],
179 | matchingEntry: undefined,
180 | sidenodes: [
181 | '960f23d9fbb44241be53efb7c4d69ac129bb1cb9482dcb6789d3cc7e6de2de2b',
182 | '2a1aef839e68d1bdf43c1b3b1ed9ef16c27162e8a175898c9ac64a679b0fc825'
183 | ],
184 | root: 'c3c023c84afc0a7bab1dbebcef5f7beaf3d6af4af98e8f481620dec052be7d0d',
185 | membership: false
186 | }
187 | */
188 | ```
189 |
190 | # **verifyProof**(proof: _Proof_): _boolean_
191 |
192 | ```typescript
193 | console.log(tree.verifyProof(membershipProof)) // true
194 | console.log(tree.verifyProof(nonMembershipProof)) // true
195 | ```
196 |
197 | ## Contacts
198 |
199 | ### Developers
200 |
201 | - e-mail : me@cedoor.dev
202 | - github : [@cedoor](https://github.com/cedoor)
203 | - website : https://cedoor.dev
204 |
--------------------------------------------------------------------------------
/code-of-conduct.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at me@cedoor.dev. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cedoor/smt",
3 | "version": "0.1.7",
4 | "description": "Sparse Merkle tree implementation in TypeScript.",
5 | "keywords": [
6 | "typescript",
7 | "sparse-merkle-tree",
8 | "merkle-tree"
9 | ],
10 | "unpkg": "dist/smt.min.js",
11 | "jsdelivr": "dist/smt.min.js",
12 | "main": "dist/smt.node.js",
13 | "exports": {
14 | "import": "./dist/smt.mjs",
15 | "require": "./dist/smt.node.js"
16 | },
17 | "types": "dist/types/index.d.ts",
18 | "files": [
19 | "dist/",
20 | "src/",
21 | "LICENSE",
22 | "README.md"
23 | ],
24 | "homepage": "https://github.com/cedoor/sparse-merkle-tree",
25 | "license": "MIT",
26 | "author": {
27 | "name": "Omar Desogus",
28 | "email": "me@cedoor.dev",
29 | "url": "https://cedoor.dev"
30 | },
31 | "repository": {
32 | "type": "git",
33 | "url": "https://github.com/cedoor/sparse-merkle-tree.git"
34 | },
35 | "bugs": {
36 | "url": "https://github.com/cedoor/sparse-merkle-tree/issues"
37 | },
38 | "scripts": {
39 | "start": "rollup -c rollup.config.ts -w",
40 | "build": "rimraf dist && rollup -c rollup.config.ts",
41 | "lint": "eslint 'src/**/*.ts' 'test/**/*.ts'",
42 | "lint:fix": "eslint 'src/**/*.ts' 'test/**/*.ts' --fix",
43 | "test": "jest --coverage",
44 | "test:watch": "jest --coverage --watch",
45 | "test:prod": "yarn lint && yarn test --no-cache",
46 | "docs": "rimraf docs && typedoc src/index.ts",
47 | "commit": "cz",
48 | "precommit": "lint-staged"
49 | },
50 | "lint-staged": {
51 | "{src,test}/**/*.ts": [
52 | "prettier --write",
53 | "eslint --fix"
54 | ]
55 | },
56 | "jest": {
57 | "transform": {
58 | ".(ts|tsx)": "ts-jest"
59 | },
60 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
61 | "moduleFileExtensions": [
62 | "ts",
63 | "tsx",
64 | "js"
65 | ],
66 | "coveragePathIgnorePatterns": [
67 | "/node_modules/",
68 | "/test/"
69 | ],
70 | "coverageThreshold": {
71 | "global": {
72 | "branches": 90,
73 | "functions": 95,
74 | "lines": 95,
75 | "statements": 95
76 | }
77 | },
78 | "collectCoverageFrom": [
79 | "src/**/*.{js,ts}"
80 | ]
81 | },
82 | "prettier": {
83 | "semi": false,
84 | "arrowParens": "always",
85 | "trailingComma": "none"
86 | },
87 | "commitlint": {
88 | "extends": [
89 | "@commitlint/config-conventional"
90 | ]
91 | },
92 | "devDependencies": {
93 | "@commitlint/cli": "^12.0.1",
94 | "@commitlint/config-conventional": "^12.0.1",
95 | "@types/jest": "^26.0.20",
96 | "@types/node": "^14.14.35",
97 | "@typescript-eslint/eslint-plugin": "^4.18.0",
98 | "@typescript-eslint/parser": "^4.18.0",
99 | "circomlib": "^0.5.1",
100 | "commitizen": "^4.2.3",
101 | "cz-conventional-changelog": "3.3.0",
102 | "eslint": "^7.22.0",
103 | "eslint-config-prettier": "^8.1.0",
104 | "jest": "^26.6.3",
105 | "jest-config": "^26.6.3",
106 | "js-sha256": "^0.9.0",
107 | "lint-staged": "^10.5.4",
108 | "prettier": "^2.2.1",
109 | "rimraf": "^3.0.2",
110 | "rollup": "^2.41.4",
111 | "rollup-plugin-terser": "^7.0.2",
112 | "rollup-plugin-typescript2": "^0.30.0",
113 | "ts-jest": "^26.5.3",
114 | "ts-node": "^9.1.1",
115 | "tslib": "^2.1.0",
116 | "typedoc": "^0.20.32",
117 | "typescript": "^4.2.3"
118 | },
119 | "config": {
120 | "commitizen": {
121 | "path": "./node_modules/cz-conventional-changelog"
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/rollup.config.ts:
--------------------------------------------------------------------------------
1 | import { terser } from "rollup-plugin-terser"
2 | import typescript from "rollup-plugin-typescript2"
3 |
4 | const pkg = require("./package.json")
5 |
6 | const banner = `/**
7 | * @module ${pkg.name}
8 | * @version ${pkg.version}
9 | * @file ${pkg.description}
10 | * @copyright ${pkg.author.name} ${new Date().getFullYear()}
11 | * @license ${pkg.license}
12 | * @see [Github]{@link ${pkg.homepage}}
13 | */`
14 |
15 | const [, name] = pkg.name.split("/")
16 |
17 | export default {
18 | input: "src/index.ts",
19 | output: [
20 | {
21 | file: `dist/${name}.js`,
22 | name,
23 | format: "iife",
24 | banner
25 | },
26 | {
27 | file: `dist/${name}.min.js`,
28 | name,
29 | format: "iife",
30 | plugins: [terser({ output: { preamble: banner } })]
31 | },
32 | { file: pkg.exports.require, format: "cjs", banner },
33 | { file: pkg.exports.import, format: "es", banner }
34 | ],
35 | plugins: [typescript({ useTsconfigDeclarationDir: true })]
36 | }
37 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { SMT } from "./smt"
2 |
--------------------------------------------------------------------------------
/src/smt.ts:
--------------------------------------------------------------------------------
1 | import { checkHex, getFirstCommonElements, getIndexOfLastNonZeroElement, keyToPath } from "../src/utils"
2 |
3 | /**
4 | * SMT class provides all the functions to create a sparse Merkle tree
5 | * and to take advantage of its features: {@linkcode SMT.add}, {@linkcode SMT.get},
6 | * {@linkcode SMT.update}, {@linkcode SMT.delete}, {@linkcode SMT.createProof},
7 | * {@linkcode SMT.verifyProof}.
8 | * To better understand the code below it may be useful to describe the terminology used:
9 | * * **nodes**: every node in the tree is the hash of the two child nodes (`H(x, y)`);
10 | * * **root node**: the root node is the top hash and since it represents the whole data
11 | * structure it can be used to certify its integrity;
12 | * * **leaf nodes**: every leaf node is the hash of a key/value pair and an additional
13 | * value to mark the node as leaf node (`H(x, y, 1)`);
14 | * * **entry**: a tree entry is a key/value pair used to create the leaf nodes;
15 | * * **zero nodes**: a zero node is an hash of zeros and in this implementation `H(0,0) = 0`;
16 | * * **side node**: if you take one of the two child nodes, the other one is its side node;
17 | * * **path**: every entry key is a number < 2^256 that can be converted in a binary number,
18 | * and this binary number is the path used to place the entry in the tree (1 or 0 define the
19 | * child node to choose);
20 | * * **matching node**: when an entry is not found and the path leads to another existing entry,
21 | * this entry is a matching entry and it has some of the first bits in common with the entry not found;
22 | * * **depth**: the depth of a node is the length of the path to its root.
23 | */
24 | export class SMT {
25 | // Hash function used to hash the child nodes.
26 | private hash: HashFunction
27 | // Value for zero nodes.
28 | private zeroNode: Node
29 | // Additional entry value to mark the leaf nodes.
30 | private entryMark: EntryMark
31 | // If true it sets `BigInt` type as default type of the tree hashes.
32 | private bigNumbers: boolean
33 | // Key/value map in which the key is a node of the tree and
34 | // the value is an array of child nodes. When the node is
35 | // a leaf node the child nodes are an entry (key/value) of the tree.
36 | private nodes: Map
37 |
38 | // The root node of the tree.
39 | root: Node
40 |
41 | /**
42 | * Initializes the SMT attributes.
43 | * @param hash Hash function used to hash the child nodes.
44 | * @param bigNumbers BigInt type enabling.
45 | */
46 | constructor(hash: HashFunction, bigNumbers = false) {
47 | if (bigNumbers) {
48 | /* istanbul ignore next */
49 | if (typeof BigInt !== "function") {
50 | throw new Error("Big numbers are not supported")
51 | }
52 |
53 | if (typeof hash([BigInt(1), BigInt(1)]) !== "bigint") {
54 | throw new Error("The hash function must return a big number")
55 | }
56 | } else {
57 | if (!checkHex(hash(["1", "1"]) as string)) {
58 | throw new Error("The hash function must return a hexadecimal")
59 | }
60 | }
61 |
62 | this.hash = hash
63 | this.bigNumbers = bigNumbers
64 | this.zeroNode = bigNumbers ? BigInt(0) : "0"
65 | this.entryMark = bigNumbers ? BigInt(1) : "1"
66 | this.nodes = new Map()
67 |
68 | this.root = this.zeroNode // The root node is initially a zero node.
69 | }
70 |
71 | /**
72 | * Gets a key and if the key exists in the tree the function returns the
73 | * value, otherwise it returns 'undefined'.
74 | * @param key A key of a tree entry.
75 | * @returns A value of a tree entry or 'undefined'.
76 | */
77 | get(key: Key): Value | undefined {
78 | this.checkParameterType(key)
79 |
80 | const { entry } = this.retrieveEntry(key)
81 |
82 | return entry[1]
83 | }
84 |
85 | /**
86 | * Adds a new entry in the tree. It retrieves a matching entry
87 | * or a zero node with a top-down approach and then it updates all the
88 | * hashes of the nodes in the path of the new entry with a bottom-up approach.
89 | * @param key The key of the new entry.
90 | * @param value The value of the new entry.
91 | */
92 | add(key: Key, value: Value) {
93 | this.checkParameterType(key)
94 | this.checkParameterType(value)
95 |
96 | const { entry, matchingEntry, sidenodes } = this.retrieveEntry(key)
97 |
98 | if (entry[1] !== undefined) {
99 | throw new Error(`Key "${key}" already exists`)
100 | }
101 |
102 | const path = keyToPath(key)
103 | // If there is a matching entry its node is saved, otherwise
104 | // the node is a zero node. This node is used below as the first node
105 | // (starting from the bottom of the tree) to obtain the new nodes
106 | // up to the root.
107 | const node = matchingEntry ? this.hash(matchingEntry) : this.zeroNode
108 |
109 | // If there are side nodes it deletes all the nodes of the path.
110 | // These nodes will be re-created below with the new hashes.
111 | if (sidenodes.length > 0) {
112 | this.deleteOldNodes(node, path, sidenodes)
113 | }
114 |
115 | // If there is a matching entry, further N zero side nodes are added
116 | // in the `sidenodes` array, followed by the matching node itself.
117 | // N is the number of the first matching bits of the paths.
118 | // This is helpful in the non-membership proof verification
119 | // as explained in the function below.
120 | if (matchingEntry) {
121 | const matchingPath = keyToPath(matchingEntry[0])
122 |
123 | for (let i = sidenodes.length; matchingPath[i] === path[i]; i++) {
124 | sidenodes.push(this.zeroNode)
125 | }
126 |
127 | sidenodes.push(node)
128 | }
129 |
130 | // Adds the new entry and re-creates the nodes of the path with the new hashes
131 | // with a bottom-up approach. The `addNewNodes` function returns the last node
132 | // added, which is the root node.
133 | const newNode = this.hash([key, value, this.entryMark])
134 | this.nodes.set(newNode, [key, value, this.entryMark])
135 | this.root = this.addNewNodes(newNode, path, sidenodes)
136 | }
137 |
138 | /**
139 | * Updates a value of an entry in the tree. Also in this case
140 | * all the hashes of the nodes in the path of the entry are updated
141 | * with a bottom-up approach.
142 | * @param key The key of the entry.
143 | * @param value The value of the entry.
144 | */
145 | update(key: Key, value: Value) {
146 | this.checkParameterType(key)
147 | this.checkParameterType(value)
148 |
149 | const { entry, sidenodes } = this.retrieveEntry(key)
150 |
151 | if (entry[1] === undefined) {
152 | throw new Error(`Key "${key}" does not exist`)
153 | }
154 |
155 | const path = keyToPath(key)
156 |
157 | // Deletes the old entry and all the nodes in its path.
158 | const oldNode = this.hash(entry)
159 | this.nodes.delete(oldNode)
160 | this.deleteOldNodes(oldNode, path, sidenodes)
161 |
162 | // Adds the new entry and re-creates the nodes of the path
163 | // with the new hashes.
164 | const newNode = this.hash([key, value, this.entryMark])
165 | this.nodes.set(newNode, [key, value, this.entryMark])
166 | this.root = this.addNewNodes(newNode, path, sidenodes)
167 | }
168 |
169 | /**
170 | * Deletes an entry in the tree. Also in this case all the hashes of
171 | * the nodes in the path of the entry are updated with a bottom-up approach.
172 | * @param key The key of the entry.
173 | */
174 | delete(key: Key) {
175 | this.checkParameterType(key)
176 |
177 | const { entry, sidenodes } = this.retrieveEntry(key)
178 |
179 | if (entry[1] === undefined) {
180 | throw new Error(`Key "${key}" does not exist`)
181 | }
182 |
183 | const path = keyToPath(key)
184 |
185 | // Deletes the entry.
186 | const node = this.hash(entry)
187 | this.nodes.delete(node)
188 |
189 | this.root = this.zeroNode
190 |
191 | // If there are side nodes it deletes the nodes of the path and
192 | // re-creates them with the new hashes.
193 | if (sidenodes.length > 0) {
194 | this.deleteOldNodes(node, path, sidenodes)
195 |
196 | // If the last side node is not a leaf node, it adds all the
197 | // nodes of the path starting from a zero node, otherwise
198 | // it removes the last non-zero side node from the `sidenodes`
199 | // array and it starts from it by skipping the last zero nodes.
200 | if (!this.isLeaf(sidenodes[sidenodes.length - 1])) {
201 | this.root = this.addNewNodes(this.zeroNode, path, sidenodes)
202 | } else {
203 | const firstSidenode = sidenodes.pop() as Node
204 | const i = getIndexOfLastNonZeroElement(sidenodes)
205 |
206 | this.root = this.addNewNodes(firstSidenode, path, sidenodes, i)
207 | }
208 | }
209 | }
210 |
211 | /**
212 | * Creates a proof to prove the membership or the non-membership
213 | * of a tree entry.
214 | * @param key A key of an existing or a non-existing entry.
215 | * @returns The membership or the non-membership proof.
216 | */
217 | createProof(key: Key): Proof {
218 | this.checkParameterType(key)
219 |
220 | const { entry, matchingEntry, sidenodes } = this.retrieveEntry(key)
221 |
222 | // If the key exists the function returns a membership proof, otherwise it
223 | // returns a non-membership proof with the matching entry.
224 | return {
225 | entry,
226 | matchingEntry,
227 | sidenodes,
228 | root: this.root,
229 | membership: !!entry[1]
230 | }
231 | }
232 |
233 | /**
234 | * Verifies a membership or a non-membership proof.
235 | * @param proof The proof to verify.
236 | * @returns True if the proof is valid, false otherwise.
237 | */
238 | verifyProof(proof: Proof): boolean {
239 | // If there is not a matching entry it simply obtains the root
240 | // hash by using the side nodes and the path of the key.
241 | if (!proof.matchingEntry) {
242 | const path = keyToPath(proof.entry[0])
243 | // If there is not an entry value the proof is a non-membership proof,
244 | // and in this case, since there is not a matching entry, the node
245 | // is a zero node. If there is an entry value the proof is a
246 | // membership proof and the node is the hash of the entry.
247 | const node = proof.entry[1] !== undefined ? this.hash(proof.entry) : this.zeroNode
248 | const root = this.calculateRoot(node, path, proof.sidenodes)
249 |
250 | // If the obtained root is equal to the proof root, then the proof is valid.
251 | return root === proof.root
252 | } else {
253 | // If there is a matching entry the proof is definitely a non-membership
254 | // proof. In this case it checks if the matching node belongs to the tree
255 | // and then it checks if the number of the first matching bits of the keys
256 | // is greater than or equal to the number of the side nodes.
257 | const matchingPath = keyToPath(proof.matchingEntry[0])
258 | const node = this.hash(proof.matchingEntry)
259 | const root = this.calculateRoot(node, matchingPath, proof.sidenodes)
260 |
261 | if (root === proof.root) {
262 | const path = keyToPath(proof.entry[0])
263 | // Returns the first common bits of the two keys: the
264 | // non-member key and the matching key.
265 | const firstMatchingBits = getFirstCommonElements(path, matchingPath)
266 | // If the non-member key was a key of a tree entry, the depth of the
267 | // matching node should be greater than the number of the first common
268 | // bits of the keys. The depth of a node can be defined by the number
269 | // of its side nodes.
270 | return proof.sidenodes.length <= firstMatchingBits.length
271 | }
272 |
273 | return false
274 | }
275 | }
276 |
277 | /**
278 | * Searches for an entry in the tree. If the key passed as parameter exists in
279 | * the tree, the function returns the entry, otherwise it returns the entry
280 | * with only the key, and when there is another existing entry
281 | * in the same path it returns also this entry as 'matching entry'.
282 | * In any case the function returns the side nodes of the path.
283 | * @param key The key of the entry to search for.
284 | * @returns The entry response.
285 | */
286 | private retrieveEntry(key: Key): EntryResponse {
287 | const path = keyToPath(key)
288 | const sidenodes: SideNodes = []
289 |
290 | // Starts from the root and goes down into the tree until it finds
291 | // the entry, a zero node or a matching entry.
292 | for (let i = 0, node = this.root; node !== this.zeroNode; i++) {
293 | const childNodes = this.nodes.get(node) as ChildNodes
294 | const direction = path[i]
295 |
296 | // If the third position of the array is not empty the child nodes
297 | // are an entry of the tree.
298 | if (childNodes[2]) {
299 | if (childNodes[0] === key) {
300 | // An entry with the same key was found and
301 | // it returns it with the side nodes.
302 | return { entry: childNodes, sidenodes }
303 | }
304 | // The entry found does not have the same key. But the key of this
305 | // particular entry matches the first 'i' bits of the key passed
306 | // as parameter and it can be useful in several functions.
307 | return { entry: [key], matchingEntry: childNodes, sidenodes }
308 | }
309 |
310 | // When it goes down into the tree and follows the path, in every step
311 | // a node is chosen between the left and the right child nodes, and the
312 | // opposite node is saved as side node.
313 | node = childNodes[direction] as Node
314 | sidenodes.push(childNodes[Number(!direction)] as Node)
315 | }
316 |
317 | // The path led to a zero node.
318 | return { entry: [key], sidenodes }
319 | }
320 |
321 | /**
322 | * Calculates nodes with a bottom-up approach until it reaches the root node.
323 | * @param node The node to start from.
324 | * @param path The path of the key.
325 | * @param sidenodes The side nodes of the path.
326 | * @returns The root node.
327 | */
328 | private calculateRoot(node: Node, path: number[], sidenodes: SideNodes): Node {
329 | for (let i = sidenodes.length - 1; i >= 0; i--) {
330 | const childNodes: ChildNodes = path[i] ? [sidenodes[i], node] : [node, sidenodes[i]]
331 | node = this.hash(childNodes)
332 | }
333 |
334 | return node
335 | }
336 |
337 | /**
338 | * Adds new nodes in the tree with a bottom-up approach until it reaches the root node.
339 | * @param node The node to start from.
340 | * @param path The path of the key.
341 | * @param sidenodes The side nodes of the path.
342 | * @param i The index to start from.
343 | * @returns The root node.
344 | */
345 | private addNewNodes(node: Node, path: number[], sidenodes: SideNodes, i = sidenodes.length - 1): Node {
346 | for (; i >= 0; i--) {
347 | const childNodes: ChildNodes = path[i] ? [sidenodes[i], node] : [node, sidenodes[i]]
348 | node = this.hash(childNodes)
349 |
350 | this.nodes.set(node, childNodes)
351 | }
352 |
353 | return node
354 | }
355 |
356 | /**
357 | * Deletes nodes in the tree with a bottom-up approach until it reaches the root node.
358 | * @param node The node to start from.
359 | * @param path The path of the key.
360 | * @param sidenodes The side nodes of the path.
361 | * @param i The index to start from.
362 | */
363 | private deleteOldNodes(node: Node, path: number[], sidenodes: SideNodes) {
364 | for (let i = sidenodes.length - 1; i >= 0; i--) {
365 | const childNodes: ChildNodes = path[i] ? [sidenodes[i], node] : [node, sidenodes[i]]
366 | node = this.hash(childNodes)
367 |
368 | this.nodes.delete(node)
369 | }
370 | }
371 |
372 | /**
373 | * Checks if a node is a leaf node.
374 | * @param node A node of the tree.
375 | * @returns True if the node is a leaf, false otherwise.
376 | */
377 | private isLeaf(node: Node): boolean {
378 | const childNodes = this.nodes.get(node)
379 |
380 | return !!(childNodes && childNodes[2])
381 | }
382 |
383 | /**
384 | * Checks the parameter type.
385 | * @param parameter The parameter to check.
386 | */
387 | private checkParameterType(parameter: Key | Value) {
388 | if (this.bigNumbers && typeof parameter !== "bigint") {
389 | throw new Error(`Parameter ${parameter} must be a big number`)
390 | }
391 |
392 | if (!this.bigNumbers && !checkHex(parameter as string)) {
393 | throw new Error(`Parameter ${parameter} must be a hexadecimal`)
394 | }
395 | }
396 | }
397 |
398 | export type Node = string | bigint
399 | export type Key = Node
400 | export type Value = Node
401 | export type EntryMark = Node
402 |
403 | export type Entry = [Key, Value, EntryMark]
404 | export type ChildNodes = Node[]
405 | export type SideNodes = Node[]
406 |
407 | export type HashFunction = (childNodes: ChildNodes) => Node
408 |
409 | export interface EntryResponse {
410 | entry: Entry | Node[]
411 | matchingEntry?: Entry | Node[]
412 | sidenodes: SideNodes
413 | }
414 |
415 | export interface Proof extends EntryResponse {
416 | root: Node
417 | membership: boolean
418 | }
419 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the binary representation of a key. For each key it is possibile
3 | * to obtain an array of 256 padded bits.
4 | * @param key The key of a tree entry.
5 | * @returns The relative array of bits.
6 | */
7 | export function keyToPath(key: string | bigint): number[] {
8 | const bits = typeof key === "bigint" ? key.toString(2) : hexToBin(key as string)
9 |
10 | return bits.padStart(256, "0").split("").reverse().map(Number)
11 | }
12 |
13 | /**
14 | * Returns the index of the last non-zero element of an array.
15 | * If there are only zero elements the function returns -1.
16 | * @param array An array of hexadecimal or big numbers.
17 | * @returns The index of the last non-zero element.
18 | */
19 | export function getIndexOfLastNonZeroElement(array: any[]): number {
20 | for (let i = array.length - 1; i >= 0; i--) {
21 | if (Number(`0x${array[i]}`) !== 0) {
22 | return i
23 | }
24 | }
25 |
26 | return -1
27 | }
28 |
29 | /**
30 | * Returns the first common elements of two arrays.
31 | * @param array1 The first array.
32 | * @param array2 The second array.
33 | * @returns The array of the first common elements.
34 | */
35 | export function getFirstCommonElements(array1: any[], array2: any[]): any[] {
36 | const minArray = array1.length < array2.length ? array1 : array2
37 |
38 | for (let i = 0; i < minArray.length; i++) {
39 | if (array1[i] !== array2[i]) {
40 | return minArray.slice(0, i)
41 | }
42 | }
43 |
44 | return minArray.slice()
45 | }
46 |
47 | /**
48 | * Converts a hexadecimal number to a binary number.
49 | * @param n A hexadecimal number.
50 | * @returns The relative binary number.
51 | */
52 | export function hexToBin(n: string): string {
53 | let bin = Number(`0x${n[0]}`).toString(2)
54 |
55 | for (let i = 1; i < n.length; i++) {
56 | bin += Number(`0x${n[i]}`).toString(2).padStart(4, "0")
57 | }
58 |
59 | return bin
60 | }
61 |
62 | /**
63 | * Checks if a number is a hexadecimal number.
64 | * @param n A hexadecimal number.
65 | * @returns True if the number is a hexadecimal, false otherwise.
66 | */
67 | export function checkHex(n: string): boolean {
68 | return typeof n === "string" && /^[0-9A-Fa-f]{1,64}$/.test(n)
69 | }
70 |
--------------------------------------------------------------------------------
/test/smt.test.ts:
--------------------------------------------------------------------------------
1 | import { SMT } from "../src"
2 | import { ChildNodes } from "../src/smt"
3 | import { sha256 } from "js-sha256"
4 | import { poseidon, smt } from "circomlib"
5 |
6 | describe("Sparse Merkle tree", () => {
7 | const hash = (childNodes: ChildNodes) => sha256(childNodes.join(""))
8 | const testKeys = ["a", "3", "2b", "20", "9", "17"]
9 |
10 | describe("Create hexadecimal trees", () => {
11 | it("Should create an empty sparse Merkle tree", () => {
12 | const tree = new SMT(hash)
13 |
14 | expect(tree.root).toEqual("0")
15 | })
16 |
17 | it("Should not create a hexadecimal tree if the hash function does not return a hexadecimal", () => {
18 | const hash = (childNodes: ChildNodes) => poseidon(childNodes)
19 |
20 | const fun = () => new SMT(hash)
21 |
22 | expect(fun).toThrow()
23 | })
24 | })
25 |
26 | describe("Add new entries (key/value) in the tree", () => {
27 | it("Should add a new entry", () => {
28 | const tree = new SMT(hash)
29 | const oldRoot = tree.root
30 |
31 | tree.add("2", "a")
32 |
33 | expect(tree.root).not.toEqual(oldRoot)
34 | })
35 |
36 | it("Should not add a new non-hexadecimal entry", () => {
37 | const tree = new SMT(hash)
38 |
39 | const fun = () => tree.add(BigInt(2), BigInt(4))
40 |
41 | expect(fun).toThrow()
42 | })
43 |
44 | it("Should not add a new entry with an existing key", () => {
45 | const tree = new SMT(hash)
46 |
47 | tree.add("2", "a")
48 | const fun = () => tree.add("2", "a")
49 |
50 | expect(fun).toThrow()
51 | })
52 |
53 | it("Should add 6 new entries and create the correct root hash", () => {
54 | const tree = new SMT(hash)
55 |
56 | for (const key of testKeys) {
57 | tree.add(key, key)
58 | }
59 |
60 | expect(tree.root).toEqual("40770450d00520bdab58e115dd4439c20cd39028252f3973e81fb15b02eb28f7")
61 | })
62 | })
63 |
64 | describe("Get values from the tree", () => {
65 | it("Should get a value from the tree using an existing key", () => {
66 | const tree = new SMT(hash)
67 |
68 | tree.add("2", "a")
69 | const value = tree.get("2")
70 |
71 | expect(value).toEqual("a")
72 | })
73 |
74 | it("Should not get a value from the tree using a non-existing key", () => {
75 | const tree = new SMT(hash)
76 |
77 | tree.add("2", "a")
78 | const value = tree.get("1")
79 |
80 | expect(value).toBeUndefined()
81 | })
82 | })
83 |
84 | describe("Update values in the tree", () => {
85 | it("Should update a value of an existing key", () => {
86 | const tree = new SMT(hash)
87 |
88 | tree.add("2", "a")
89 | tree.update("2", "5")
90 |
91 | expect(tree.root).toEqual("c75d3f1f5bcd6914d0331ce5ec17c0db8f2070a2d4285f8e3ff11c6ca19168ff")
92 | })
93 |
94 | it("Should not update a value with a non-existing key", () => {
95 | const tree = new SMT(hash)
96 |
97 | const fun = () => tree.update("1", "5")
98 |
99 | expect(fun).toThrow()
100 | })
101 | })
102 |
103 | describe("Delete entries from the tree", () => {
104 | it("Should delete an entry with an existing key", () => {
105 | const tree = new SMT(hash)
106 |
107 | tree.add("2", "a")
108 | tree.delete("2")
109 |
110 | expect(tree.root).toEqual("0")
111 | })
112 |
113 | it("Should delete 3 entries and create the correct root hash", () => {
114 | const tree = new SMT(hash)
115 |
116 | for (const key of testKeys) {
117 | tree.add(key, key)
118 | }
119 |
120 | tree.delete(testKeys[1])
121 | tree.delete(testKeys[3])
122 | tree.delete(testKeys[4])
123 |
124 | expect(tree.root).toEqual("5d2bfda7c24d9e9e59fe89a271f7d0a3435892c98bc1121b9b590d800deeca10")
125 | })
126 |
127 | it("Should not delete an entry with a non-existing key", () => {
128 | const tree = new SMT(hash)
129 |
130 | const fun = () => tree.delete("1")
131 |
132 | expect(fun).toThrow()
133 | })
134 | })
135 |
136 | describe("Create Merkle proofs and verify them", () => {
137 | it("Should create some Merkle proofs and verify them", () => {
138 | const tree = new SMT(hash)
139 |
140 | for (const key of testKeys) {
141 | tree.add(key, key)
142 | }
143 |
144 | for (let i = 0; i < 100; i++) {
145 | const randomKey = Math.floor(Math.random() * 100).toString(16)
146 | const proof = tree.createProof(randomKey)
147 |
148 | expect(tree.verifyProof(proof)).toBeTruthy()
149 | }
150 |
151 | tree.add("12", "1")
152 |
153 | const proof = tree.createProof("6")
154 | expect(tree.verifyProof(proof)).toBeTruthy()
155 | })
156 |
157 | it("Should not verify a wrong Merkle proof", () => {
158 | const tree = new SMT(hash)
159 |
160 | for (const key of testKeys) {
161 | tree.add(key, key)
162 | }
163 |
164 | const proof = tree.createProof("19")
165 | proof.matchingEntry = ["20", "a"]
166 |
167 | expect(tree.verifyProof(proof)).toBeFalsy()
168 | })
169 | })
170 |
171 | describe("Create big number trees", () => {
172 | const hash = (childNodes: ChildNodes) => poseidon(childNodes)
173 |
174 | it("Should create a big number tree", () => {
175 | const tree = new SMT(hash, true)
176 |
177 | expect(tree.root).toEqual(BigInt(0))
178 | })
179 |
180 | it("Should not create a big number tree if the hash function does not return a big number", () => {
181 | const hash = (childNodes: ChildNodes) => sha256(childNodes.join(""))
182 |
183 | const fun = () => new SMT(hash, true)
184 |
185 | expect(fun).toThrow()
186 | })
187 |
188 | it("Should add a big number new entry", () => {
189 | const tree = new SMT(hash, true)
190 | const oldRoot = tree.root
191 |
192 | tree.add(BigInt(2), BigInt(4))
193 |
194 | expect(tree.root).not.toEqual(oldRoot)
195 | })
196 |
197 | it("Should not add a new non-big number entry", () => {
198 | const tree = new SMT(hash, true)
199 |
200 | const fun = () => tree.add("2", "a")
201 |
202 | expect(fun).toThrow()
203 | })
204 | })
205 |
206 | describe("Matching with Circomlib smt implementation", () => {
207 | it("Should create two trees with different implementations and match their root nodes", async () => {
208 | const hash = (childNodes: ChildNodes) => poseidon(childNodes)
209 | const tree = new SMT(hash, true)
210 | const tree2 = await smt.newMemEmptyTrie()
211 | const entries: any = [
212 | [
213 | BigInt("20438969296305830531522370305156029982566273432331621236661483041446048135547"),
214 | BigInt("17150136040889237739751319962368206600863150289695545292530539263327413090784")
215 | ],
216 | [
217 | BigInt("8459688297517826598613412977307486050019239051864711035321718508109192087854"),
218 | BigInt("8510347201346963732943571140849185725417245763047403804445415726302354045170")
219 | ],
220 | [
221 | BigInt("18746990989203767017840856832962652635369613415011636432610873672704085238844"),
222 | BigInt("10223238458026721676606706894638558676629446348345239719814856822628482567791")
223 | ],
224 | [
225 | BigInt("13924553918840562069536446401916499801909138643922241340476956069386532478098"),
226 | BigInt("13761779908325789083343687318102407319424329800042729673292939195255502025802")
227 | ]
228 | ]
229 |
230 | for (const entry of entries) {
231 | tree.add(entry[0], entry[1])
232 | await tree2.insert(entry[0], entry[1])
233 | }
234 |
235 | expect(tree.root).toEqual(tree2.root)
236 | })
237 | })
238 | })
239 |
--------------------------------------------------------------------------------
/test/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { getFirstCommonElements, getIndexOfLastNonZeroElement, hexToBin, keyToPath, checkHex } from "../src/utils"
2 |
3 | describe("Utility functions", () => {
4 | describe("Convert SMT keys in 256-bit paths", () => {
5 | it("Should convert a key in an array of 256 bits", () => {
6 | const path = keyToPath("17")
7 |
8 | expect(path.length).toEqual(256)
9 | expect(path.every((b) => b === 0 || b === 1)).toBeTruthy()
10 | })
11 |
12 | it("Should create a path in the correct order", () => {
13 | const path = keyToPath("17")
14 |
15 | expect(path.slice(0, 5)).toEqual([1, 1, 1, 0, 1])
16 | })
17 | })
18 |
19 | describe("Get index of the last non-zero element", () => {
20 | it("Should return the correct index of the last non-zero element", () => {
21 | const index = getIndexOfLastNonZeroElement(["0", "17", "3", "0", "3", "0", "3", "2", "0", "0"])
22 |
23 | expect(index).toEqual(7)
24 | })
25 |
26 | it("Should return -1 if there are not non-zero elements", () => {
27 | const index = getIndexOfLastNonZeroElement(["0", "0", "0", "0"])
28 |
29 | expect(index).toEqual(-1)
30 | })
31 | })
32 |
33 | describe("Get first matching elements", () => {
34 | it("Should return the first matching elements of two arrays", () => {
35 | const array1 = [1, 4, 3, 8, 2, 9]
36 | const array2 = [1, 4, 2, 7, 2]
37 |
38 | const matchingArray = getFirstCommonElements(array1, array2)
39 |
40 | expect(matchingArray).toEqual([1, 4])
41 | })
42 |
43 | it("Should return the smallest array if all its elements are the first elements of the other array", () => {
44 | const array1 = [1, 4, 3, 8, 2]
45 | const array2 = [1, 4, 3, 8, 2, 32, 23]
46 |
47 | const matchingArray = getFirstCommonElements(array1, array2)
48 |
49 | expect(matchingArray).toEqual(array1)
50 | })
51 | })
52 |
53 | describe("Check hexadecimal", () => {
54 | it("Should return true if the number is a hexadecimal", () => {
55 | expect(checkHex("be12")).toBeTruthy()
56 | })
57 |
58 | it("Should return false if the number is not a hexadecimal", () => {
59 | expect(checkHex("gbe12")).toBeFalsy()
60 | })
61 | })
62 |
63 | describe("Convert hexadecimal to binary", () => {
64 | it("Should convert a hexadecimal number to a binary number", () => {
65 | expect(hexToBin("12")).toEqual("10010")
66 | })
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "moduleResolution": "node",
5 | "esModuleInterop": true,
6 | "target": "ES5",
7 | "module": "ES6",
8 | "declaration": true,
9 | "declarationDir": "dist/types",
10 | "allowSyntheticDefaultImports": true,
11 | "typeRoots": ["node_modules/@types", "./types"]
12 | },
13 | "include": ["src", "types"]
14 | }
15 |
--------------------------------------------------------------------------------
/types/circomlib.d.ts:
--------------------------------------------------------------------------------
1 | /** Declaration file generated by dts-gen */
2 |
3 | declare module "circomlib" {
4 | export class SMT {
5 | constructor(...args: any[])
6 |
7 | delete(...args: any[]): void
8 |
9 | find(...args: any[]): void
10 |
11 | insert(...args: any[]): void
12 |
13 | update(...args: any[]): void
14 | }
15 |
16 | export class SMTMemDB {
17 | constructor(...args: any[])
18 |
19 | get(...args: any[]): void
20 |
21 | getRoot(...args: any[]): void
22 |
23 | multiDel(...args: any[]): void
24 |
25 | multiGet(...args: any[]): void
26 |
27 | multiIns(...args: any[]): void
28 |
29 | setRoot(...args: any[]): void
30 | }
31 |
32 | export function poseidon(inputs: any): any
33 |
34 | export namespace babyJub {
35 | const A: any
36 |
37 | const Base8: any[]
38 |
39 | const D: any
40 |
41 | const Generator: any[]
42 |
43 | const order: any
44 |
45 | const p: any
46 |
47 | const subOrder: any
48 |
49 | function addPoint(a: any, b: any): any
50 |
51 | function inCurve(P: any): any
52 |
53 | function inSubgroup(P: any): any
54 |
55 | function mulPointEscalar(base: any, e: any): any
56 |
57 | function packPoint(P: any): any
58 |
59 | function unpackPoint(_buff: any): any
60 |
61 | namespace F {
62 | const R: any
63 |
64 | const bitLength: number
65 |
66 | const half: any
67 |
68 | const mask: any
69 |
70 | const minusone: any
71 |
72 | const n64: number
73 |
74 | const nqr: any
75 |
76 | const nqr_to_t: any
77 |
78 | const one: any
79 |
80 | const p: any
81 |
82 | const s: number
83 |
84 | const t: any
85 |
86 | const two: any
87 |
88 | const zero: any
89 |
90 | function add(...args: any[]): void
91 |
92 | function band(...args: any[]): void
93 |
94 | function bnot(...args: any[]): void
95 |
96 | function bor(...args: any[]): void
97 |
98 | function bxor(...args: any[]): void
99 |
100 | function div(...args: any[]): void
101 |
102 | function e(...args: any[]): void
103 |
104 | function eq(...args: any[]): void
105 |
106 | function geq(...args: any[]): void
107 |
108 | function gt(...args: any[]): void
109 |
110 | function idiv(...args: any[]): void
111 |
112 | function inv(...args: any[]): void
113 |
114 | function isZero(...args: any[]): void
115 |
116 | function land(...args: any[]): void
117 |
118 | function leq(...args: any[]): void
119 |
120 | function lnot(...args: any[]): void
121 |
122 | function lor(...args: any[]): void
123 |
124 | function lt(...args: any[]): void
125 |
126 | function mod(...args: any[]): void
127 |
128 | function mul(...args: any[]): void
129 |
130 | function mulScalar(...args: any[]): void
131 |
132 | function neg(...args: any[]): void
133 |
134 | function neq(...args: any[]): void
135 |
136 | function normalize(...args: any[]): void
137 |
138 | function pow(...args: any[]): void
139 |
140 | function random(...args: any[]): void
141 |
142 | function shl(...args: any[]): void
143 |
144 | function shr(...args: any[]): void
145 |
146 | function sqrt(...args: any[]): void
147 |
148 | function square(...args: any[]): void
149 |
150 | function sub(...args: any[]): void
151 |
152 | function toString(...args: any[]): void
153 | }
154 | }
155 |
156 | export namespace eddsa {
157 | function packSignature(sig: any): any
158 |
159 | function pruneBuffer(_buff: any): any
160 |
161 | function prv2pub(prv: any): any
162 |
163 | function sign(prv: any, msg: any): any
164 |
165 | function signMiMC(prv: any, msg: any): any
166 |
167 | function signMiMCSponge(prv: any, msg: any): any
168 |
169 | function signPoseidon(prv: any, msg: any): any
170 |
171 | function unpackSignature(sigBuff: any): any
172 |
173 | function verify(msg: any, sig: any, A: any): any
174 |
175 | function verifyMiMC(msg: any, sig: any, A: any): any
176 |
177 | function verifyMiMCSponge(msg: any, sig: any, A: any): any
178 |
179 | function verifyPoseidon(msg: any, sig: any, A: any): any
180 | }
181 |
182 | export namespace mimc7 {
183 | function getConstants(seed: any, nRounds: any): any
184 |
185 | function getIV(seed: any): any
186 |
187 | function hash(_x_in: any, _k: any): any
188 |
189 | function multiHash(arr: any, key: any): any
190 |
191 | namespace F {
192 | const R: any
193 |
194 | const bitLength: number
195 |
196 | const half: any
197 |
198 | const mask: any
199 |
200 | const minusone: any
201 |
202 | const n64: number
203 |
204 | const nqr: any
205 |
206 | const nqr_to_t: any
207 |
208 | const one: any
209 |
210 | const p: any
211 |
212 | const s: number
213 |
214 | const t: any
215 |
216 | const two: any
217 |
218 | const zero: any
219 |
220 | function add(...args: any[]): void
221 |
222 | function band(...args: any[]): void
223 |
224 | function bnot(...args: any[]): void
225 |
226 | function bor(...args: any[]): void
227 |
228 | function bxor(...args: any[]): void
229 |
230 | function div(...args: any[]): void
231 |
232 | function e(...args: any[]): void
233 |
234 | function eq(...args: any[]): void
235 |
236 | function geq(...args: any[]): void
237 |
238 | function gt(...args: any[]): void
239 |
240 | function idiv(...args: any[]): void
241 |
242 | function inv(...args: any[]): void
243 |
244 | function isZero(...args: any[]): void
245 |
246 | function land(...args: any[]): void
247 |
248 | function leq(...args: any[]): void
249 |
250 | function lnot(...args: any[]): void
251 |
252 | function lor(...args: any[]): void
253 |
254 | function lt(...args: any[]): void
255 |
256 | function mod(...args: any[]): void
257 |
258 | function mul(...args: any[]): void
259 |
260 | function mulScalar(...args: any[]): void
261 |
262 | function neg(...args: any[]): void
263 |
264 | function neq(...args: any[]): void
265 |
266 | function normalize(...args: any[]): void
267 |
268 | function pow(...args: any[]): void
269 |
270 | function random(...args: any[]): void
271 |
272 | function shl(...args: any[]): void
273 |
274 | function shr(...args: any[]): void
275 |
276 | function sqrt(...args: any[]): void
277 |
278 | function square(...args: any[]): void
279 |
280 | function sub(...args: any[]): void
281 |
282 | function toString(...args: any[]): void
283 | }
284 | }
285 |
286 | export namespace mimcsponge {
287 | function getConstants(seed: any, nRounds: any): any
288 |
289 | function getIV(seed: any): any
290 |
291 | function hash(_xL_in: any, _xR_in: any, _k: any): any
292 |
293 | function multiHash(arr: any, key: any, numOutputs: any): any
294 | }
295 |
296 | export namespace pedersenHash {
297 | function getBasePoint(baseHashType: any, pointIdx: any): any
298 |
299 | function hash(msg: any, options: any): any
300 | }
301 |
302 | export namespace smt {
303 | class SMT {
304 | constructor(...args: any[])
305 |
306 | delete(...args: any[]): void
307 |
308 | find(...args: any[]): void
309 |
310 | insert(...args: any[]): void
311 |
312 | update(...args: any[]): void
313 | }
314 |
315 | class SMTMemDB {
316 | constructor(...args: any[])
317 |
318 | get(...args: any[]): void
319 |
320 | getRoot(...args: any[]): void
321 |
322 | multiDel(...args: any[]): void
323 |
324 | multiGet(...args: any[]): void
325 |
326 | multiIns(...args: any[]): void
327 |
328 | setRoot(...args: any[]): void
329 | }
330 |
331 | function loadFromFile(fileName: any): void
332 |
333 | function newMemEmptyTrie(): any
334 | }
335 | }
336 |
--------------------------------------------------------------------------------