├── .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 | NPM version 11 | 12 | 13 | Github license 14 | 15 | 16 | GitHub Workflow test 17 | 18 | 19 | Coveralls 20 | 21 | 22 | Linter eslint 23 | 24 | 25 | Code style prettier 26 | 27 | Repository top language 28 | NPM bundle size 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 | --------------------------------------------------------------------------------