From 302fa2dbac7172bdc78b2556791b44102c7010a7 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 23 Feb 2024 02:28:59 -0600 Subject: [PATCH 01/36] Simple MerkleTree alternative --- CHANGELOG.md | 1 + index.test.ts | 0 package-lock.json | 1731 +++---------------------------- package.json | 4 +- src/bytes.ts | 29 +- src/core.test.ts | 23 +- src/core.ts | 51 +- src/index.test.ts | 9 + src/index.ts | 1 + src/options.ts | 14 +- src/simple.test.ts | 167 +++ src/simple.ts | 255 +++++ src/standard.test.ts | 245 +++-- src/standard.ts | 328 +++--- src/utils/standard-leaf-hash.ts | 8 - 15 files changed, 978 insertions(+), 1888 deletions(-) create mode 100644 index.test.ts create mode 100644 src/index.test.ts create mode 100644 src/simple.test.ts create mode 100644 src/simple.ts delete mode 100644 src/utils/standard-leaf-hash.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 726d7a9..57be5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.0.6 - Added an option to disable leaf sorting. +- Added `SimpleMerkleTree` class that supports `bytes32` leaves with no extra hashing. ## 1.0.5 diff --git a/index.test.ts b/index.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index da65104..3083cbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "@openzeppelin/merkle-tree", "version": "1.0.6", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -10,7 +10,9 @@ "license": "MIT", "dependencies": { "@ethersproject/abi": "^5.7.0", - "ethereum-cryptography": "^1.1.2" + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0" }, "devDependencies": { "@types/mocha": "^10.0.0", @@ -423,18 +425,18 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -447,70 +449,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, - "node_modules/@noble/secp256k1": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.6.3.tgz", - "integrity": "sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, - "node_modules/@scure/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, - "node_modules/@scure/bip32": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.0.tgz", - "integrity": "sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "@noble/hashes": "~1.1.1", - "@noble/secp256k1": "~1.6.0", - "@scure/base": "~1.1.0" - } - }, - "node_modules/@scure/bip39": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", - "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "@noble/hashes": "~1.1.1", - "@scure/base": "~1.1.0" - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -530,34 +468,37 @@ "dev": true }, "node_modules/@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, "node_modules/@types/mocha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.0.tgz", - "integrity": "sha512-rADY+HtTOA52l9VZWtgQfn4p+UDVM2eDVkMZT1I6syp0YKxW2F9v+0pbRZLsvskhQv/vMb6ZfCay81GHbz5SHg==", + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", + "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", "dev": true }, "node_modules/@types/node": { - "version": "18.11.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.7.tgz", - "integrity": "sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==", + "version": "20.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", "dev": true, - "peer": true + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -567,9 +508,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", "dev": true, "engines": { "node": ">=0.4.0" @@ -609,9 +550,9 @@ } }, "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "dependencies": { "normalize-path": "^3.0.0", @@ -686,9 +627,9 @@ "dev": true }, "node_modules/c8": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", - "integrity": "sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", + "integrity": "sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -711,15 +652,6 @@ "node": ">=10.12.0" } }, - "node_modules/c8/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -748,18 +680,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -823,9 +743,9 @@ "dev": true }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, "node_modules/create-require": { @@ -884,9 +804,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true, "engines": { "node": ">=0.3.1" @@ -938,34 +858,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ethereum-cryptography": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz", - "integrity": "sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==", - "dependencies": { - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.6.3", - "@scure/bip32": "1.1.0", - "@scure/bip39": "1.1.0" - } - }, "node_modules/fast-check": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.3.0.tgz", - "integrity": "sha512-Zu6tZ4g0T4H9Tiz3tdNPEHrSbuICj7yhdOM9RCZKNMkpjZ9avDV3ORklXaEmh4zvkX24/bGZ9DxKKqWfXttUqw==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.15.0.tgz", + "integrity": "sha512-iBz6c+EXL6+nI931x/sbZs1JYTZtLG6Cko0ouS8LRTikhDR7+wZk4TYzdRavlnByBs2G6+nuuJ7NYL9QplNt8Q==", "dev": true, "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - }, { "type": "individual", "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" } ], "dependencies": { - "pure-rand": "^5.0.2" + "pure-rand": "^6.0.0" }, "engines": { "node": ">=8.0.0" @@ -1028,9 +937,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -1241,44 +1150,32 @@ "dev": true }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -1336,16 +1233,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "dependencies": { - "semver": "^6.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1380,9 +1289,9 @@ } }, "node_modules/mocha": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", - "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "dev": true, "dependencies": { "ansi-colors": "4.1.1", @@ -1419,13 +1328,28 @@ "url": "https://opencollective.com/mochajs" } }, - "node_modules/mocha/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=0.3.1" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" } }, "node_modules/ms": { @@ -1534,14 +1458,20 @@ } }, "node_modules/pure-rand": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-5.0.3.tgz", - "integrity": "sha512-9N8x1h8dptBQpHyC7aZMS+iNOAm97WMGY0AFrguU1cpfW3I5jINkWe5BIY5md0ofy+1TCIELsVcm/GJXZSaPbw==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] }, "node_modules/randombytes": { "version": "2.1.0", @@ -1609,12 +1539,18 @@ ] }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/serialize-javascript": { @@ -1692,18 +1628,15 @@ } }, "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=8" } }, "node_modules/test-exclude": { @@ -1755,9 +1688,9 @@ } }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -1797,10 +1730,19 @@ } } }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -1810,6 +1752,13 @@ "node": ">=4.2.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "peer": true + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -1817,27 +1766,27 @@ "dev": true }, "node_modules/v8-to-istanbul": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", - "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" } }, "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/which": { @@ -1893,6 +1842,12 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -1912,9 +1867,9 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "engines": { "node": ">=10" @@ -1956,1347 +1911,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "0.3.9" - } - }, - "@ethersproject/abi": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", - "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", - "requires": { - "@ethersproject/address": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/hash": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/strings": "^5.7.0" - } - }, - "@ethersproject/abstract-provider": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", - "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", - "requires": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/networks": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/transactions": "^5.7.0", - "@ethersproject/web": "^5.7.0" - } - }, - "@ethersproject/abstract-signer": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", - "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", - "requires": { - "@ethersproject/abstract-provider": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0" - } - }, - "@ethersproject/address": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", - "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", - "requires": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/rlp": "^5.7.0" - } - }, - "@ethersproject/base64": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", - "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", - "requires": { - "@ethersproject/bytes": "^5.7.0" - } - }, - "@ethersproject/bignumber": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", - "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", - "requires": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "bn.js": "^5.2.1" - } - }, - "@ethersproject/bytes": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", - "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", - "requires": { - "@ethersproject/logger": "^5.7.0" - } - }, - "@ethersproject/constants": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", - "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", - "requires": { - "@ethersproject/bignumber": "^5.7.0" - } - }, - "@ethersproject/hash": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", - "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", - "requires": { - "@ethersproject/abstract-signer": "^5.7.0", - "@ethersproject/address": "^5.7.0", - "@ethersproject/base64": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/strings": "^5.7.0" - } - }, - "@ethersproject/keccak256": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", - "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", - "requires": { - "@ethersproject/bytes": "^5.7.0", - "js-sha3": "0.8.0" - } - }, - "@ethersproject/logger": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", - "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==" - }, - "@ethersproject/networks": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", - "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", - "requires": { - "@ethersproject/logger": "^5.7.0" - } - }, - "@ethersproject/properties": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", - "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", - "requires": { - "@ethersproject/logger": "^5.7.0" - } - }, - "@ethersproject/rlp": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", - "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", - "requires": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0" - } - }, - "@ethersproject/signing-key": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", - "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", - "requires": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "bn.js": "^5.2.1", - "elliptic": "6.5.4", - "hash.js": "1.1.7" - } - }, - "@ethersproject/strings": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", - "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", - "requires": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/logger": "^5.7.0" - } - }, - "@ethersproject/transactions": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", - "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", - "requires": { - "@ethersproject/address": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/rlp": "^5.7.0", - "@ethersproject/signing-key": "^5.7.0" - } - }, - "@ethersproject/web": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", - "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", - "requires": { - "@ethersproject/base64": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/strings": "^5.7.0" - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==" - }, - "@noble/secp256k1": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.6.3.tgz", - "integrity": "sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ==" - }, - "@scure/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" - }, - "@scure/bip32": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.0.tgz", - "integrity": "sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q==", - "requires": { - "@noble/hashes": "~1.1.1", - "@noble/secp256k1": "~1.6.0", - "@scure/base": "~1.1.0" - } - }, - "@scure/bip39": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", - "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==", - "requires": { - "@noble/hashes": "~1.1.1", - "@scure/base": "~1.1.0" - } - }, - "@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "@types/mocha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.0.tgz", - "integrity": "sha512-rADY+HtTOA52l9VZWtgQfn4p+UDVM2eDVkMZT1I6syp0YKxW2F9v+0pbRZLsvskhQv/vMb6ZfCay81GHbz5SHg==", - "dev": true - }, - "@types/node": { - "version": "18.11.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.7.tgz", - "integrity": "sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==", - "dev": true, - "peer": true - }, - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "dev": true - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "c8": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", - "integrity": "sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^2.0.0", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-reports": "^3.1.4", - "rimraf": "^3.0.2", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.0.0", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9" - }, - "dependencies": { - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "ethereum-cryptography": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz", - "integrity": "sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==", - "requires": { - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.6.3", - "@scure/bip32": "1.1.0", - "@scure/bip39": "1.1.0" - } - }, - "fast-check": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.3.0.tgz", - "integrity": "sha512-Zu6tZ4g0T4H9Tiz3tdNPEHrSbuICj7yhdOM9RCZKNMkpjZ9avDV3ORklXaEmh4zvkX24/bGZ9DxKKqWfXttUqw==", - "dev": true, - "requires": { - "pure-rand": "^5.0.2" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true - }, - "foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" - }, - "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "mocha": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", - "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", - "dev": true, - "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "dependencies": { - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pure-rand": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-5.0.3.tgz", - "integrity": "sha512-9N8x1h8dptBQpHyC7aZMS+iNOAm97WMGY0AFrguU1cpfW3I5jINkWe5BIY5md0ofy+1TCIELsVcm/GJXZSaPbw==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, - "requires": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - } - }, - "typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", - "dev": true - }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, - "v8-to-istanbul": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", - "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - } - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true - }, - "yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "requires": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - } - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } } } diff --git a/package.json b/package.json index 493ff4d..a485cbc 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "license": "MIT", "dependencies": { "@ethersproject/abi": "^5.7.0", - "ethereum-cryptography": "^1.1.2" + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0" }, "devDependencies": { "@types/mocha": "^10.0.0", diff --git a/src/bytes.ts b/src/bytes.ts index aed0e0a..ea9054f 100644 --- a/src/bytes.ts +++ b/src/bytes.ts @@ -1,19 +1,20 @@ -import { bytesToHex } from 'ethereum-cryptography/utils'; +import type { BytesLike, Hexable } from "@ethersproject/bytes"; -export type Bytes = Uint8Array; +type Hex = BytesLike | Hexable | number | bigint; +type HexString = string; -export function compareBytes(a: Bytes, b: Bytes): number { - const n = Math.min(a.length, b.length); +import { + isBytesLike, + arrayify as toBytes, + hexlify as toHex, + concat, + isBytes, +} from "@ethersproject/bytes"; - for (let i = 0; i < n; i++) { - if (a[i] !== b[i]) { - return a[i]! - b[i]!; - } - } - - return a.length - b.length; +function compare(a: Hex, b: Hex): number { + const diff = BigInt(toHex(a)) - BigInt(toHex(b)); + return diff > 0 ? 1 : diff < 0 ? -1 : 0; } -export function hex(b: Bytes): string { - return '0x' + bytesToHex(b); -} +export type { Hex, HexString, BytesLike }; +export { isBytesLike, toBytes, toHex, concat, compare, isBytes }; diff --git a/src/core.test.ts b/src/core.test.ts index ac83990..737cae6 100644 --- a/src/core.test.ts +++ b/src/core.test.ts @@ -1,13 +1,11 @@ import fc from 'fast-check'; import assert from 'assert/strict'; -import { equalsBytes } from 'ethereum-cryptography/utils'; +import { HashZero as zero } from '@ethersproject/constants'; +import { keccak256 } from '@ethersproject/keccak256'; import { makeMerkleTree, getProof, processProof, getMultiProof, processMultiProof, isValidMerkleTree, renderMerkleTree } from './core'; -import { compareBytes, hex } from './bytes'; -import { keccak256 } from 'ethereum-cryptography/keccak'; +import { toHex, compare } from './bytes'; -const zero = new Uint8Array(32); - -const leaf = fc.uint8Array({ minLength: 32, maxLength: 32 }).map(x => PrettyBytes.from(x)); +const leaf = fc.uint8Array({ minLength: 32, maxLength: 32 }).map(toHex); const leaves = fc.array(leaf, { minLength: 1 }); const leavesAndIndex = leaves.chain(xs => fc.tuple(fc.constant(xs), fc.nat({ max: xs.length - 1 }))); const leavesAndIndices = leaves.chain(xs => fc.tuple(fc.constant(xs), fc.uniqueArray(fc.nat({ max: xs.length - 1 })))); @@ -25,7 +23,7 @@ describe('core properties', () => { const proof = getProof(tree, treeIndex); const leaf = leaves[leafIndex]!; const impliedRoot = processProof(leaf, proof); - return equalsBytes(root, impliedRoot); + return root === impliedRoot; }), ); }); @@ -41,7 +39,7 @@ describe('core properties', () => { if (leafIndices.length !== proof.leaves.length) return false; if (leafIndices.some(i => !proof.leaves.includes(leaves[i]!))) return false; const impliedRoot = processMultiProof(proof); - return equalsBytes(root, impliedRoot); + return root === impliedRoot; }), ); }); @@ -79,7 +77,7 @@ describe('core error conditions', () => { const tree = makeMerkleTree([leaf, zero]); const badMultiProof = { - leaves: [128, 129].map(n => keccak256(Uint8Array.of(n))).sort(compareBytes), + leaves: [128, 129].map(n => keccak256(Uint8Array.of(n))).sort(compare), proof: [leaf, leaf], proofFlags: [true, true, false], }; @@ -89,11 +87,4 @@ describe('core error conditions', () => { /^Error: Broken invariant$/, ); }); - }); - -class PrettyBytes extends Uint8Array { - [fc.toStringMethod]() { - return hex(this); - } -} diff --git a/src/core.ts b/src/core.ts index a44c405..dba1c94 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,9 +1,8 @@ -import { keccak256 } from 'ethereum-cryptography/keccak'; -import { concatBytes, bytesToHex, equalsBytes } from 'ethereum-cryptography/utils'; -import { Bytes, compareBytes } from './bytes'; +import { keccak256 } from '@ethersproject/keccak256'; +import { BytesLike, HexString, toHex, toBytes, concat, compare } from './bytes'; import { throwError } from './utils/throw-error'; -const hashPair = (a: Bytes, b: Bytes) => keccak256(concatBytes(...[a, b].sort(compareBytes))); +const hashPair = (a: BytesLike, b: BytesLike): HexString => keccak256(concat([a, b].sort(compare))); const leftChildIndex = (i: number) => 2 * i + 1; const rightChildIndex = (i: number) => 2 * i + 2; @@ -13,24 +12,22 @@ const siblingIndex = (i: number) => i > 0 ? i - (-1) ** (i % 2) : throwEr const isTreeNode = (tree: unknown[], i: number) => i >= 0 && i < tree.length; const isInternalNode = (tree: unknown[], i: number) => isTreeNode(tree, leftChildIndex(i)); const isLeafNode = (tree: unknown[], i: number) => isTreeNode(tree, i) && !isInternalNode(tree, i); -const isValidMerkleNode = (node: Bytes) => node instanceof Uint8Array && node.length === 32; +const isValidMerkleNode = (node: BytesLike) => toBytes(node).length === 32; -const checkTreeNode = (tree: unknown[], i: number) => void (isTreeNode(tree, i) || throwError('Index is not in tree')); -const checkInternalNode = (tree: unknown[], i: number) => void (isInternalNode(tree, i) || throwError('Index is not an internal tree node')); const checkLeafNode = (tree: unknown[], i: number) => void (isLeafNode(tree, i) || throwError('Index is not a leaf')); -const checkValidMerkleNode = (node: Bytes) => void (isValidMerkleNode(node) || throwError('Merkle tree nodes must be Uint8Array of length 32')); +const checkValidMerkleNode = (node: BytesLike) => void (isValidMerkleNode(node) || throwError('Merkle tree nodes must be Uint8Array of length 32')); -export function makeMerkleTree(leaves: Bytes[]): Bytes[] { +export function makeMerkleTree(leaves: BytesLike[]): HexString[] { leaves.forEach(checkValidMerkleNode); if (leaves.length === 0) { throw new Error('Expected non-zero number of leaves'); } - const tree = new Array(2 * leaves.length - 1); + const tree = new Array(2 * leaves.length - 1); for (const [i, leaf] of leaves.entries()) { - tree[tree.length - 1 - i] = leaf; + tree[tree.length - 1 - i] = toHex(leaf); } for (let i = tree.length - 1 - leaves.length; i >= 0; i--) { tree[i] = hashPair( @@ -42,7 +39,7 @@ export function makeMerkleTree(leaves: Bytes[]): Bytes[] { return tree; } -export function getProof(tree: Bytes[], index: number): Bytes[] { +export function getProof(tree: BytesLike[], index: number): HexString[] { checkLeafNode(tree, index); const proof = []; @@ -50,23 +47,23 @@ export function getProof(tree: Bytes[], index: number): Bytes[] { proof.push(tree[siblingIndex(index)]!); index = parentIndex(index); } - return proof; + return proof.map(node => toHex(node)); } -export function processProof(leaf: Bytes, proof: Bytes[]): Bytes { +export function processProof(leaf: BytesLike, proof: BytesLike[]): HexString { checkValidMerkleNode(leaf); proof.forEach(checkValidMerkleNode); - return proof.reduce(hashPair, leaf); + return toHex(proof.reduce(hashPair, leaf)); } -export interface MultiProof { - leaves: L[]; - proof: T[]; +export interface MultiProof { + leaves: BytesLike[]; + proof: HexString[]; proofFlags: boolean[]; } -export function getMultiProof(tree: Bytes[], indices: number[]): MultiProof { +export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof { indices.forEach(i => checkLeafNode(tree, i)); indices.sort((a, b) => b - a); @@ -98,13 +95,13 @@ export function getMultiProof(tree: Bytes[], indices: number[]): MultiProof tree[i]!), - proof, + leaves: indices.map(i => tree[i]!).map(node => toHex(node)), + proof: proof.map(node => toHex(node)), proofFlags, }; } -export function processMultiProof(multiproof: MultiProof): Bytes { +export function processMultiProof(multiproof: MultiProof): HexString { multiproof.leaves.forEach(checkValidMerkleNode); multiproof.proof.forEach(checkValidMerkleNode); @@ -132,10 +129,10 @@ export function processMultiProof(multiproof: MultiProof): Bytes { throw new Error('Broken invariant'); } - return stack.pop() ?? proof.shift()!; + return toHex(stack.pop() ?? proof.shift()!); } -export function isValidMerkleTree(tree: Bytes[]): boolean { +export function isValidMerkleTree(tree: BytesLike[]): boolean { for (const [i, node] of tree.entries()) { if (!isValidMerkleNode(node)) { return false; @@ -148,7 +145,7 @@ export function isValidMerkleTree(tree: Bytes[]): boolean { if (l < tree.length) { return false; } - } else if (!equalsBytes(node, hashPair(tree[l]!, tree[r]!))) { + } else if (node !== hashPair(tree[l]!, tree[r]!)) { return false; } } @@ -156,7 +153,7 @@ export function isValidMerkleTree(tree: Bytes[]): boolean { return tree.length > 0; } -export function renderMerkleTree(tree: Bytes[]): string { +export function renderMerkleTree(tree: BytesLike[]): HexString { if (tree.length === 0) { throw new Error('Expected non-zero number of nodes'); } @@ -172,7 +169,7 @@ export function renderMerkleTree(tree: Bytes[]): string { path.slice(0, -1).map(p => [' ', '│ '][p]).join('') + path.slice(-1).map(p => ['└─ ', '├─ '][p]).join('') + i + ') ' + - bytesToHex(tree[i]!) + toHex(tree[i]!) ); if (rightChildIndex(i) < tree.length) { diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..cec81fe --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,9 @@ +import assert from "assert/strict"; +import { SimpleMerkleTree, StandardMerkleTree } from "."; + +describe("index properties", () => { + it("classes are exported", () => { + assert.notEqual(SimpleMerkleTree, undefined); + assert.notEqual(StandardMerkleTree, undefined); + }); +}); diff --git a/src/index.ts b/src/index.ts index 7d16c9d..f00f42f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ +export { SimpleMerkleTree } from './simple'; export { StandardMerkleTree } from './standard'; diff --git a/src/options.ts b/src/options.ts index 3516460..31f3d24 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,11 +1,17 @@ -// MerkleTree building options -export type MerkleTreeOptions = Partial<{ +// SimpleMerkleTree building options +export type SimpleMerkleTreeOptions = Partial<{ /** Enable or disable sorted leaves. Sorting is strongly recommended for multiproofs. */ sortLeaves: boolean; }>; -// Recommended (default) options. +// StandardMerkleTree building options +export type StandardMerkleTreeOptions = SimpleMerkleTreeOptions & { + /** ABI Encoding for leaf values. */ + leafEncoding: string[]; +}; + +// Recommended (default) SimpleMerkleTree options. // - leaves are sorted by default to facilitate onchain verification of multiproofs. -export const defaultOptions: Required = { +export const defaultOptions: Required = { sortLeaves: true, }; diff --git a/src/simple.test.ts b/src/simple.test.ts new file mode 100644 index 0000000..134d811 --- /dev/null +++ b/src/simple.test.ts @@ -0,0 +1,167 @@ +import assert from "assert/strict"; +import { HashZero as zero } from "@ethersproject/constants"; +import { keccak256 } from "@ethersproject/keccak256"; +import { SimpleMerkleTree } from "./simple"; + +describe("simple merkle tree", () => { + for (const opts of [{}, { sortLeaves: true }, { sortLeaves: false }]) { + describe(`with options '${JSON.stringify(opts)}'`, () => { + const leaves = "abcdef".split("").map((c) => [keccak256(Buffer.from(c))]); + const otherLeaves = "abc" + .split("") + .map((c) => [keccak256(Buffer.from(c))]); + const tree = SimpleMerkleTree.of(leaves, opts); + const otherTree = SimpleMerkleTree.of(otherLeaves, opts); + + it("generates a valid tree", () => { + tree.validate(); + }); + + it("generates valid single proofs for all leaves", () => { + for (const [id, leaf] of tree.entries()) { + const proof1 = tree.getProof(id); + const proof2 = tree.getProof(leaf); + + assert.deepEqual(proof1, proof2); + + assert(tree.verify(id, proof1)); + assert(tree.verify(leaf, proof1)); + assert(SimpleMerkleTree.verify(tree.root, leaf, proof1)); + } + }); + + it("rejects invalid proofs", () => { + const leaf = leaves[0]!; + const invalidProof = otherTree.getProof(leaf); + + assert(!tree.verify(leaf, invalidProof)); + assert(!SimpleMerkleTree.verify(tree.root, leaf, invalidProof)); + }); + + it("generates valid multiproofs", () => { + for (const ids of [ + [], + [0, 1], + [0, 1, 5], + [1, 3, 4, 5], + [0, 2, 4, 5], + [0, 1, 2, 3, 4, 5], + [4, 1, 5, 0, 2], + ]) { + const proof1 = tree.getMultiProof(ids); + const proof2 = tree.getMultiProof(ids.map((i) => leaves[i]!)); + + assert.deepEqual(proof1, proof2); + + assert(tree.verifyMultiProof(proof1)); + assert(SimpleMerkleTree.verifyMultiProof(tree.root, proof1)); + } + }); + + it("rejects invalid multiproofs", () => { + const multiProof = otherTree.getMultiProof(leaves.slice(0, 3)); + + assert(!tree.verifyMultiProof(multiProof)); + assert(!SimpleMerkleTree.verifyMultiProof(tree.root, multiProof)); + }); + + it("renders tree representation", () => { + assert.equal( + tree.render(), + opts.sortLeaves == false + ? [ + "0) 0x9012f1e18a87790d2e01faace75aaaca38e53df437cdce2c0552464dda4af49c", + "├─ 1) 0x68203f90e9d07dc5859259d7536e87a6ba9d345f2552b5b9de2999ddce9ce1bf", + "│ ├─ 3) 0xd253a52d4cb00de2895e85f2529e2976e6aaaa5c18106b68ab66813e14415669", + "│ │ ├─ 7) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3", + "│ │ └─ 8) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2", + "│ └─ 4) 0x805b21d846b189efaeb0377d6bb0d201b3872a363e607c25088f025b0c6ae1f8", + "│ ├─ 9) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510", + "│ └─ 10) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb", + "└─ 2) 0xf0b49bb4b0d9396e0315755ceafaa280707b32e75e6c9053f5cdf2679dcd5c6a", + " ├─ 5) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483", + " └─ 6) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761", + ].join("\n") + : [ + "0) 0x1b404f199ea828ec5771fb30139c222d8417a82175fefad5cd42bc3a189bd8d5", + "├─ 1) 0xec554bdfb01d31fa838d0830339b0e6e8a70e0d55a8f172ffa8bebbf8e8d5ba0", + "│ ├─ 3) 0x434d51cfeb80272378f4c3a8fd2824561c2cad9fce556ea600d46f20550976a6", + "│ │ ├─ 7) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510", + "│ │ └─ 8) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761", + "│ └─ 4) 0x7dea550f679f3caab547cbbc5ee1a4c978c8c039b572ba00af1baa6481b88360", + "│ ├─ 9) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb", + "│ └─ 10) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2", + "└─ 2) 0xaf46af0745b433e1d5bed9a04b1fdf4002f67a733c20db2fca5b2af6120d9bcb", + " ├─ 5) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3", + " └─ 6) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483", + ].join("\n") + ); + }); + + it("dump and load", () => { + const recoveredTree = SimpleMerkleTree.load(tree.dump()); + + recoveredTree.validate(); + assert.deepEqual(tree, recoveredTree); + }); + + it("reject out of bounds value index", () => { + assert.throws( + () => tree.getProof(leaves.length), + /^Error: Index out of bounds$/ + ); + }); + + it("reject invalid leaf size", () => { + const invalidLeaf = [zero + "00"]; // 33 bytes (all zero) + assert.throws( + () => SimpleMerkleTree.of([invalidLeaf], opts), + `Error: ${invalidLeaf} is not a valid 32 bytes object (pos: 0)` + ); + }); + }); + } + + describe("tree dumps", () => { + it("reject unrecognized tree dump", () => { + assert.throws( + () => SimpleMerkleTree.load({ format: "nonstandard" } as any), + /^Error: Unknown format 'nonstandard'$/ + ); + + assert.throws( + () => SimpleMerkleTree.load({ format: "standard-v1" } as any), + /^Error: Unknown format 'standard-v1'$/ + ); + }); + + it("reject malformed tree dump", () => { + const loadedTree1 = SimpleMerkleTree.load({ + format: "simple-v1", + tree: [zero], + values: [ + { + value: [ + "0x0000000000000000000000000000000000000000000000000000000000000001", + ], + treeIndex: 0, + }, + ], + }); + assert.throws( + () => loadedTree1.getProof(0), + /^Error: Merkle tree does not contain the expected value$/ + ); + + const loadedTree2 = SimpleMerkleTree.load({ + format: "simple-v1", + tree: [zero, zero, zero], + values: [{ value: [zero], treeIndex: 2 }], + }); + assert.throws( + () => loadedTree2.getProof(0), + /^Error: Unable to prove value$/ + ); + }); + }); +}); diff --git a/src/simple.ts b/src/simple.ts new file mode 100644 index 0000000..2112f69 --- /dev/null +++ b/src/simple.ts @@ -0,0 +1,255 @@ +import { defaultAbiCoder } from "@ethersproject/abi"; +import { BytesLike, HexString, isBytesLike, toHex, compare } from "./bytes"; + +import { + MultiProof, + makeMerkleTree, + isValidMerkleTree, + getProof, + getMultiProof, + processProof, + processMultiProof, + renderMerkleTree, +} from "./core"; + +import { SimpleMerkleTreeOptions, defaultOptions } from "./options"; +import { checkBounds } from "./utils/check-bounds"; +import { throwError } from "./utils/throw-error"; + +export type SimpleMerkleTreeValue = { + value: BytesLike[]; + treeIndex: number; +}; + +export type SimpleMerkleTreeData = { + format: string; + tree: HexString[]; + values: SimpleMerkleTreeValue[]; +}; + +/// @notice Not an actual hash function, just a placeholder +export function simpleLeafHash(value: BytesLike[]): HexString { + return defaultAbiCoder.encode(["bytes32"], value); +} + +export interface MerkleTree { + root: HexString; + dump(): SimpleMerkleTreeData; + render(): string; + entries(): Iterable<[number, T]>; + validate(): void; + leafLookup(leaf: T): number; + getProof(leaf: number | T): HexString[]; + getMultiProof(leaves: (number | T)[]): MultiProof; + verify(leaf: number | T, proof: HexString[]): boolean; + verifyMultiProof(multiproof: MultiProof): boolean; +} + +export class SimpleMerkleTree implements MerkleTree { + private readonly hashLookup: { [hash: HexString]: number }; + + protected constructor( + protected readonly tree: HexString[], + protected readonly values: SimpleMerkleTreeValue[], + protected readonly options: SimpleMerkleTreeOptions = {} + ) { + this.options = Object.assign(options, defaultOptions); + this.hashLookup = Object.fromEntries( + values.map(({ value }, valueIndex) => [this.leafHash(value), valueIndex]) + ); + } + + protected static parameters( + values: BytesLike[][], + options: SimpleMerkleTreeOptions = {}, + leafHash: (value: BytesLike[]) => HexString = simpleLeafHash + ): [HexString[], indexedValues: SimpleMerkleTreeValue[]] { + const { sortLeaves } = { ...defaultOptions, ...options }; + + const hashedValues = values.map((value, valueIndex) => ({ + value, + valueIndex, + hash: leafHash(value), + })); + + if (sortLeaves) { + hashedValues.sort((a, b) => compare(a.hash, b.hash)); + } + + const tree = makeMerkleTree(hashedValues.map((v) => v.hash)); + + const indexedValues = values.map((value) => ({ + value, + treeIndex: 0, + })); + for (const [leafIndex, { valueIndex }] of hashedValues.entries()) { + indexedValues[valueIndex]!.treeIndex = tree.length - leafIndex - 1; + } + + return [tree, indexedValues]; + } + + static of( + values: BytesLike[][], + options: O + ): SimpleMerkleTree { + return new this(...this.parameters(values, options)); + } + + static load(data: D): SimpleMerkleTree { + if (data.format !== "simple-v1") { + throwError(`Unknown format '${data.format}'`); + } + + return new this( + data.tree, + data.values.map(({ value, treeIndex }) => ({ + value: value.map((v) => toHex(v)), + treeIndex, + })) + ); + } + + static verify( + root: BytesLike, + leaf: T, + proof: BytesLike[], + _?: SimpleMerkleTreeOptions + ): boolean { + return toHex(root) === processProof(simpleLeafHash(leaf), proof); + } + + static verifyMultiProof( + root: BytesLike, + multiproof: MultiProof, + _?: SimpleMerkleTreeOptions + ): boolean { + return ( + toHex(root) === + processMultiProof({ + leaves: multiproof.leaves, + proof: multiproof.proof, + proofFlags: multiproof.proofFlags, + }) + ); + } + + get root(): HexString { + return this.tree[0]!; + } + + dump(): SimpleMerkleTreeData { + return { + format: "simple-v1", + tree: this.tree, + values: this.values, + }; + } + + render() { + return renderMerkleTree(this.tree); + } + + *entries(): Iterable<[number, BytesLike[]]> { + for (const [i, { value }] of this.values.entries()) { + yield [i, value]; + } + } + + validate() { + for (let i = 0; i < this.values.length; i++) { + this.validateValue(i); + } + if (!isValidMerkleTree(this.tree)) { + throwError("Merkle tree is invalid"); + } + } + + leafLookup(leaf: BytesLike[]): number { + return ( + this.hashLookup[toHex(this.leafHash(leaf))] ?? + throwError("Leaf is not in tree") + ); + } + + getProof(leaf: number | BytesLike[]): HexString[] { + // input validity + const valueIndex = typeof leaf === "number" ? leaf : this.leafLookup(leaf); + this.validateValue(valueIndex); + + // rebuild tree index and generate proof + const { treeIndex } = this.values[valueIndex]!; + const proof = getProof(this.tree, treeIndex); + + // sanity check proof + if (!this._verify(this.tree[treeIndex]!, proof)) { + throwError("Unable to prove value"); + } + + // return proof in hex format + return proof; + } + + getMultiProof(leaves: (number | BytesLike[])[]): MultiProof { + // input validity + const valueIndices = leaves.map((leaf) => + typeof leaf === "number" ? leaf : this.leafLookup(leaf) + ); + for (const valueIndex of valueIndices) this.validateValue(valueIndex); + + // rebuild tree indices and generate proof + const indices = valueIndices.map((i) => this.values[i]!.treeIndex); + const proof = getMultiProof(this.tree, indices); + + // sanity check proof + if (!this._verifyMultiProof(proof)) { + throwError("Unable to prove values"); + } + + // return multiproof in hex format + return { + leaves: proof.leaves, + proof: proof.proof, + proofFlags: proof.proofFlags, + }; + } + + verify(leaf: number | BytesLike[], proof: HexString[]): boolean { + return this._verify(this.leafHash(leaf), proof); + } + + verifyMultiProof(multiproof: MultiProof): boolean { + return this._verifyMultiProof({ + leaves: multiproof.leaves, + proof: multiproof.proof, + proofFlags: multiproof.proofFlags, + }); + } + + protected validateValue(valueIndex: number): HexString { + checkBounds(this.values, valueIndex); + const { value: leaf, treeIndex } = this.values[valueIndex]!; + checkBounds(this.tree, treeIndex); + const hashedLeaf = this.leafHash(leaf); + if (hashedLeaf !== this.tree[treeIndex]!) { + throwError("Merkle tree does not contain the expected value"); + } + return hashedLeaf; + } + + protected leafHash(leaf: number | BytesLike[]): HexString { + if (Array.isArray(leaf)) { + return simpleLeafHash(leaf); + } else { + return this.validateValue(leaf); + } + } + + private _verify(leafHash: HexString, proof: HexString[]): boolean { + return this.root === processProof(leafHash, proof); + } + + private _verifyMultiProof(multiproof: MultiProof): boolean { + return this.root === processMultiProof(multiproof); + } +} diff --git a/src/standard.test.ts b/src/standard.test.ts index cd1217d..a2a7d3d 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -1,33 +1,39 @@ -import assert from 'assert/strict'; -import { keccak256 } from 'ethereum-cryptography/keccak'; -import { hex } from './bytes'; -import { MerkleTreeOptions } from './options'; -import { StandardMerkleTree } from './standard'; - -const zeroBytes = new Uint8Array(32); -const zero = hex(zeroBytes); - -const makeTree = (s: string, opts: MerkleTreeOptions = {}) => { - const l = s.split('').map(c => [c]); - const t = StandardMerkleTree.of(l, ['string'], opts); - return { l, t }; -} - -describe('standard merkle tree', () => { - for (const opts of [ - {}, - { sortLeaves: true }, - { sortLeaves: false }, - ]) { +import assert from "assert/strict"; +import { HashZero as zero } from "@ethersproject/constants"; +import { keccak256 } from "@ethersproject/keccak256"; +import { StandardMerkleTree } from "./standard"; +import { StandardMerkleTreeOptions } from "./options"; + +describe("standard merkle tree", () => { + for (const opts of [{}, { sortLeaves: true }, { sortLeaves: false }]) { describe(`with options '${JSON.stringify(opts)}'`, () => { - const { l: leaves, t: tree } = makeTree('abcdef', opts); - const { l: otherLeaves, t: otherTree } = makeTree('abc', opts); + const leaves = "abcdef".split("").map((c) => [c]); + const otherLeaves = "abc".split("").map((c) => [c]); - it('generates valid single proofs for all leaves', () => { + const tree = StandardMerkleTree.of( + leaves, + Object.assign(opts, { leafEncoding: ["string"] }) + ); + + const otherTree = StandardMerkleTree.of(otherLeaves, ["string"]); + + it("rejects loading a tree without leaf encoding", () => { + assert.throws( + () => + StandardMerkleTree.load({ + format: "standard-v1", + tree: [zero], + values: [{ value: ["0"], treeIndex: 0 }], + }), + /^Error: Expected leaf encoding$/ + ); + }); + + it("generates a valid tree", () => { tree.validate(); }); - it('generates valid single proofs for all leaves', () => { + it("generates valid single proofs for all leaves", () => { for (const [id, leaf] of tree.entries()) { const proof1 = tree.getProof(id); const proof2 = tree.getProof(leaf); @@ -36,114 +42,169 @@ describe('standard merkle tree', () => { assert(tree.verify(id, proof1)); assert(tree.verify(leaf, proof1)); - assert(StandardMerkleTree.verify(tree.root, ['string'], leaf, proof1)); + assert( + StandardMerkleTree.verify(tree.root, ["string"], leaf, proof1) + ); + assert( + StandardMerkleTree.verify(tree.root, leaf, proof1, { + leafEncoding: ["string"], + }) + ); } }); - it('rejects invalid proofs', () => { - const leaf = ['a']; + it("rejects invalid proofs", () => { + const leaf = leaves[0]!; const invalidProof = otherTree.getProof(leaf); assert(!tree.verify(leaf, invalidProof)); - assert(!StandardMerkleTree.verify(tree.root, ['string'], leaf, invalidProof)); + assert( + !StandardMerkleTree.verify(tree.root, ["string"], leaf, invalidProof) + ); + assert( + !StandardMerkleTree.verify(tree.root, leaf, invalidProof, { + leafEncoding: ["string"], + }) + ); }); - it('generates valid multiproofs', () => { - for (const ids of [[], [0, 1], [0, 1, 5], [1, 3, 4, 5], [0, 2, 4, 5], [0, 1, 2, 3, 4, 5], [4, 1, 5, 0, 2]]) { + it("generates valid multiproofs", () => { + for (const ids of [ + [], + [0, 1], + [0, 1, 5], + [1, 3, 4, 5], + [0, 2, 4, 5], + [0, 1, 2, 3, 4, 5], + [4, 1, 5, 0, 2], + ]) { const proof1 = tree.getMultiProof(ids); - const proof2 = tree.getMultiProof(ids.map(i => leaves[i]!)); + const proof2 = tree.getMultiProof(ids.map((i) => leaves[i]!)); assert.deepEqual(proof1, proof2); assert(tree.verifyMultiProof(proof1)); - assert(StandardMerkleTree.verifyMultiProof(tree.root, ['string'], proof1)); + assert( + StandardMerkleTree.verifyMultiProof(tree.root, ["string"], proof1) + ); + // assert( + // StandardMerkleTree.verifyMultiProof(tree.root, proof1, { + // leafEncoding: ["string"], + // }) + // ); } }); - it('rejects invalid multiproofs', () => { - const multiProof = otherTree.getMultiProof([['a'], ['b'], ['c']]); + it("rejects invalid multiproofs", () => { + const multiProof = otherTree.getMultiProof(leaves.slice(0, 3)); assert(!tree.verifyMultiProof(multiProof)); - assert(!StandardMerkleTree.verifyMultiProof(tree.root, ['string'], multiProof)); + assert( + !StandardMerkleTree.verifyMultiProof( + tree.root, + ["string"], + multiProof + ) + ); + // assert( + // !StandardMerkleTree.verifyMultiProof(tree.root, multiProof, { + // leafEncoding: ["string"], + // }) + // ); }); - it('renders tree representation', () => { + it("renders tree representation", () => { assert.equal( tree.render(), opts.sortLeaves == false ? [ - "0) 23be0977360f08bb0bd7f709a7d543d2cd779c79c66d74e0441919871647de2b", - "├─ 1) 8f7234e8cfe39c08ca84a3a3e3274f574af26fd15165fe29e09cbab742daccd9", - "│ ├─ 3) 03707d7802a71ca56a8ad8028da98c4f1dbec55b31b4a25d536b5309cc20eda9", - "│ │ ├─ 7) eba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b", - "│ │ └─ 8) 9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b", - "│ └─ 4) fa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016", - "│ ├─ 9) 19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681", - "│ └─ 10) 9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c", - "└─ 2) 7b0c6cd04b82bfc0e250030a5d2690c52585e0cc6a4f3bc7909d7723b0236ece", - " ├─ 5) c62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848", - " └─ 6) 9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e", + "0) 0x23be0977360f08bb0bd7f709a7d543d2cd779c79c66d74e0441919871647de2b", + "├─ 1) 0x8f7234e8cfe39c08ca84a3a3e3274f574af26fd15165fe29e09cbab742daccd9", + "│ ├─ 3) 0x03707d7802a71ca56a8ad8028da98c4f1dbec55b31b4a25d536b5309cc20eda9", + "│ │ ├─ 7) 0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b", + "│ │ └─ 8) 0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b", + "│ └─ 4) 0xfa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016", + "│ ├─ 9) 0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681", + "│ └─ 10) 0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c", + "└─ 2) 0x7b0c6cd04b82bfc0e250030a5d2690c52585e0cc6a4f3bc7909d7723b0236ece", + " ├─ 5) 0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848", + " └─ 6) 0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e", ].join("\n") : [ - "0) 6deb52b5da8fd108f79fab00341f38d2587896634c646ee52e49f845680a70c8", - "├─ 1) 52426e0f1f65ff7e209a13b8c29cffe82e3acaf3dad0a9b9088f3b9a61a929c3", - "│ ├─ 3) 8076923e76cf01a7c048400a2304c9a9c23bbbdac3a98ea3946340fdafbba34f", - "│ │ ├─ 7) 9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b", - "│ │ └─ 8) 9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c", - "│ └─ 4) 965b92c6cf08303cc4feb7f3e0819c436c2cec17c6f0688a6af139c9a368707c", - "│ ├─ 9) 9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e", - "│ └─ 10) 19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681", - "└─ 2) fd3cf45654e88d1cc5d663578c82c76f4b5e3826bacaa1216441443504538f51", - " ├─ 5) eba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b", - " └─ 6) c62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848", - ].join("\n"), + "0) 0x6deb52b5da8fd108f79fab00341f38d2587896634c646ee52e49f845680a70c8", + "├─ 1) 0x52426e0f1f65ff7e209a13b8c29cffe82e3acaf3dad0a9b9088f3b9a61a929c3", + "│ ├─ 3) 0x8076923e76cf01a7c048400a2304c9a9c23bbbdac3a98ea3946340fdafbba34f", + "│ │ ├─ 7) 0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b", + "│ │ └─ 8) 0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c", + "│ └─ 4) 0x965b92c6cf08303cc4feb7f3e0819c436c2cec17c6f0688a6af139c9a368707c", + "│ ├─ 9) 0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e", + "│ └─ 10) 0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681", + "└─ 2) 0xfd3cf45654e88d1cc5d663578c82c76f4b5e3826bacaa1216441443504538f51", + " ├─ 5) 0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b", + " └─ 6) 0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848", + ].join("\n") ); }); - it('dump and load', () => { + it("dump and load", () => { const recoveredTree = StandardMerkleTree.load(tree.dump()); recoveredTree.validate(); assert.deepEqual(tree, recoveredTree); }); - it('reject out of bounds value index', () => { + it("reject out of bounds value index", () => { assert.throws( () => tree.getProof(leaves.length), - /^Error: Index out of bounds$/, - ); - }); - - it('reject unrecognized tree dump', () => { - assert.throws( - () => StandardMerkleTree.load({ format: 'nonstandard' } as any), - /^Error: Unknown format 'nonstandard'$/, + /^Error: Index out of bounds$/ ); }); + }); + } - it('reject malformed tree dump', () => { - const loadedTree1 = StandardMerkleTree.load({ - format: 'standard-v1', - tree: [zero], - values: [{ value: ['0'], treeIndex: 0 }], - leafEncoding: ['uint256'], - }); - assert.throws( - () => loadedTree1.getProof(0), - /^Error: Merkle tree does not contain the expected value$/, - ); + describe("tree dumps", () => { + it("reject unrecognized tree dump", () => { + assert.throws( + () => + StandardMerkleTree.load({ + format: "nonstandard", + leafEncoding: ["string"], + } as any), + /^Error: Unknown format 'nonstandard'$/ + ); + + assert.throws( + () => + StandardMerkleTree.load({ + format: "simple-v1", + leafEncoding: ["string"], + } as any), + /^Error: Unknown format 'simple-v1'$/ + ); + }); - const loadedTree2 = StandardMerkleTree.load({ - format: 'standard-v1', - tree: [zero, zero, hex(keccak256(keccak256(zeroBytes)))], - values: [{ value: ['0'], treeIndex: 2 }], - leafEncoding: ['uint256'], - }); - assert.throws( - () => loadedTree2.getProof(0), - /^Error: Unable to prove value$/, - ); + it("reject malformed tree dump", () => { + const loadedTree1 = StandardMerkleTree.load({ + format: "standard-v1", + tree: [zero], + values: [{ value: ["0"], treeIndex: 0 }], + leafEncoding: ["uint256"], }); + assert.throws( + () => loadedTree1.getProof(0), + /^Error: Merkle tree does not contain the expected value$/ + ); + + const loadedTree2 = StandardMerkleTree.load({ + format: "standard-v1", + tree: [zero, zero, keccak256(keccak256(zero))], + values: [{ value: ["0"], treeIndex: 2 }], + leafEncoding: ["uint256"], + }); + assert.throws( + () => loadedTree2.getProof(0), + /^Error: Unable to prove value$/ + ); }); - } + }); }); diff --git a/src/standard.ts b/src/standard.ts index 20b40e1..4bfcf05 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -1,202 +1,196 @@ -import { equalsBytes, hexToBytes } from 'ethereum-cryptography/utils'; -import { Bytes, compareBytes, hex } from './bytes'; -import { getProof, isValidMerkleTree, makeMerkleTree, processProof, renderMerkleTree, MultiProof, getMultiProof, processMultiProof } from './core'; -import { MerkleTreeOptions, defaultOptions } from './options'; -import { checkBounds } from './utils/check-bounds'; -import { throwError } from './utils/throw-error'; -import { standardLeafHash } from './utils/standard-leaf-hash'; - -interface StandardMerkleTreeData { - format: 'standard-v1'; - tree: string[]; - values: { - value: T; - treeIndex: number; - }[]; - leafEncoding: string[]; -} +import { BytesLike, HexString, toHex, isBytes, isBytesLike } from "./bytes"; -export class StandardMerkleTree { - private readonly hashLookup: { [hash: string]: number }; +import { MultiProof, processProof, processMultiProof } from "./core"; - private constructor( - private readonly tree: Bytes[], - private readonly values: { value: T, treeIndex: number }[], - private readonly leafEncoding: string[], - ) { - this.hashLookup = - Object.fromEntries(values.map(({ value }, valueIndex) => [ - hex(standardLeafHash(value, leafEncoding)), - valueIndex, - ])); - } +import { + SimpleMerkleTree, + SimpleMerkleTreeData, + SimpleMerkleTreeValue, +} from "./simple"; - static of(values: T[], leafEncoding: string[], options: MerkleTreeOptions = {}) { - const sortLeaves = options.sortLeaves ?? defaultOptions.sortLeaves; +import { defaultAbiCoder } from "@ethersproject/abi"; +import { keccak256 } from "@ethersproject/keccak256"; - const hashedValues = values.map((value, valueIndex) => ({ value, valueIndex, hash: standardLeafHash(value, leafEncoding) })); +import { StandardMerkleTreeOptions } from "./options"; +import { throwError } from "./utils/throw-error"; - if (sortLeaves) { - hashedValues.sort((a, b) => compareBytes(a.hash, b.hash)); - } +export type StandardMerkleTreeValue = SimpleMerkleTreeValue; - const tree = makeMerkleTree(hashedValues.map(v => v.hash)); +export type StandardMerkleTreeData = SimpleMerkleTreeData & { + leafEncoding?: string[]; +}; - const indexedValues = values.map(value => ({ value, treeIndex: 0 })); - for (const [leafIndex, { valueIndex }] of hashedValues.entries()) { - indexedValues[valueIndex]!.treeIndex = tree.length - leafIndex - 1; - } +export function standardLeafHash( + value: T, + types: string[] +): HexString { + return keccak256(keccak256(defaultAbiCoder.encode(types, value))); +} - return new StandardMerkleTree(tree, indexedValues, leafEncoding); +export class StandardMerkleTree extends SimpleMerkleTree { + protected constructor( + protected readonly tree: HexString[], + protected readonly values: SimpleMerkleTreeValue[], + protected readonly options: StandardMerkleTreeOptions + ) { + super(tree, values, options); } - static load(data: StandardMerkleTreeData): StandardMerkleTree { - if (data.format !== 'standard-v1') { - throw new Error(`Unknown format '${data.format}'`); - } - return new StandardMerkleTree( - data.tree.map(hexToBytes), - data.values, - data.leafEncoding, + protected static parameters( + values: BytesLike[][], + options: StandardMerkleTreeOptions + ): [HexString[], indexedValues: SimpleMerkleTreeValue[]] { + return super.parameters(values, options, (value) => + standardLeafHash(value, options.leafEncoding) ); } - static verify(root: string, leafEncoding: string[], leaf: T, proof: string[]): boolean { - const impliedRoot = processProof(standardLeafHash(leaf, leafEncoding), proof.map(hexToBytes)); - return equalsBytes(impliedRoot, hexToBytes(root)); - } - - static verifyMultiProof(root: string, leafEncoding: string[], multiproof: MultiProof): boolean { - const leafHashes = multiproof.leaves.map(leaf => standardLeafHash(leaf, leafEncoding)); - const proofBytes = multiproof.proof.map(hexToBytes); - - const impliedRoot = processMultiProof({ - leaves: leafHashes, - proof: proofBytes, - proofFlags: multiproof.proofFlags, - }); - - return equalsBytes(impliedRoot, hexToBytes(root)); - } - - dump(): StandardMerkleTreeData { - return { - format: 'standard-v1', - tree: this.tree.map(hex), - values: this.values, - leafEncoding: this.leafEncoding, - }; - } - - render() { - return renderMerkleTree(this.tree); - } - - get root(): string { - return hex(this.tree[0]!); - } - - *entries(): Iterable<[number, T]> { - for (const [i, { value }] of this.values.entries()) { - yield [i, value]; + static of( + values: BytesLike[][], + options: StandardMerkleTreeOptions + ): StandardMerkleTree; + static of( + values: BytesLike[][], + leafEncoding: StandardMerkleTreeOptions["leafEncoding"] + ): StandardMerkleTree; + static of( + values: BytesLike[][], + optionsOrLeafEncoding: + | StandardMerkleTreeOptions + | StandardMerkleTreeOptions["leafEncoding"] + ): StandardMerkleTree { + if (Array.isArray(optionsOrLeafEncoding)) { + optionsOrLeafEncoding = { leafEncoding: optionsOrLeafEncoding }; } - } - validate() { - for (let i = 0; i < this.values.length; i++) { - this.validateValue(i); - } - if (!isValidMerkleTree(this.tree)) { - throw new Error('Merkle tree is invalid'); - } - } - - leafHash(leaf: T): string { - return hex(standardLeafHash(leaf, this.leafEncoding)); - } + const options = Object.assign({}, optionsOrLeafEncoding); - leafLookup(leaf: T): number { - return this.hashLookup[this.leafHash(leaf)] ?? throwError('Leaf is not in tree'); + return new StandardMerkleTree(...this.parameters(values, options), options); } - getProof(leaf: number | T): string[] { - // input validity - const valueIndex = typeof leaf === 'number' ? leaf : this.leafLookup(leaf); - this.validateValue(valueIndex); - - // rebuild tree index and generate proof - const { treeIndex } = this.values[valueIndex]!; - const proof = getProof(this.tree, treeIndex); - - // sanity check proof - if (!this._verify(this.tree[treeIndex]!, proof)) { - throw new Error('Unable to prove value'); + static load(data: D): StandardMerkleTree { + if (data.leafEncoding === undefined) { + throwError("Expected leaf encoding"); } - // return proof in hex format - return proof.map(hex); - } - - getMultiProof(leaves: (number | T)[]): MultiProof { - // input validity - const valueIndices = leaves.map(leaf => typeof leaf === 'number' ? leaf : this.leafLookup(leaf)); - for (const valueIndex of valueIndices) this.validateValue(valueIndex); - - // rebuild tree indices and generate proof - const indices = valueIndices.map(i => this.values[i]!.treeIndex); - const proof = getMultiProof(this.tree, indices); - - // sanity check proof - if (!this._verifyMultiProof(proof)) { - throw new Error('Unable to prove values'); + if (data.format !== "standard-v1") { + throw new Error(`Unknown format '${data.format}'`); } - // return multiproof in hex format - return { - leaves: proof.leaves.map(hash => this.values[this.hashLookup[hex(hash)]!]!.value), - proof: proof.proof.map(hex), - proofFlags: proof.proofFlags, + const leafEncoding = data.leafEncoding; + + return new StandardMerkleTree(data.tree, data.values, { leafEncoding }); + } + + static verify( + root: BytesLike, + leaf: BytesLike[], + proof: BytesLike[], + options: StandardMerkleTreeOptions + ): boolean; + static verify( + root: BytesLike, + leafEncoding: StandardMerkleTreeOptions["leafEncoding"], + leaf: BytesLike[], + proof: BytesLike[] + ): boolean; + static verify( + root: BytesLike, + leafOrLeafEncoding: BytesLike[] | StandardMerkleTreeOptions["leafEncoding"], + proofOrLeaf: BytesLike[] | BytesLike[], + optionsOrProof: StandardMerkleTreeOptions | BytesLike[] + ): boolean { + let leaf: BytesLike[]; + let proof: BytesLike[]; + let options: StandardMerkleTreeOptions; + + if ("leafEncoding" in optionsOrProof) { + leaf = leafOrLeafEncoding; + proof = proofOrLeaf; + options = optionsOrProof; + } else if (leafOrLeafEncoding.some((v) => !isBytesLike(v))) { + leaf = proofOrLeaf; + proof = optionsOrProof; + + options = { + leafEncoding: + leafOrLeafEncoding as StandardMerkleTreeOptions["leafEncoding"], + }; + } else { + throwError("Invalid arguments"); } - } - verify(leaf: number | T, proof: string[]): boolean { - return this._verify(this.getLeafHash(leaf), proof.map(hexToBytes)); - } - - private _verify(leafHash: Bytes, proof: Bytes[]): boolean { - const impliedRoot = processProof(leafHash, proof); - return equalsBytes(impliedRoot, this.tree[0]!); + return ( + toHex(root) === + processProof(standardLeafHash(leaf, options.leafEncoding), proof) + ); } - verifyMultiProof(multiproof: MultiProof): boolean { - return this._verifyMultiProof({ - leaves: multiproof.leaves.map(l => this.getLeafHash(l)), - proof: multiproof.proof.map(hexToBytes), - proofFlags: multiproof.proofFlags, - }); - } + static verifyMultiProof( + root: BytesLike, + leafEncoding: string[], + multiproof: MultiProof + ): boolean; + static verifyMultiProof( + root: BytesLike, + multiproof: MultiProof, + options?: StandardMerkleTreeOptions + ): boolean; + static verifyMultiProof( + root: BytesLike, + leafEncodingOrMultiproof: string[] | MultiProof, + multiProofOrOptions?: MultiProof | StandardMerkleTreeOptions + ): boolean { + let leafEncoding: string[]; + let multiproof: MultiProof; + let options: StandardMerkleTreeOptions; + + if ( + "proof" in leafEncodingOrMultiproof && + Array.isArray(multiProofOrOptions) + ) { + leafEncoding = multiProofOrOptions; + multiproof = leafEncodingOrMultiproof; + options = { leafEncoding }; + } else if (Array.isArray(leafEncodingOrMultiproof)) { + if ( + multiProofOrOptions === undefined || + "leafEncoding" in multiProofOrOptions + ) { + throwError("Invalid arguments"); + } + + leafEncoding = leafEncodingOrMultiproof; + multiproof = multiProofOrOptions; + options = { leafEncoding }; + } else { + throwError("Invalid arguments"); + } - private _verifyMultiProof(multiproof: MultiProof): boolean { - const impliedRoot = processMultiProof(multiproof); - return equalsBytes(impliedRoot, this.tree[0]!); + return ( + toHex(root) === + processMultiProof({ + leaves: multiproof.leaves, + proof: multiproof.proof, + proofFlags: multiproof.proofFlags, + }) + ); } - private validateValue(valueIndex: number): Bytes { - checkBounds(this.values, valueIndex); - const { value, treeIndex } = this.values[valueIndex]!; - checkBounds(this.tree, treeIndex); - const leaf = standardLeafHash(value, this.leafEncoding); - if (!equalsBytes(leaf, this.tree[treeIndex]!)) { - throw new Error('Merkle tree does not contain the expected value'); - } - return leaf; + dump(): StandardMerkleTreeData { + return { + format: "standard-v1", + tree: this.tree, + values: this.values, + leafEncoding: this.options.leafEncoding, + }; } - private getLeafHash(leaf: number | T): Bytes { - if (typeof leaf === 'number') { - return this.validateValue(leaf); + protected leafHash(leaf: number | BytesLike[]): HexString { + if (Array.isArray(leaf)) { + return standardLeafHash(leaf, this.options.leafEncoding); } else { - return standardLeafHash(leaf, this.leafEncoding); + return this.validateValue(leaf); } } } diff --git a/src/utils/standard-leaf-hash.ts b/src/utils/standard-leaf-hash.ts deleted file mode 100644 index df65671..0000000 --- a/src/utils/standard-leaf-hash.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { keccak256 } from 'ethereum-cryptography/keccak'; -import { hexToBytes } from 'ethereum-cryptography/utils'; -import { defaultAbiCoder } from '@ethersproject/abi'; -import { Bytes } from '../bytes'; - -export function standardLeafHash(value: T, types: string[]): Bytes { - return keccak256(keccak256(hexToBytes(defaultAbiCoder.encode(types, value)))); -} From f24e2056ffd06ac00b2d2c45a6d368febc68a030 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 23 Feb 2024 12:23:17 +0100 Subject: [PATCH 02/36] wip --- src/bytes.ts | 8 +- src/core.ts | 8 +- src/interface.ts | 21 +++++ src/options.ts | 14 +-- src/simple.test.ts | 14 ++- src/simple.ts | 126 ++++++++++----------------- src/standard.test.ts | 49 +++++------ src/standard.ts | 198 +++++++++---------------------------------- 8 files changed, 143 insertions(+), 295 deletions(-) create mode 100644 src/interface.ts diff --git a/src/bytes.ts b/src/bytes.ts index ea9054f..5f59c86 100644 --- a/src/bytes.ts +++ b/src/bytes.ts @@ -1,6 +1,4 @@ -import type { BytesLike, Hexable } from "@ethersproject/bytes"; - -type Hex = BytesLike | Hexable | number | bigint; +import type { BytesLike } from "@ethersproject/bytes"; type HexString = string; import { @@ -11,10 +9,10 @@ import { isBytes, } from "@ethersproject/bytes"; -function compare(a: Hex, b: Hex): number { +function compare(a: BytesLike, b: BytesLike): number { const diff = BigInt(toHex(a)) - BigInt(toHex(b)); return diff > 0 ? 1 : diff < 0 ? -1 : 0; } -export type { Hex, HexString, BytesLike }; +export type { HexString, BytesLike }; export { isBytesLike, toBytes, toHex, concat, compare, isBytes }; diff --git a/src/core.ts b/src/core.ts index dba1c94..3672314 100644 --- a/src/core.ts +++ b/src/core.ts @@ -57,13 +57,13 @@ export function processProof(leaf: BytesLike, proof: BytesLike[]): HexString { return toHex(proof.reduce(hashPair, leaf)); } -export interface MultiProof { - leaves: BytesLike[]; +export interface MultiProof { + leaves: T[]; proof: HexString[]; proofFlags: boolean[]; } -export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof { +export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof { indices.forEach(i => checkLeafNode(tree, i)); indices.sort((a, b) => b - a); @@ -101,7 +101,7 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof }; } -export function processMultiProof(multiproof: MultiProof): HexString { +export function processMultiProof(multiproof: MultiProof): HexString { multiproof.leaves.forEach(checkValidMerkleNode); multiproof.proof.forEach(checkValidMerkleNode); diff --git a/src/interface.ts b/src/interface.ts new file mode 100644 index 0000000..129986f --- /dev/null +++ b/src/interface.ts @@ -0,0 +1,21 @@ +import { HexString } from "./bytes"; +import { MultiProof } from "./core"; + +export type MerkleTreeData = { + format: string; + tree: HexString[]; + values: { value: T; treeIndex: number; }[]; +}; + +export interface MerkleTree { + root: HexString; + dump(): MerkleTreeData; + render(): string; + entries(): Iterable<[number, T]>; + validate(): void; + leafLookup(leaf: T): number; + getProof(leaf: number | T): HexString[]; + getMultiProof(leaves: (number | T)[]): MultiProof; + verify(leaf: number | T, proof: HexString[]): boolean; + verifyMultiProof(multiproof: MultiProof): boolean; +} \ No newline at end of file diff --git a/src/options.ts b/src/options.ts index 31f3d24..da40f98 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,17 +1,11 @@ -// SimpleMerkleTree building options -export type SimpleMerkleTreeOptions = Partial<{ +// MerkleTree building options +export type MerkleTreeOptions = Partial<{ /** Enable or disable sorted leaves. Sorting is strongly recommended for multiproofs. */ sortLeaves: boolean; }>; -// StandardMerkleTree building options -export type StandardMerkleTreeOptions = SimpleMerkleTreeOptions & { - /** ABI Encoding for leaf values. */ - leafEncoding: string[]; -}; - -// Recommended (default) SimpleMerkleTree options. +// Recommended (default) MerkleTree options. // - leaves are sorted by default to facilitate onchain verification of multiproofs. -export const defaultOptions: Required = { +export const defaultOptions: Required = { sortLeaves: true, }; diff --git a/src/simple.test.ts b/src/simple.test.ts index 134d811..b426c49 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -6,10 +6,8 @@ import { SimpleMerkleTree } from "./simple"; describe("simple merkle tree", () => { for (const opts of [{}, { sortLeaves: true }, { sortLeaves: false }]) { describe(`with options '${JSON.stringify(opts)}'`, () => { - const leaves = "abcdef".split("").map((c) => [keccak256(Buffer.from(c))]); - const otherLeaves = "abc" - .split("") - .map((c) => [keccak256(Buffer.from(c))]); + const leaves = "abcdef".split("").map((c) => keccak256(Buffer.from(c))); + const otherLeaves = "abc".split("").map((c) => keccak256(Buffer.from(c))); const tree = SimpleMerkleTree.of(leaves, opts); const otherTree = SimpleMerkleTree.of(otherLeaves, opts); @@ -115,7 +113,7 @@ describe("simple merkle tree", () => { it("reject invalid leaf size", () => { const invalidLeaf = [zero + "00"]; // 33 bytes (all zero) assert.throws( - () => SimpleMerkleTree.of([invalidLeaf], opts), + () => SimpleMerkleTree.of(invalidLeaf, opts), `Error: ${invalidLeaf} is not a valid 32 bytes object (pos: 0)` ); }); @@ -141,9 +139,7 @@ describe("simple merkle tree", () => { tree: [zero], values: [ { - value: [ - "0x0000000000000000000000000000000000000000000000000000000000000001", - ], + value: "0x0000000000000000000000000000000000000000000000000000000000000001", treeIndex: 0, }, ], @@ -156,7 +152,7 @@ describe("simple merkle tree", () => { const loadedTree2 = SimpleMerkleTree.load({ format: "simple-v1", tree: [zero, zero, zero], - values: [{ value: [zero], treeIndex: 2 }], + values: [{ value: zero, treeIndex: 2 }], }); assert.throws( () => loadedTree2.getProof(0), diff --git a/src/simple.ts b/src/simple.ts index 2112f69..97348f3 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -1,5 +1,5 @@ import { defaultAbiCoder } from "@ethersproject/abi"; -import { BytesLike, HexString, isBytesLike, toHex, compare } from "./bytes"; +import { BytesLike, HexString, toHex, compare } from "./bytes"; import { MultiProof, @@ -12,64 +12,39 @@ import { renderMerkleTree, } from "./core"; -import { SimpleMerkleTreeOptions, defaultOptions } from "./options"; +import { MerkleTree, MerkleTreeData } from "./interface"; +import { MerkleTreeOptions, defaultOptions } from "./options"; import { checkBounds } from "./utils/check-bounds"; import { throwError } from "./utils/throw-error"; -export type SimpleMerkleTreeValue = { - value: BytesLike[]; - treeIndex: number; -}; - -export type SimpleMerkleTreeData = { - format: string; - tree: HexString[]; - values: SimpleMerkleTreeValue[]; -}; - -/// @notice Not an actual hash function, just a placeholder -export function simpleLeafHash(value: BytesLike[]): HexString { - return defaultAbiCoder.encode(["bytes32"], value); +export function formatLeaf(value: BytesLike): HexString { + return defaultAbiCoder.encode(["bytes32"], [ value ]); } -export interface MerkleTree { - root: HexString; - dump(): SimpleMerkleTreeData; - render(): string; - entries(): Iterable<[number, T]>; - validate(): void; - leafLookup(leaf: T): number; - getProof(leaf: number | T): HexString[]; - getMultiProof(leaves: (number | T)[]): MultiProof; - verify(leaf: number | T, proof: HexString[]): boolean; - verifyMultiProof(multiproof: MultiProof): boolean; -} - -export class SimpleMerkleTree implements MerkleTree { +export class SimpleMerkleTree implements MerkleTree { private readonly hashLookup: { [hash: HexString]: number }; protected constructor( protected readonly tree: HexString[], - protected readonly values: SimpleMerkleTreeValue[], - protected readonly options: SimpleMerkleTreeOptions = {} + protected readonly values: MerkleTreeData['values'], + protected readonly leafHasher: (leaf: T) => HexString, ) { - this.options = Object.assign(options, defaultOptions); this.hashLookup = Object.fromEntries( - values.map(({ value }, valueIndex) => [this.leafHash(value), valueIndex]) + values.map(({ treeIndex }, valueIndex) => [tree.at(treeIndex), valueIndex]) ); } - protected static parameters( - values: BytesLike[][], - options: SimpleMerkleTreeOptions = {}, - leafHash: (value: BytesLike[]) => HexString = simpleLeafHash - ): [HexString[], indexedValues: SimpleMerkleTreeValue[]] { + protected static prepare( + values: T[], + options: MerkleTreeOptions = {}, + leafHasher: (value: T) => HexString, + ): [tree: HexString[], indexedValues: MerkleTreeData['values']] { const { sortLeaves } = { ...defaultOptions, ...options }; const hashedValues = values.map((value, valueIndex) => ({ value, valueIndex, - hash: leafHash(value), + hash: leafHasher(value), })); if (sortLeaves) { @@ -89,56 +64,41 @@ export class SimpleMerkleTree implements MerkleTree { return [tree, indexedValues]; } - static of( - values: BytesLike[][], - options: O - ): SimpleMerkleTree { - return new this(...this.parameters(values, options)); + static of( + values: T[], + options: MerkleTreeOptions = {} + ): SimpleMerkleTree { + const [ tree, indexedValues ] = SimpleMerkleTree.prepare(values, options, formatLeaf); + return new SimpleMerkleTree(tree, indexedValues, formatLeaf); } - static load(data: D): SimpleMerkleTree { + static load(data: MerkleTreeData): SimpleMerkleTree { if (data.format !== "simple-v1") { throwError(`Unknown format '${data.format}'`); } - - return new this( - data.tree, - data.values.map(({ value, treeIndex }) => ({ - value: value.map((v) => toHex(v)), - treeIndex, - })) - ); + return new this(data.tree, data.values, formatLeaf); } - static verify( + static verify( root: BytesLike, leaf: T, proof: BytesLike[], - _?: SimpleMerkleTreeOptions ): boolean { - return toHex(root) === processProof(simpleLeafHash(leaf), proof); + return toHex(root) === processProof(formatLeaf(leaf), proof); } - static verifyMultiProof( + static verifyMultiProof( root: BytesLike, - multiproof: MultiProof, - _?: SimpleMerkleTreeOptions + multiproof: MultiProof, ): boolean { - return ( - toHex(root) === - processMultiProof({ - leaves: multiproof.leaves, - proof: multiproof.proof, - proofFlags: multiproof.proofFlags, - }) - ); + return toHex(root) === processMultiProof(multiproof); } get root(): HexString { return this.tree[0]!; } - dump(): SimpleMerkleTreeData { + dump(): MerkleTreeData { return { format: "simple-v1", tree: this.tree, @@ -150,7 +110,7 @@ export class SimpleMerkleTree implements MerkleTree { return renderMerkleTree(this.tree); } - *entries(): Iterable<[number, BytesLike[]]> { + *entries(): Iterable<[number, T]> { for (const [i, { value }] of this.values.entries()) { yield [i, value]; } @@ -165,14 +125,14 @@ export class SimpleMerkleTree implements MerkleTree { } } - leafLookup(leaf: BytesLike[]): number { + leafLookup(leaf: T): number { return ( this.hashLookup[toHex(this.leafHash(leaf))] ?? throwError("Leaf is not in tree") ); } - getProof(leaf: number | BytesLike[]): HexString[] { + getProof(leaf: number | T): HexString[] { // input validity const valueIndex = typeof leaf === "number" ? leaf : this.leafLookup(leaf); this.validateValue(valueIndex); @@ -190,7 +150,7 @@ export class SimpleMerkleTree implements MerkleTree { return proof; } - getMultiProof(leaves: (number | BytesLike[])[]): MultiProof { + getMultiProof(leaves: (number | T)[]): MultiProof { // input validity const valueIndices = leaves.map((leaf) => typeof leaf === "number" ? leaf : this.leafLookup(leaf) @@ -208,19 +168,19 @@ export class SimpleMerkleTree implements MerkleTree { // return multiproof in hex format return { - leaves: proof.leaves, + leaves: proof.leaves.map(hash => this.values[this.hashLookup[hash]!]!.value), proof: proof.proof, proofFlags: proof.proofFlags, }; } - verify(leaf: number | BytesLike[], proof: HexString[]): boolean { + verify(leaf: number | T, proof: HexString[]): boolean { return this._verify(this.leafHash(leaf), proof); } - verifyMultiProof(multiproof: MultiProof): boolean { + verifyMultiProof(multiproof: MultiProof): boolean { return this._verifyMultiProof({ - leaves: multiproof.leaves, + leaves: multiproof.leaves.map(l => this.leafHash(l)), proof: multiproof.proof, proofFlags: multiproof.proofFlags, }); @@ -237,19 +197,19 @@ export class SimpleMerkleTree implements MerkleTree { return hashedLeaf; } - protected leafHash(leaf: number | BytesLike[]): HexString { - if (Array.isArray(leaf)) { - return simpleLeafHash(leaf); - } else { + protected leafHash(leaf: number | T): HexString { + if (typeof leaf === 'number') { return this.validateValue(leaf); + } else { + return this.leafHasher(leaf); } } - private _verify(leafHash: HexString, proof: HexString[]): boolean { + private _verify(leafHash: BytesLike, proof: BytesLike[]): boolean { return this.root === processProof(leafHash, proof); } - private _verifyMultiProof(multiproof: MultiProof): boolean { + private _verifyMultiProof(multiproof: MultiProof): boolean { return this.root === processMultiProof(multiproof); } } diff --git a/src/standard.test.ts b/src/standard.test.ts index a2a7d3d..b0bee88 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -1,21 +1,27 @@ import assert from "assert/strict"; import { HashZero as zero } from "@ethersproject/constants"; import { keccak256 } from "@ethersproject/keccak256"; -import { StandardMerkleTree } from "./standard"; -import { StandardMerkleTreeOptions } from "./options"; +import { StandardMerkleTree, StandardMerkleTreeData } from "./standard"; describe("standard merkle tree", () => { + it('Supports complex leaf types', () => { + const leaves = [ + [0, []], + [1, ['openzeppelin']], + [2, ['hello', 'world']], + [3, ['merkle', 'tree']], + ]; + const types = [ 'uint256', 'string[]' ]; + StandardMerkleTree.of(leaves, types); + }); + for (const opts of [{}, { sortLeaves: true }, { sortLeaves: false }]) { describe(`with options '${JSON.stringify(opts)}'`, () => { const leaves = "abcdef".split("").map((c) => [c]); const otherLeaves = "abc".split("").map((c) => [c]); - const tree = StandardMerkleTree.of( - leaves, - Object.assign(opts, { leafEncoding: ["string"] }) - ); - - const otherTree = StandardMerkleTree.of(otherLeaves, ["string"]); + const tree = StandardMerkleTree.of(leaves, ["string"], opts); + const otherTree = StandardMerkleTree.of(otherLeaves, ["string"], opts); it("rejects loading a tree without leaf encoding", () => { assert.throws( @@ -24,7 +30,7 @@ describe("standard merkle tree", () => { format: "standard-v1", tree: [zero], values: [{ value: ["0"], treeIndex: 0 }], - }), + } as StandardMerkleTreeData<[string]>), /^Error: Expected leaf encoding$/ ); }); @@ -42,14 +48,7 @@ describe("standard merkle tree", () => { assert(tree.verify(id, proof1)); assert(tree.verify(leaf, proof1)); - assert( - StandardMerkleTree.verify(tree.root, ["string"], leaf, proof1) - ); - assert( - StandardMerkleTree.verify(tree.root, leaf, proof1, { - leafEncoding: ["string"], - }) - ); + assert(StandardMerkleTree.verify(tree.root, ["string"], leaf, proof1)); } }); @@ -58,14 +57,7 @@ describe("standard merkle tree", () => { const invalidProof = otherTree.getProof(leaf); assert(!tree.verify(leaf, invalidProof)); - assert( - !StandardMerkleTree.verify(tree.root, ["string"], leaf, invalidProof) - ); - assert( - !StandardMerkleTree.verify(tree.root, leaf, invalidProof, { - leafEncoding: ["string"], - }) - ); + assert(!StandardMerkleTree.verify(tree.root, ["string"], leaf, invalidProof)); }); it("generates valid multiproofs", () => { @@ -150,7 +142,12 @@ describe("standard merkle tree", () => { const recoveredTree = StandardMerkleTree.load(tree.dump()); recoveredTree.validate(); - assert.deepEqual(tree, recoveredTree); + + // assert.deepEqual(tree, recoveredTree); + for (const key of Object.keys(tree)) { + if (key === 'leafHasher') continue; // This is a function tat is not reference-equal + assert.deepEqual((tree as any)[key], (recoveredTree as any)[key]); + } }); it("reject out of bounds value index", () => { diff --git a/src/standard.ts b/src/standard.ts index 4bfcf05..152cd7f 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -1,196 +1,78 @@ -import { BytesLike, HexString, toHex, isBytes, isBytesLike } from "./bytes"; - -import { MultiProof, processProof, processMultiProof } from "./core"; - -import { - SimpleMerkleTree, - SimpleMerkleTreeData, - SimpleMerkleTreeValue, -} from "./simple"; - -import { defaultAbiCoder } from "@ethersproject/abi"; import { keccak256 } from "@ethersproject/keccak256"; +import { defaultAbiCoder } from "@ethersproject/abi"; +import { BytesLike, HexString, toHex, isBytesLike } from "./bytes"; +import { MultiProof, processProof, processMultiProof } from "./core"; -import { StandardMerkleTreeOptions } from "./options"; +import { SimpleMerkleTree } from "./simple"; +import { MerkleTreeData } from "./interface"; +import { MerkleTreeOptions } from "./options"; import { throwError } from "./utils/throw-error"; -export type StandardMerkleTreeValue = SimpleMerkleTreeValue; - -export type StandardMerkleTreeData = SimpleMerkleTreeData & { - leafEncoding?: string[]; -}; +export type StandardMerkleTreeData = MerkleTreeData & { leafEncoding: string[]; }; -export function standardLeafHash( - value: T, - types: string[] -): HexString { - return keccak256(keccak256(defaultAbiCoder.encode(types, value))); +export function standardLeafHasher(types: string[]) { + return (value: T) => keccak256(keccak256(defaultAbiCoder.encode(types, value))); } -export class StandardMerkleTree extends SimpleMerkleTree { +export class StandardMerkleTree extends SimpleMerkleTree { protected constructor( protected readonly tree: HexString[], - protected readonly values: SimpleMerkleTreeValue[], - protected readonly options: StandardMerkleTreeOptions + protected readonly values: StandardMerkleTreeData['values'], + protected readonly leafEncoding: string[], ) { - super(tree, values, options); + super(tree, values, standardLeafHasher(leafEncoding)); } - protected static parameters( - values: BytesLike[][], - options: StandardMerkleTreeOptions - ): [HexString[], indexedValues: SimpleMerkleTreeValue[]] { - return super.parameters(values, options, (value) => - standardLeafHash(value, options.leafEncoding) + static of( + values: T[], + leafEncoding: string[], + options: MerkleTreeOptions = {} + ): StandardMerkleTree { + const [ tree, indexedValues ] = SimpleMerkleTree.prepare( + values, + options, + standardLeafHasher(leafEncoding), ); + return new StandardMerkleTree(tree, indexedValues, leafEncoding); } - static of( - values: BytesLike[][], - options: StandardMerkleTreeOptions - ): StandardMerkleTree; - static of( - values: BytesLike[][], - leafEncoding: StandardMerkleTreeOptions["leafEncoding"] - ): StandardMerkleTree; - static of( - values: BytesLike[][], - optionsOrLeafEncoding: - | StandardMerkleTreeOptions - | StandardMerkleTreeOptions["leafEncoding"] - ): StandardMerkleTree { - if (Array.isArray(optionsOrLeafEncoding)) { - optionsOrLeafEncoding = { leafEncoding: optionsOrLeafEncoding }; + static load(data: StandardMerkleTreeData): StandardMerkleTree { + if (data.format !== "standard-v1") { + throw new Error(`Unknown format '${data.format}'`); } - - const options = Object.assign({}, optionsOrLeafEncoding); - - return new StandardMerkleTree(...this.parameters(values, options), options); - } - - static load(data: D): StandardMerkleTree { if (data.leafEncoding === undefined) { throwError("Expected leaf encoding"); } - - if (data.format !== "standard-v1") { - throw new Error(`Unknown format '${data.format}'`); - } - - const leafEncoding = data.leafEncoding; - - return new StandardMerkleTree(data.tree, data.values, { leafEncoding }); + return new StandardMerkleTree(data.tree, data.values, data.leafEncoding); } - static verify( + static verify( root: BytesLike, - leaf: BytesLike[], + leafEncoding: string[], + leaf: T, proof: BytesLike[], - options: StandardMerkleTreeOptions - ): boolean; - static verify( - root: BytesLike, - leafEncoding: StandardMerkleTreeOptions["leafEncoding"], - leaf: BytesLike[], - proof: BytesLike[] - ): boolean; - static verify( - root: BytesLike, - leafOrLeafEncoding: BytesLike[] | StandardMerkleTreeOptions["leafEncoding"], - proofOrLeaf: BytesLike[] | BytesLike[], - optionsOrProof: StandardMerkleTreeOptions | BytesLike[] ): boolean { - let leaf: BytesLike[]; - let proof: BytesLike[]; - let options: StandardMerkleTreeOptions; - - if ("leafEncoding" in optionsOrProof) { - leaf = leafOrLeafEncoding; - proof = proofOrLeaf; - options = optionsOrProof; - } else if (leafOrLeafEncoding.some((v) => !isBytesLike(v))) { - leaf = proofOrLeaf; - proof = optionsOrProof; - - options = { - leafEncoding: - leafOrLeafEncoding as StandardMerkleTreeOptions["leafEncoding"], - }; - } else { - throwError("Invalid arguments"); - } - - return ( - toHex(root) === - processProof(standardLeafHash(leaf, options.leafEncoding), proof) - ); + return toHex(root) === processProof(standardLeafHasher(leafEncoding)(leaf), proof); } - static verifyMultiProof( + static verifyMultiProof( root: BytesLike, leafEncoding: string[], - multiproof: MultiProof - ): boolean; - static verifyMultiProof( - root: BytesLike, - multiproof: MultiProof, - options?: StandardMerkleTreeOptions - ): boolean; - static verifyMultiProof( - root: BytesLike, - leafEncodingOrMultiproof: string[] | MultiProof, - multiProofOrOptions?: MultiProof | StandardMerkleTreeOptions + multiproof: MultiProof ): boolean { - let leafEncoding: string[]; - let multiproof: MultiProof; - let options: StandardMerkleTreeOptions; - - if ( - "proof" in leafEncodingOrMultiproof && - Array.isArray(multiProofOrOptions) - ) { - leafEncoding = multiProofOrOptions; - multiproof = leafEncodingOrMultiproof; - options = { leafEncoding }; - } else if (Array.isArray(leafEncodingOrMultiproof)) { - if ( - multiProofOrOptions === undefined || - "leafEncoding" in multiProofOrOptions - ) { - throwError("Invalid arguments"); - } - - leafEncoding = leafEncodingOrMultiproof; - multiproof = multiProofOrOptions; - options = { leafEncoding }; - } else { - throwError("Invalid arguments"); - } - - return ( - toHex(root) === - processMultiProof({ - leaves: multiproof.leaves, - proof: multiproof.proof, - proofFlags: multiproof.proofFlags, - }) - ); + return toHex(root) === processMultiProof({ + leaves: multiproof.leaves.map(leaf => standardLeafHasher(leafEncoding)(leaf)), + proof: multiproof.proof, + proofFlags: multiproof.proofFlags, + }); } - dump(): StandardMerkleTreeData { + dump(): StandardMerkleTreeData { return { format: "standard-v1", tree: this.tree, values: this.values, - leafEncoding: this.options.leafEncoding, + leafEncoding: this.leafEncoding, }; } - - protected leafHash(leaf: number | BytesLike[]): HexString { - if (Array.isArray(leaf)) { - return standardLeafHash(leaf, this.options.leafEncoding); - } else { - return this.validateValue(leaf); - } - } } From 30d150c82fb37e22946fef281332c06cc509494e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 23 Feb 2024 14:45:07 +0100 Subject: [PATCH 03/36] Use generic MerkleTreeImpl --- src/interface.ts | 21 ----- src/merkletree.ts | 198 ++++++++++++++++++++++++++++++++++++++++++++ src/simple.ts | 203 ++++------------------------------------------ src/standard.ts | 10 +-- 4 files changed, 218 insertions(+), 214 deletions(-) delete mode 100644 src/interface.ts create mode 100644 src/merkletree.ts diff --git a/src/interface.ts b/src/interface.ts deleted file mode 100644 index 129986f..0000000 --- a/src/interface.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { HexString } from "./bytes"; -import { MultiProof } from "./core"; - -export type MerkleTreeData = { - format: string; - tree: HexString[]; - values: { value: T; treeIndex: number; }[]; -}; - -export interface MerkleTree { - root: HexString; - dump(): MerkleTreeData; - render(): string; - entries(): Iterable<[number, T]>; - validate(): void; - leafLookup(leaf: T): number; - getProof(leaf: number | T): HexString[]; - getMultiProof(leaves: (number | T)[]): MultiProof; - verify(leaf: number | T, proof: HexString[]): boolean; - verifyMultiProof(multiproof: MultiProof): boolean; -} \ No newline at end of file diff --git a/src/merkletree.ts b/src/merkletree.ts new file mode 100644 index 0000000..6b3b77c --- /dev/null +++ b/src/merkletree.ts @@ -0,0 +1,198 @@ +import { BytesLike, HexString, toHex, compare } from "./bytes"; + +import { + MultiProof, + makeMerkleTree, + isValidMerkleTree, + getProof, + getMultiProof, + processProof, + processMultiProof, + renderMerkleTree, +} from "./core"; + +import { MerkleTreeOptions, defaultOptions } from "./options"; +import { checkBounds } from "./utils/check-bounds"; +import { throwError } from "./utils/throw-error"; + +export type MerkleTreeData = { + format: string; + tree: HexString[]; + values: { value: T; treeIndex: number; }[]; +}; + +export interface MerkleTree { + root: HexString; + dump(): MerkleTreeData; + render(): string; + entries(): Iterable<[number, T]>; + validate(): void; + leafLookup(leaf: T): number; + getProof(leaf: number | T): HexString[]; + getMultiProof(leaves: (number | T)[]): MultiProof; + verify(leaf: number | T, proof: HexString[]): boolean; + verifyMultiProof(multiproof: MultiProof): boolean; +} + +export class MerkleTreeImpl implements MerkleTree { + private readonly hashLookup: { [hash: HexString]: number }; + + protected constructor( + protected readonly tree: HexString[], + protected readonly values: MerkleTreeData['values'], + protected readonly leafHasher: (leaf: T) => HexString, + ) { + this.hashLookup = Object.fromEntries( + values.map(({ treeIndex }, valueIndex) => [tree.at(treeIndex), valueIndex]) + ); + } + + protected static prepare( + values: T[], + options: MerkleTreeOptions = {}, + leafHasher: (value: T) => HexString, + ): [tree: HexString[], indexedValues: MerkleTreeData['values']] { + const { sortLeaves } = { ...defaultOptions, ...options }; + + const hashedValues = values.map((value, valueIndex) => ({ + value, + valueIndex, + hash: leafHasher(value), + })); + + if (sortLeaves) { + hashedValues.sort((a, b) => compare(a.hash, b.hash)); + } + + const tree = makeMerkleTree(hashedValues.map((v) => v.hash)); + + const indexedValues = values.map((value) => ({ + value, + treeIndex: 0, + })); + for (const [leafIndex, { valueIndex }] of hashedValues.entries()) { + indexedValues[valueIndex]!.treeIndex = tree.length - leafIndex - 1; + } + + return [tree, indexedValues]; + } + + get root(): HexString { + return this.tree[0]!; + } + + dump(): MerkleTreeData { + return { + format: "simple-v1", + tree: this.tree, + values: this.values, + }; + } + + render() { + return renderMerkleTree(this.tree); + } + + *entries(): Iterable<[number, T]> { + for (const [i, { value }] of this.values.entries()) { + yield [i, value]; + } + } + + validate() { + for (let i = 0; i < this.values.length; i++) { + this.validateValue(i); + } + if (!isValidMerkleTree(this.tree)) { + throwError("Merkle tree is invalid"); + } + } + + leafLookup(leaf: T): number { + return ( + this.hashLookup[toHex(this.leafHash(leaf))] ?? + throwError("Leaf is not in tree") + ); + } + + getProof(leaf: number | T): HexString[] { + // input validity + const valueIndex = typeof leaf === "number" ? leaf : this.leafLookup(leaf); + this.validateValue(valueIndex); + + // rebuild tree index and generate proof + const { treeIndex } = this.values[valueIndex]!; + const proof = getProof(this.tree, treeIndex); + + // sanity check proof + if (!this._verify(this.tree[treeIndex]!, proof)) { + throwError("Unable to prove value"); + } + + // return proof in hex format + return proof; + } + + getMultiProof(leaves: (number | T)[]): MultiProof { + // input validity + const valueIndices = leaves.map((leaf) => + typeof leaf === "number" ? leaf : this.leafLookup(leaf) + ); + for (const valueIndex of valueIndices) this.validateValue(valueIndex); + + // rebuild tree indices and generate proof + const indices = valueIndices.map((i) => this.values[i]!.treeIndex); + const proof = getMultiProof(this.tree, indices); + + // sanity check proof + if (!this._verifyMultiProof(proof)) { + throwError("Unable to prove values"); + } + + // return multiproof in hex format + return { + leaves: proof.leaves.map(hash => this.values[this.hashLookup[hash]!]!.value), + proof: proof.proof, + proofFlags: proof.proofFlags, + }; + } + + verify(leaf: number | T, proof: HexString[]): boolean { + return this._verify(this.leafHash(leaf), proof); + } + + verifyMultiProof(multiproof: MultiProof): boolean { + return this._verifyMultiProof({ + leaves: multiproof.leaves.map(l => this.leafHash(l)), + proof: multiproof.proof, + proofFlags: multiproof.proofFlags, + }); + } + + protected validateValue(valueIndex: number): HexString { + checkBounds(this.values, valueIndex); + const { value: leaf, treeIndex } = this.values[valueIndex]!; + checkBounds(this.tree, treeIndex); + const hashedLeaf = this.leafHash(leaf); + if (hashedLeaf !== this.tree[treeIndex]!) { + throwError("Merkle tree does not contain the expected value"); + } + return hashedLeaf; + } + + protected leafHash(leaf: number | T): HexString { + if (typeof leaf === 'number') { + return this.validateValue(leaf); + } else { + return this.leafHasher(leaf); + } + } + + private _verify(leafHash: BytesLike, proof: BytesLike[]): boolean { + return this.root === processProof(leafHash, proof); + } + + private _verifyMultiProof(multiproof: MultiProof): boolean { + return this.root === processMultiProof(multiproof); + } +} diff --git a/src/simple.ts b/src/simple.ts index 97348f3..8e3e993 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -1,215 +1,44 @@ import { defaultAbiCoder } from "@ethersproject/abi"; -import { BytesLike, HexString, toHex, compare } from "./bytes"; - -import { - MultiProof, - makeMerkleTree, - isValidMerkleTree, - getProof, - getMultiProof, - processProof, - processMultiProof, - renderMerkleTree, -} from "./core"; - -import { MerkleTree, MerkleTreeData } from "./interface"; -import { MerkleTreeOptions, defaultOptions } from "./options"; -import { checkBounds } from "./utils/check-bounds"; +import { BytesLike, HexString, toHex } from "./bytes"; +import { MultiProof, processProof, processMultiProof } from "./core"; +import { MerkleTreeData, MerkleTreeImpl } from "./merkletree"; +import { MerkleTreeOptions } from "./options"; import { throwError } from "./utils/throw-error"; export function formatLeaf(value: BytesLike): HexString { return defaultAbiCoder.encode(["bytes32"], [ value ]); } -export class SimpleMerkleTree implements MerkleTree { - private readonly hashLookup: { [hash: HexString]: number }; - - protected constructor( - protected readonly tree: HexString[], - protected readonly values: MerkleTreeData['values'], - protected readonly leafHasher: (leaf: T) => HexString, - ) { - this.hashLookup = Object.fromEntries( - values.map(({ treeIndex }, valueIndex) => [tree.at(treeIndex), valueIndex]) - ); - } - - protected static prepare( - values: T[], +export class SimpleMerkleTree extends MerkleTreeImpl { + static of = ( + values: BytesLike[], options: MerkleTreeOptions = {}, - leafHasher: (value: T) => HexString, - ): [tree: HexString[], indexedValues: MerkleTreeData['values']] { - const { sortLeaves } = { ...defaultOptions, ...options }; - - const hashedValues = values.map((value, valueIndex) => ({ - value, - valueIndex, - hash: leafHasher(value), - })); - - if (sortLeaves) { - hashedValues.sort((a, b) => compare(a.hash, b.hash)); - } - - const tree = makeMerkleTree(hashedValues.map((v) => v.hash)); - - const indexedValues = values.map((value) => ({ - value, - treeIndex: 0, - })); - for (const [leafIndex, { valueIndex }] of hashedValues.entries()) { - indexedValues[valueIndex]!.treeIndex = tree.length - leafIndex - 1; - } - - return [tree, indexedValues]; - } - - static of( - values: T[], - options: MerkleTreeOptions = {} - ): SimpleMerkleTree { + ): SimpleMerkleTree => { const [ tree, indexedValues ] = SimpleMerkleTree.prepare(values, options, formatLeaf); return new SimpleMerkleTree(tree, indexedValues, formatLeaf); } - static load(data: MerkleTreeData): SimpleMerkleTree { + static load( + data: MerkleTreeData, + ): SimpleMerkleTree { if (data.format !== "simple-v1") { throwError(`Unknown format '${data.format}'`); } return new this(data.tree, data.values, formatLeaf); } - static verify( + static verify( root: BytesLike, - leaf: T, + leaf: BytesLike, proof: BytesLike[], ): boolean { return toHex(root) === processProof(formatLeaf(leaf), proof); } - static verifyMultiProof( + static verifyMultiProof( root: BytesLike, - multiproof: MultiProof, + multiproof: MultiProof, ): boolean { return toHex(root) === processMultiProof(multiproof); } - - get root(): HexString { - return this.tree[0]!; - } - - dump(): MerkleTreeData { - return { - format: "simple-v1", - tree: this.tree, - values: this.values, - }; - } - - render() { - return renderMerkleTree(this.tree); - } - - *entries(): Iterable<[number, T]> { - for (const [i, { value }] of this.values.entries()) { - yield [i, value]; - } - } - - validate() { - for (let i = 0; i < this.values.length; i++) { - this.validateValue(i); - } - if (!isValidMerkleTree(this.tree)) { - throwError("Merkle tree is invalid"); - } - } - - leafLookup(leaf: T): number { - return ( - this.hashLookup[toHex(this.leafHash(leaf))] ?? - throwError("Leaf is not in tree") - ); - } - - getProof(leaf: number | T): HexString[] { - // input validity - const valueIndex = typeof leaf === "number" ? leaf : this.leafLookup(leaf); - this.validateValue(valueIndex); - - // rebuild tree index and generate proof - const { treeIndex } = this.values[valueIndex]!; - const proof = getProof(this.tree, treeIndex); - - // sanity check proof - if (!this._verify(this.tree[treeIndex]!, proof)) { - throwError("Unable to prove value"); - } - - // return proof in hex format - return proof; - } - - getMultiProof(leaves: (number | T)[]): MultiProof { - // input validity - const valueIndices = leaves.map((leaf) => - typeof leaf === "number" ? leaf : this.leafLookup(leaf) - ); - for (const valueIndex of valueIndices) this.validateValue(valueIndex); - - // rebuild tree indices and generate proof - const indices = valueIndices.map((i) => this.values[i]!.treeIndex); - const proof = getMultiProof(this.tree, indices); - - // sanity check proof - if (!this._verifyMultiProof(proof)) { - throwError("Unable to prove values"); - } - - // return multiproof in hex format - return { - leaves: proof.leaves.map(hash => this.values[this.hashLookup[hash]!]!.value), - proof: proof.proof, - proofFlags: proof.proofFlags, - }; - } - - verify(leaf: number | T, proof: HexString[]): boolean { - return this._verify(this.leafHash(leaf), proof); - } - - verifyMultiProof(multiproof: MultiProof): boolean { - return this._verifyMultiProof({ - leaves: multiproof.leaves.map(l => this.leafHash(l)), - proof: multiproof.proof, - proofFlags: multiproof.proofFlags, - }); - } - - protected validateValue(valueIndex: number): HexString { - checkBounds(this.values, valueIndex); - const { value: leaf, treeIndex } = this.values[valueIndex]!; - checkBounds(this.tree, treeIndex); - const hashedLeaf = this.leafHash(leaf); - if (hashedLeaf !== this.tree[treeIndex]!) { - throwError("Merkle tree does not contain the expected value"); - } - return hashedLeaf; - } - - protected leafHash(leaf: number | T): HexString { - if (typeof leaf === 'number') { - return this.validateValue(leaf); - } else { - return this.leafHasher(leaf); - } - } - - private _verify(leafHash: BytesLike, proof: BytesLike[]): boolean { - return this.root === processProof(leafHash, proof); - } - - private _verifyMultiProof(multiproof: MultiProof): boolean { - return this.root === processMultiProof(multiproof); - } -} +} \ No newline at end of file diff --git a/src/standard.ts b/src/standard.ts index 152cd7f..2dbf68b 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -1,10 +1,8 @@ import { keccak256 } from "@ethersproject/keccak256"; import { defaultAbiCoder } from "@ethersproject/abi"; -import { BytesLike, HexString, toHex, isBytesLike } from "./bytes"; +import { BytesLike, HexString, toHex } from "./bytes"; import { MultiProof, processProof, processMultiProof } from "./core"; - -import { SimpleMerkleTree } from "./simple"; -import { MerkleTreeData } from "./interface"; +import { MerkleTreeData, MerkleTreeImpl } from "./merkletree"; import { MerkleTreeOptions } from "./options"; import { throwError } from "./utils/throw-error"; @@ -14,7 +12,7 @@ export function standardLeafHasher(types: s return (value: T) => keccak256(keccak256(defaultAbiCoder.encode(types, value))); } -export class StandardMerkleTree extends SimpleMerkleTree { +export class StandardMerkleTree extends MerkleTreeImpl { protected constructor( protected readonly tree: HexString[], protected readonly values: StandardMerkleTreeData['values'], @@ -28,7 +26,7 @@ export class StandardMerkleTree extends SimpleMerkleTree { leafEncoding: string[], options: MerkleTreeOptions = {} ): StandardMerkleTree { - const [ tree, indexedValues ] = SimpleMerkleTree.prepare( + const [ tree, indexedValues ] = MerkleTreeImpl.prepare( values, options, standardLeafHasher(leafEncoding), From 21399c18b119a92856efc95cc0028ef3b1828b18 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 23 Feb 2024 15:06:41 +0100 Subject: [PATCH 04/36] MerkleTree interface --- src/merkletree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 6b3b77c..d42ee7b 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -31,7 +31,7 @@ export interface MerkleTree { getProof(leaf: number | T): HexString[]; getMultiProof(leaves: (number | T)[]): MultiProof; verify(leaf: number | T, proof: HexString[]): boolean; - verifyMultiProof(multiproof: MultiProof): boolean; + verifyMultiProof(multiproof: MultiProof): boolean; } export class MerkleTreeImpl implements MerkleTree { From eabc28ee0998d267277eb758197328502586fbe2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 23 Feb 2024 20:37:44 +0100 Subject: [PATCH 05/36] explicit type --- src/simple.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/simple.ts b/src/simple.ts index 8e3e993..91571b4 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -14,7 +14,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { values: BytesLike[], options: MerkleTreeOptions = {}, ): SimpleMerkleTree => { - const [ tree, indexedValues ] = SimpleMerkleTree.prepare(values, options, formatLeaf); + const [ tree, indexedValues ] = MerkleTreeImpl.prepare(values, options, formatLeaf); return new SimpleMerkleTree(tree, indexedValues, formatLeaf); } @@ -24,7 +24,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { if (data.format !== "simple-v1") { throwError(`Unknown format '${data.format}'`); } - return new this(data.tree, data.values, formatLeaf); + return new SimpleMerkleTree(data.tree, data.values, formatLeaf); } static verify( From 6eaf64697c4a982d50877ca08c476c6e75932ef4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 23 Feb 2024 20:48:36 +0100 Subject: [PATCH 06/36] use bind instead of curation --- src/standard.test.ts | 6 +++--- src/standard.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/standard.test.ts b/src/standard.test.ts index b0bee88..aede028 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -144,9 +144,9 @@ describe("standard merkle tree", () => { recoveredTree.validate(); // assert.deepEqual(tree, recoveredTree); - for (const key of Object.keys(tree)) { - if (key === 'leafHasher') continue; // This is a function tat is not reference-equal - assert.deepEqual((tree as any)[key], (recoveredTree as any)[key]); + for (const [key, value] of Object.entries(tree)) { + if (typeof value === 'function') continue; // LeafHasher is a function that is not reference-equal + assert.deepEqual(value, (recoveredTree as any)[key]); } }); diff --git a/src/standard.ts b/src/standard.ts index 2dbf68b..e23351e 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -8,8 +8,8 @@ import { throwError } from "./utils/throw-error"; export type StandardMerkleTreeData = MerkleTreeData & { leafEncoding: string[]; }; -export function standardLeafHasher(types: string[]) { - return (value: T) => keccak256(keccak256(defaultAbiCoder.encode(types, value))); +export function standardLeafHasher(types: string[], value: T): HexString { + return keccak256(keccak256(defaultAbiCoder.encode(types, value))); } export class StandardMerkleTree extends MerkleTreeImpl { @@ -18,7 +18,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { protected readonly values: StandardMerkleTreeData['values'], protected readonly leafEncoding: string[], ) { - super(tree, values, standardLeafHasher(leafEncoding)); + super(tree, values, standardLeafHasher.bind(null, leafEncoding)); } static of( @@ -29,7 +29,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { const [ tree, indexedValues ] = MerkleTreeImpl.prepare( values, options, - standardLeafHasher(leafEncoding), + standardLeafHasher.bind(null, leafEncoding), ); return new StandardMerkleTree(tree, indexedValues, leafEncoding); } @@ -50,7 +50,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { leaf: T, proof: BytesLike[], ): boolean { - return toHex(root) === processProof(standardLeafHasher(leafEncoding)(leaf), proof); + return toHex(root) === processProof(standardLeafHasher(leafEncoding, leaf), proof); } static verifyMultiProof( @@ -59,7 +59,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { multiproof: MultiProof ): boolean { return toHex(root) === processMultiProof({ - leaves: multiproof.leaves.map(leaf => standardLeafHasher(leafEncoding)(leaf)), + leaves: multiproof.leaves.map(leaf => standardLeafHasher(leafEncoding, leaf)), proof: multiproof.proof, proofFlags: multiproof.proofFlags, }); From d9678a6b572fff4617964ec08ad303e3c6a6e49a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 23 Feb 2024 22:41:16 +0100 Subject: [PATCH 07/36] fix default --- src/merkletree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index d42ee7b..1d2d278 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -52,7 +52,7 @@ export class MerkleTreeImpl implements MerkleTree { options: MerkleTreeOptions = {}, leafHasher: (value: T) => HexString, ): [tree: HexString[], indexedValues: MerkleTreeData['values']] { - const { sortLeaves } = { ...defaultOptions, ...options }; + const sortLeaves = options.sortLeaves ?? defaultOptions.sortLeaves; const hashedValues = values.map((value, valueIndex) => ({ value, From 034ce88c8aaa7c6e45856bcc2fd71fe15b66de53 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 23 Feb 2024 22:50:40 +0100 Subject: [PATCH 08/36] newline --- src/simple.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/simple.ts b/src/simple.ts index 91571b4..ccc7c9a 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -41,4 +41,4 @@ export class SimpleMerkleTree extends MerkleTreeImpl { ): boolean { return toHex(root) === processMultiProof(multiproof); } -} \ No newline at end of file +} From 2da68b173baf4a6e3e083181e4cfa3b6142e708f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 23 Feb 2024 22:54:53 +0100 Subject: [PATCH 09/36] add prettier (config ported from @openzeppelin/contracts) --- .github/workflows/test.yml | 1 + .prettierrc | 6 ++ package-lock.json | 16 ++++ package.json | 3 + src/bytes.ts | 10 +-- src/core.test.ts | 30 +++---- src/core.ts | 39 +++++---- src/index.test.ts | 8 +- src/merkletree.ts | 43 ++++------ src/simple.test.ts | 131 +++++++++++++---------------- src/simple.ts | 40 ++++----- src/standard.test.ts | 165 ++++++++++++++++--------------------- src/standard.ts | 54 ++++++------ 13 files changed, 251 insertions(+), 295 deletions(-) create mode 100644 .prettierrc diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5832939..14fb690 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,4 +13,5 @@ jobs: - name: Set up environment uses: ./.github/actions/setup - run: npm run coverage -- --forbid-only + - run: npm run lint - uses: codecov/codecov-action@v3 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5a43c2d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "avoid" +} diff --git a/package-lock.json b/package-lock.json index 3083cbe..d23ff93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "c8": "^7.12.0", "fast-check": "^3.3.0", "mocha": "^10.1.0", + "prettier": "^3.2.5", "ts-node": "^10.9.1", "typescript": "^4.8.4" } @@ -1457,6 +1458,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pure-rand": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", diff --git a/package.json b/package.json index a485cbc..deb44a9 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "prepublishOnly": "npm run clean", "prepare": "tsc", "clean": "rm -rf dist", + "lint": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check", + "lint:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write", "test": "mocha", "coverage": "c8 npm run test --" }, @@ -31,6 +33,7 @@ "c8": "^7.12.0", "fast-check": "^3.3.0", "mocha": "^10.1.0", + "prettier": "^3.2.5", "ts-node": "^10.9.1", "typescript": "^4.8.4" } diff --git a/src/bytes.ts b/src/bytes.ts index 5f59c86..ae4d0a8 100644 --- a/src/bytes.ts +++ b/src/bytes.ts @@ -1,13 +1,7 @@ -import type { BytesLike } from "@ethersproject/bytes"; +import type { BytesLike } from '@ethersproject/bytes'; type HexString = string; -import { - isBytesLike, - arrayify as toBytes, - hexlify as toHex, - concat, - isBytes, -} from "@ethersproject/bytes"; +import { isBytesLike, arrayify as toBytes, hexlify as toHex, concat, isBytes } from '@ethersproject/bytes'; function compare(a: BytesLike, b: BytesLike): number { const diff = BigInt(toHex(a)) - BigInt(toHex(b)); diff --git a/src/core.test.ts b/src/core.test.ts index 737cae6..8822a46 100644 --- a/src/core.test.ts +++ b/src/core.test.ts @@ -2,7 +2,15 @@ import fc from 'fast-check'; import assert from 'assert/strict'; import { HashZero as zero } from '@ethersproject/constants'; import { keccak256 } from '@ethersproject/keccak256'; -import { makeMerkleTree, getProof, processProof, getMultiProof, processMultiProof, isValidMerkleTree, renderMerkleTree } from './core'; +import { + makeMerkleTree, + getProof, + processProof, + getMultiProof, + processMultiProof, + isValidMerkleTree, + renderMerkleTree, +} from './core'; import { toHex, compare } from './bytes'; const leaf = fc.uint8Array({ minLength: 32, maxLength: 32 }).map(toHex); @@ -47,18 +55,12 @@ describe('core properties', () => { describe('core error conditions', () => { it('zero leaves', () => { - assert.throws( - () => makeMerkleTree([]), - /^Error: Expected non-zero number of leaves$/, - ); + assert.throws(() => makeMerkleTree([]), /^Error: Expected non-zero number of leaves$/); }); it('multiproof duplicate index', () => { const tree = makeMerkleTree(new Array(2).fill(zero)); - assert.throws( - () => getMultiProof(tree, [1, 1]), - /^Error: Cannot prove duplicated index$/, - ); + assert.throws(() => getMultiProof(tree, [1, 1]), /^Error: Cannot prove duplicated index$/); }); it('tree validity', () => { @@ -66,10 +68,7 @@ describe('core error conditions', () => { assert(!isValidMerkleTree([zero, zero]), 'even number of nodes'); assert(!isValidMerkleTree([zero, zero, zero]), 'inner node not hash of children'); - assert.throws( - () => renderMerkleTree([]), - /^Error: Expected non-zero number of nodes$/, - ); + assert.throws(() => renderMerkleTree([]), /^Error: Expected non-zero number of nodes$/); }); it('multiproof invariants', () => { @@ -82,9 +81,6 @@ describe('core error conditions', () => { proofFlags: [true, true, false], }; - assert.throws( - () => processMultiProof(badMultiProof), - /^Error: Broken invariant$/, - ); + assert.throws(() => processMultiProof(badMultiProof), /^Error: Broken invariant$/); }); }); diff --git a/src/core.ts b/src/core.ts index 3672314..065fc0e 100644 --- a/src/core.ts +++ b/src/core.ts @@ -4,18 +4,19 @@ import { throwError } from './utils/throw-error'; const hashPair = (a: BytesLike, b: BytesLike): HexString => keccak256(concat([a, b].sort(compare))); -const leftChildIndex = (i: number) => 2 * i + 1; +const leftChildIndex = (i: number) => 2 * i + 1; const rightChildIndex = (i: number) => 2 * i + 2; -const parentIndex = (i: number) => i > 0 ? Math.floor((i - 1) / 2) : throwError('Root has no parent'); -const siblingIndex = (i: number) => i > 0 ? i - (-1) ** (i % 2) : throwError('Root has no siblings'); +const parentIndex = (i: number) => (i > 0 ? Math.floor((i - 1) / 2) : throwError('Root has no parent')); +const siblingIndex = (i: number) => (i > 0 ? i - (-1) ** (i % 2) : throwError('Root has no siblings')); -const isTreeNode = (tree: unknown[], i: number) => i >= 0 && i < tree.length; -const isInternalNode = (tree: unknown[], i: number) => isTreeNode(tree, leftChildIndex(i)); -const isLeafNode = (tree: unknown[], i: number) => isTreeNode(tree, i) && !isInternalNode(tree, i); +const isTreeNode = (tree: unknown[], i: number) => i >= 0 && i < tree.length; +const isInternalNode = (tree: unknown[], i: number) => isTreeNode(tree, leftChildIndex(i)); +const isLeafNode = (tree: unknown[], i: number) => isTreeNode(tree, i) && !isInternalNode(tree, i); const isValidMerkleNode = (node: BytesLike) => toBytes(node).length === 32; -const checkLeafNode = (tree: unknown[], i: number) => void (isLeafNode(tree, i) || throwError('Index is not a leaf')); -const checkValidMerkleNode = (node: BytesLike) => void (isValidMerkleNode(node) || throwError('Merkle tree nodes must be Uint8Array of length 32')); +const checkLeafNode = (tree: unknown[], i: number) => void (isLeafNode(tree, i) || throwError('Index is not a leaf')); +const checkValidMerkleNode = (node: BytesLike) => + void (isValidMerkleNode(node) || throwError('Merkle tree nodes must be Uint8Array of length 32')); export function makeMerkleTree(leaves: BytesLike[]): HexString[] { leaves.forEach(checkValidMerkleNode); @@ -30,10 +31,7 @@ export function makeMerkleTree(leaves: BytesLike[]): HexString[] { tree[tree.length - 1 - i] = toHex(leaf); } for (let i = tree.length - 1 - leaves.length; i >= 0; i--) { - tree[i] = hashPair( - tree[leftChildIndex(i)]!, - tree[rightChildIndex(i)]!, - ); + tree[i] = hashPair(tree[leftChildIndex(i)]!, tree[rightChildIndex(i)]!); } return tree; @@ -126,7 +124,7 @@ export function processMultiProof(multiproof: MultiProof): HexString } if (stack.length + proof.length !== 1) { - throw new Error('Broken invariant'); + throw new Error('Broken invariant'); } return toHex(stack.pop() ?? proof.shift()!); @@ -166,10 +164,17 @@ export function renderMerkleTree(tree: BytesLike[]): HexString { const [i, path] = stack.pop()!; lines.push( - path.slice(0, -1).map(p => [' ', '│ '][p]).join('') + - path.slice(-1).map(p => ['└─ ', '├─ '][p]).join('') + - i + ') ' + - toHex(tree[i]!) + path + .slice(0, -1) + .map(p => [' ', '│ '][p]) + .join('') + + path + .slice(-1) + .map(p => ['└─ ', '├─ '][p]) + .join('') + + i + + ') ' + + toHex(tree[i]!), ); if (rightChildIndex(i) < tree.length) { diff --git a/src/index.test.ts b/src/index.test.ts index cec81fe..095cff5 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,8 +1,8 @@ -import assert from "assert/strict"; -import { SimpleMerkleTree, StandardMerkleTree } from "."; +import assert from 'assert/strict'; +import { SimpleMerkleTree, StandardMerkleTree } from '.'; -describe("index properties", () => { - it("classes are exported", () => { +describe('index properties', () => { + it('classes are exported', () => { assert.notEqual(SimpleMerkleTree, undefined); assert.notEqual(StandardMerkleTree, undefined); }); diff --git a/src/merkletree.ts b/src/merkletree.ts index 1d2d278..be67c04 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -1,4 +1,4 @@ -import { BytesLike, HexString, toHex, compare } from "./bytes"; +import { BytesLike, HexString, toHex, compare } from './bytes'; import { MultiProof, @@ -9,16 +9,16 @@ import { processProof, processMultiProof, renderMerkleTree, -} from "./core"; +} from './core'; -import { MerkleTreeOptions, defaultOptions } from "./options"; -import { checkBounds } from "./utils/check-bounds"; -import { throwError } from "./utils/throw-error"; +import { MerkleTreeOptions, defaultOptions } from './options'; +import { checkBounds } from './utils/check-bounds'; +import { throwError } from './utils/throw-error'; export type MerkleTreeData = { format: string; tree: HexString[]; - values: { value: T; treeIndex: number; }[]; + values: { value: T; treeIndex: number }[]; }; export interface MerkleTree { @@ -42,9 +42,7 @@ export class MerkleTreeImpl implements MerkleTree { protected readonly values: MerkleTreeData['values'], protected readonly leafHasher: (leaf: T) => HexString, ) { - this.hashLookup = Object.fromEntries( - values.map(({ treeIndex }, valueIndex) => [tree.at(treeIndex), valueIndex]) - ); + this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree.at(treeIndex), valueIndex])); } protected static prepare( @@ -64,9 +62,9 @@ export class MerkleTreeImpl implements MerkleTree { hashedValues.sort((a, b) => compare(a.hash, b.hash)); } - const tree = makeMerkleTree(hashedValues.map((v) => v.hash)); + const tree = makeMerkleTree(hashedValues.map(v => v.hash)); - const indexedValues = values.map((value) => ({ + const indexedValues = values.map(value => ({ value, treeIndex: 0, })); @@ -83,7 +81,7 @@ export class MerkleTreeImpl implements MerkleTree { dump(): MerkleTreeData { return { - format: "simple-v1", + format: 'simple-v1', tree: this.tree, values: this.values, }; @@ -104,20 +102,17 @@ export class MerkleTreeImpl implements MerkleTree { this.validateValue(i); } if (!isValidMerkleTree(this.tree)) { - throwError("Merkle tree is invalid"); + throwError('Merkle tree is invalid'); } } leafLookup(leaf: T): number { - return ( - this.hashLookup[toHex(this.leafHash(leaf))] ?? - throwError("Leaf is not in tree") - ); + return this.hashLookup[toHex(this.leafHash(leaf))] ?? throwError('Leaf is not in tree'); } getProof(leaf: number | T): HexString[] { // input validity - const valueIndex = typeof leaf === "number" ? leaf : this.leafLookup(leaf); + const valueIndex = typeof leaf === 'number' ? leaf : this.leafLookup(leaf); this.validateValue(valueIndex); // rebuild tree index and generate proof @@ -126,7 +121,7 @@ export class MerkleTreeImpl implements MerkleTree { // sanity check proof if (!this._verify(this.tree[treeIndex]!, proof)) { - throwError("Unable to prove value"); + throwError('Unable to prove value'); } // return proof in hex format @@ -135,18 +130,16 @@ export class MerkleTreeImpl implements MerkleTree { getMultiProof(leaves: (number | T)[]): MultiProof { // input validity - const valueIndices = leaves.map((leaf) => - typeof leaf === "number" ? leaf : this.leafLookup(leaf) - ); + const valueIndices = leaves.map(leaf => (typeof leaf === 'number' ? leaf : this.leafLookup(leaf))); for (const valueIndex of valueIndices) this.validateValue(valueIndex); // rebuild tree indices and generate proof - const indices = valueIndices.map((i) => this.values[i]!.treeIndex); + const indices = valueIndices.map(i => this.values[i]!.treeIndex); const proof = getMultiProof(this.tree, indices); // sanity check proof if (!this._verifyMultiProof(proof)) { - throwError("Unable to prove values"); + throwError('Unable to prove values'); } // return multiproof in hex format @@ -175,7 +168,7 @@ export class MerkleTreeImpl implements MerkleTree { checkBounds(this.tree, treeIndex); const hashedLeaf = this.leafHash(leaf); if (hashedLeaf !== this.tree[treeIndex]!) { - throwError("Merkle tree does not contain the expected value"); + throwError('Merkle tree does not contain the expected value'); } return hashedLeaf; } diff --git a/src/simple.test.ts b/src/simple.test.ts index b426c49..1cb62c5 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -1,21 +1,21 @@ -import assert from "assert/strict"; -import { HashZero as zero } from "@ethersproject/constants"; -import { keccak256 } from "@ethersproject/keccak256"; -import { SimpleMerkleTree } from "./simple"; +import assert from 'assert/strict'; +import { HashZero as zero } from '@ethersproject/constants'; +import { keccak256 } from '@ethersproject/keccak256'; +import { SimpleMerkleTree } from './simple'; -describe("simple merkle tree", () => { +describe('simple merkle tree', () => { for (const opts of [{}, { sortLeaves: true }, { sortLeaves: false }]) { describe(`with options '${JSON.stringify(opts)}'`, () => { - const leaves = "abcdef".split("").map((c) => keccak256(Buffer.from(c))); - const otherLeaves = "abc".split("").map((c) => keccak256(Buffer.from(c))); + const leaves = 'abcdef'.split('').map(c => keccak256(Buffer.from(c))); + const otherLeaves = 'abc'.split('').map(c => keccak256(Buffer.from(c))); const tree = SimpleMerkleTree.of(leaves, opts); const otherTree = SimpleMerkleTree.of(otherLeaves, opts); - it("generates a valid tree", () => { + it('generates a valid tree', () => { tree.validate(); }); - it("generates valid single proofs for all leaves", () => { + it('generates valid single proofs for all leaves', () => { for (const [id, leaf] of tree.entries()) { const proof1 = tree.getProof(id); const proof2 = tree.getProof(leaf); @@ -28,7 +28,7 @@ describe("simple merkle tree", () => { } }); - it("rejects invalid proofs", () => { + it('rejects invalid proofs', () => { const leaf = leaves[0]!; const invalidProof = otherTree.getProof(leaf); @@ -36,18 +36,10 @@ describe("simple merkle tree", () => { assert(!SimpleMerkleTree.verify(tree.root, leaf, invalidProof)); }); - it("generates valid multiproofs", () => { - for (const ids of [ - [], - [0, 1], - [0, 1, 5], - [1, 3, 4, 5], - [0, 2, 4, 5], - [0, 1, 2, 3, 4, 5], - [4, 1, 5, 0, 2], - ]) { + it('generates valid multiproofs', () => { + for (const ids of [[], [0, 1], [0, 1, 5], [1, 3, 4, 5], [0, 2, 4, 5], [0, 1, 2, 3, 4, 5], [4, 1, 5, 0, 2]]) { const proof1 = tree.getMultiProof(ids); - const proof2 = tree.getMultiProof(ids.map((i) => leaves[i]!)); + const proof2 = tree.getMultiProof(ids.map(i => leaves[i]!)); assert.deepEqual(proof1, proof2); @@ -56,108 +48,99 @@ describe("simple merkle tree", () => { } }); - it("rejects invalid multiproofs", () => { + it('rejects invalid multiproofs', () => { const multiProof = otherTree.getMultiProof(leaves.slice(0, 3)); assert(!tree.verifyMultiProof(multiProof)); assert(!SimpleMerkleTree.verifyMultiProof(tree.root, multiProof)); }); - it("renders tree representation", () => { + it('renders tree representation', () => { assert.equal( tree.render(), opts.sortLeaves == false ? [ - "0) 0x9012f1e18a87790d2e01faace75aaaca38e53df437cdce2c0552464dda4af49c", - "├─ 1) 0x68203f90e9d07dc5859259d7536e87a6ba9d345f2552b5b9de2999ddce9ce1bf", - "│ ├─ 3) 0xd253a52d4cb00de2895e85f2529e2976e6aaaa5c18106b68ab66813e14415669", - "│ │ ├─ 7) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3", - "│ │ └─ 8) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2", - "│ └─ 4) 0x805b21d846b189efaeb0377d6bb0d201b3872a363e607c25088f025b0c6ae1f8", - "│ ├─ 9) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510", - "│ └─ 10) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb", - "└─ 2) 0xf0b49bb4b0d9396e0315755ceafaa280707b32e75e6c9053f5cdf2679dcd5c6a", - " ├─ 5) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483", - " └─ 6) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761", - ].join("\n") + '0) 0x9012f1e18a87790d2e01faace75aaaca38e53df437cdce2c0552464dda4af49c', + '├─ 1) 0x68203f90e9d07dc5859259d7536e87a6ba9d345f2552b5b9de2999ddce9ce1bf', + '│ ├─ 3) 0xd253a52d4cb00de2895e85f2529e2976e6aaaa5c18106b68ab66813e14415669', + '│ │ ├─ 7) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3', + '│ │ └─ 8) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2', + '│ └─ 4) 0x805b21d846b189efaeb0377d6bb0d201b3872a363e607c25088f025b0c6ae1f8', + '│ ├─ 9) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510', + '│ └─ 10) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb', + '└─ 2) 0xf0b49bb4b0d9396e0315755ceafaa280707b32e75e6c9053f5cdf2679dcd5c6a', + ' ├─ 5) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483', + ' └─ 6) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761', + ].join('\n') : [ - "0) 0x1b404f199ea828ec5771fb30139c222d8417a82175fefad5cd42bc3a189bd8d5", - "├─ 1) 0xec554bdfb01d31fa838d0830339b0e6e8a70e0d55a8f172ffa8bebbf8e8d5ba0", - "│ ├─ 3) 0x434d51cfeb80272378f4c3a8fd2824561c2cad9fce556ea600d46f20550976a6", - "│ │ ├─ 7) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510", - "│ │ └─ 8) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761", - "│ └─ 4) 0x7dea550f679f3caab547cbbc5ee1a4c978c8c039b572ba00af1baa6481b88360", - "│ ├─ 9) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb", - "│ └─ 10) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2", - "└─ 2) 0xaf46af0745b433e1d5bed9a04b1fdf4002f67a733c20db2fca5b2af6120d9bcb", - " ├─ 5) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3", - " └─ 6) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483", - ].join("\n") + '0) 0x1b404f199ea828ec5771fb30139c222d8417a82175fefad5cd42bc3a189bd8d5', + '├─ 1) 0xec554bdfb01d31fa838d0830339b0e6e8a70e0d55a8f172ffa8bebbf8e8d5ba0', + '│ ├─ 3) 0x434d51cfeb80272378f4c3a8fd2824561c2cad9fce556ea600d46f20550976a6', + '│ │ ├─ 7) 0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510', + '│ │ └─ 8) 0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761', + '│ └─ 4) 0x7dea550f679f3caab547cbbc5ee1a4c978c8c039b572ba00af1baa6481b88360', + '│ ├─ 9) 0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb', + '│ └─ 10) 0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2', + '└─ 2) 0xaf46af0745b433e1d5bed9a04b1fdf4002f67a733c20db2fca5b2af6120d9bcb', + ' ├─ 5) 0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3', + ' └─ 6) 0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483', + ].join('\n'), ); }); - it("dump and load", () => { + it('dump and load', () => { const recoveredTree = SimpleMerkleTree.load(tree.dump()); recoveredTree.validate(); assert.deepEqual(tree, recoveredTree); }); - it("reject out of bounds value index", () => { - assert.throws( - () => tree.getProof(leaves.length), - /^Error: Index out of bounds$/ - ); + it('reject out of bounds value index', () => { + assert.throws(() => tree.getProof(leaves.length), /^Error: Index out of bounds$/); }); - it("reject invalid leaf size", () => { - const invalidLeaf = [zero + "00"]; // 33 bytes (all zero) + it('reject invalid leaf size', () => { + const invalidLeaf = [zero + '00']; // 33 bytes (all zero) assert.throws( () => SimpleMerkleTree.of(invalidLeaf, opts), - `Error: ${invalidLeaf} is not a valid 32 bytes object (pos: 0)` + `Error: ${invalidLeaf} is not a valid 32 bytes object (pos: 0)`, ); }); }); } - describe("tree dumps", () => { - it("reject unrecognized tree dump", () => { + describe('tree dumps', () => { + it('reject unrecognized tree dump', () => { assert.throws( - () => SimpleMerkleTree.load({ format: "nonstandard" } as any), - /^Error: Unknown format 'nonstandard'$/ + () => SimpleMerkleTree.load({ format: 'nonstandard' } as any), + /^Error: Unknown format 'nonstandard'$/, ); assert.throws( - () => SimpleMerkleTree.load({ format: "standard-v1" } as any), - /^Error: Unknown format 'standard-v1'$/ + () => SimpleMerkleTree.load({ format: 'standard-v1' } as any), + /^Error: Unknown format 'standard-v1'$/, ); }); - it("reject malformed tree dump", () => { + it('reject malformed tree dump', () => { const loadedTree1 = SimpleMerkleTree.load({ - format: "simple-v1", + format: 'simple-v1', tree: [zero], values: [ { - value: "0x0000000000000000000000000000000000000000000000000000000000000001", + value: '0x0000000000000000000000000000000000000000000000000000000000000001', treeIndex: 0, }, ], }); - assert.throws( - () => loadedTree1.getProof(0), - /^Error: Merkle tree does not contain the expected value$/ - ); + assert.throws(() => loadedTree1.getProof(0), /^Error: Merkle tree does not contain the expected value$/); const loadedTree2 = SimpleMerkleTree.load({ - format: "simple-v1", + format: 'simple-v1', tree: [zero, zero, zero], values: [{ value: zero, treeIndex: 2 }], }); - assert.throws( - () => loadedTree2.getProof(0), - /^Error: Unable to prove value$/ - ); + assert.throws(() => loadedTree2.getProof(0), /^Error: Unable to prove value$/); }); }); }); diff --git a/src/simple.ts b/src/simple.ts index ccc7c9a..c3b82bf 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -1,44 +1,32 @@ -import { defaultAbiCoder } from "@ethersproject/abi"; -import { BytesLike, HexString, toHex } from "./bytes"; -import { MultiProof, processProof, processMultiProof } from "./core"; -import { MerkleTreeData, MerkleTreeImpl } from "./merkletree"; -import { MerkleTreeOptions } from "./options"; -import { throwError } from "./utils/throw-error"; +import { defaultAbiCoder } from '@ethersproject/abi'; +import { BytesLike, HexString, toHex } from './bytes'; +import { MultiProof, processProof, processMultiProof } from './core'; +import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; +import { MerkleTreeOptions } from './options'; +import { throwError } from './utils/throw-error'; export function formatLeaf(value: BytesLike): HexString { - return defaultAbiCoder.encode(["bytes32"], [ value ]); + return defaultAbiCoder.encode(['bytes32'], [value]); } export class SimpleMerkleTree extends MerkleTreeImpl { - static of = ( - values: BytesLike[], - options: MerkleTreeOptions = {}, - ): SimpleMerkleTree => { - const [ tree, indexedValues ] = MerkleTreeImpl.prepare(values, options, formatLeaf); + static of = (values: BytesLike[], options: MerkleTreeOptions = {}): SimpleMerkleTree => { + const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, formatLeaf); return new SimpleMerkleTree(tree, indexedValues, formatLeaf); - } + }; - static load( - data: MerkleTreeData, - ): SimpleMerkleTree { - if (data.format !== "simple-v1") { + static load(data: MerkleTreeData): SimpleMerkleTree { + if (data.format !== 'simple-v1') { throwError(`Unknown format '${data.format}'`); } return new SimpleMerkleTree(data.tree, data.values, formatLeaf); } - static verify( - root: BytesLike, - leaf: BytesLike, - proof: BytesLike[], - ): boolean { + static verify(root: BytesLike, leaf: BytesLike, proof: BytesLike[]): boolean { return toHex(root) === processProof(formatLeaf(leaf), proof); } - static verifyMultiProof( - root: BytesLike, - multiproof: MultiProof, - ): boolean { + static verifyMultiProof(root: BytesLike, multiproof: MultiProof): boolean { return toHex(root) === processMultiProof(multiproof); } } diff --git a/src/standard.test.ts b/src/standard.test.ts index aede028..01abf16 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -1,9 +1,9 @@ -import assert from "assert/strict"; -import { HashZero as zero } from "@ethersproject/constants"; -import { keccak256 } from "@ethersproject/keccak256"; -import { StandardMerkleTree, StandardMerkleTreeData } from "./standard"; +import assert from 'assert/strict'; +import { HashZero as zero } from '@ethersproject/constants'; +import { keccak256 } from '@ethersproject/keccak256'; +import { StandardMerkleTree, StandardMerkleTreeData } from './standard'; -describe("standard merkle tree", () => { +describe('standard merkle tree', () => { it('Supports complex leaf types', () => { const leaves = [ [0, []], @@ -11,35 +11,35 @@ describe("standard merkle tree", () => { [2, ['hello', 'world']], [3, ['merkle', 'tree']], ]; - const types = [ 'uint256', 'string[]' ]; + const types = ['uint256', 'string[]']; StandardMerkleTree.of(leaves, types); }); for (const opts of [{}, { sortLeaves: true }, { sortLeaves: false }]) { describe(`with options '${JSON.stringify(opts)}'`, () => { - const leaves = "abcdef".split("").map((c) => [c]); - const otherLeaves = "abc".split("").map((c) => [c]); + const leaves = 'abcdef'.split('').map(c => [c]); + const otherLeaves = 'abc'.split('').map(c => [c]); - const tree = StandardMerkleTree.of(leaves, ["string"], opts); - const otherTree = StandardMerkleTree.of(otherLeaves, ["string"], opts); + const tree = StandardMerkleTree.of(leaves, ['string'], opts); + const otherTree = StandardMerkleTree.of(otherLeaves, ['string'], opts); - it("rejects loading a tree without leaf encoding", () => { + it('rejects loading a tree without leaf encoding', () => { assert.throws( () => StandardMerkleTree.load({ - format: "standard-v1", + format: 'standard-v1', tree: [zero], - values: [{ value: ["0"], treeIndex: 0 }], + values: [{ value: ['0'], treeIndex: 0 }], } as StandardMerkleTreeData<[string]>), - /^Error: Expected leaf encoding$/ + /^Error: Expected leaf encoding$/, ); }); - it("generates a valid tree", () => { + it('generates a valid tree', () => { tree.validate(); }); - it("generates valid single proofs for all leaves", () => { + it('generates valid single proofs for all leaves', () => { for (const [id, leaf] of tree.entries()) { const proof1 = tree.getProof(id); const proof2 = tree.getProof(leaf); @@ -48,37 +48,27 @@ describe("standard merkle tree", () => { assert(tree.verify(id, proof1)); assert(tree.verify(leaf, proof1)); - assert(StandardMerkleTree.verify(tree.root, ["string"], leaf, proof1)); + assert(StandardMerkleTree.verify(tree.root, ['string'], leaf, proof1)); } }); - it("rejects invalid proofs", () => { + it('rejects invalid proofs', () => { const leaf = leaves[0]!; const invalidProof = otherTree.getProof(leaf); assert(!tree.verify(leaf, invalidProof)); - assert(!StandardMerkleTree.verify(tree.root, ["string"], leaf, invalidProof)); + assert(!StandardMerkleTree.verify(tree.root, ['string'], leaf, invalidProof)); }); - it("generates valid multiproofs", () => { - for (const ids of [ - [], - [0, 1], - [0, 1, 5], - [1, 3, 4, 5], - [0, 2, 4, 5], - [0, 1, 2, 3, 4, 5], - [4, 1, 5, 0, 2], - ]) { + it('generates valid multiproofs', () => { + for (const ids of [[], [0, 1], [0, 1, 5], [1, 3, 4, 5], [0, 2, 4, 5], [0, 1, 2, 3, 4, 5], [4, 1, 5, 0, 2]]) { const proof1 = tree.getMultiProof(ids); - const proof2 = tree.getMultiProof(ids.map((i) => leaves[i]!)); + const proof2 = tree.getMultiProof(ids.map(i => leaves[i]!)); assert.deepEqual(proof1, proof2); assert(tree.verifyMultiProof(proof1)); - assert( - StandardMerkleTree.verifyMultiProof(tree.root, ["string"], proof1) - ); + assert(StandardMerkleTree.verifyMultiProof(tree.root, ['string'], proof1)); // assert( // StandardMerkleTree.verifyMultiProof(tree.root, proof1, { // leafEncoding: ["string"], @@ -87,17 +77,11 @@ describe("standard merkle tree", () => { } }); - it("rejects invalid multiproofs", () => { + it('rejects invalid multiproofs', () => { const multiProof = otherTree.getMultiProof(leaves.slice(0, 3)); assert(!tree.verifyMultiProof(multiProof)); - assert( - !StandardMerkleTree.verifyMultiProof( - tree.root, - ["string"], - multiProof - ) - ); + assert(!StandardMerkleTree.verifyMultiProof(tree.root, ['string'], multiProof)); // assert( // !StandardMerkleTree.verifyMultiProof(tree.root, multiProof, { // leafEncoding: ["string"], @@ -105,40 +89,40 @@ describe("standard merkle tree", () => { // ); }); - it("renders tree representation", () => { + it('renders tree representation', () => { assert.equal( tree.render(), opts.sortLeaves == false ? [ - "0) 0x23be0977360f08bb0bd7f709a7d543d2cd779c79c66d74e0441919871647de2b", - "├─ 1) 0x8f7234e8cfe39c08ca84a3a3e3274f574af26fd15165fe29e09cbab742daccd9", - "│ ├─ 3) 0x03707d7802a71ca56a8ad8028da98c4f1dbec55b31b4a25d536b5309cc20eda9", - "│ │ ├─ 7) 0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b", - "│ │ └─ 8) 0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b", - "│ └─ 4) 0xfa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016", - "│ ├─ 9) 0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681", - "│ └─ 10) 0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c", - "└─ 2) 0x7b0c6cd04b82bfc0e250030a5d2690c52585e0cc6a4f3bc7909d7723b0236ece", - " ├─ 5) 0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848", - " └─ 6) 0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e", - ].join("\n") + '0) 0x23be0977360f08bb0bd7f709a7d543d2cd779c79c66d74e0441919871647de2b', + '├─ 1) 0x8f7234e8cfe39c08ca84a3a3e3274f574af26fd15165fe29e09cbab742daccd9', + '│ ├─ 3) 0x03707d7802a71ca56a8ad8028da98c4f1dbec55b31b4a25d536b5309cc20eda9', + '│ │ ├─ 7) 0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b', + '│ │ └─ 8) 0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b', + '│ └─ 4) 0xfa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016', + '│ ├─ 9) 0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681', + '│ └─ 10) 0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c', + '└─ 2) 0x7b0c6cd04b82bfc0e250030a5d2690c52585e0cc6a4f3bc7909d7723b0236ece', + ' ├─ 5) 0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848', + ' └─ 6) 0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e', + ].join('\n') : [ - "0) 0x6deb52b5da8fd108f79fab00341f38d2587896634c646ee52e49f845680a70c8", - "├─ 1) 0x52426e0f1f65ff7e209a13b8c29cffe82e3acaf3dad0a9b9088f3b9a61a929c3", - "│ ├─ 3) 0x8076923e76cf01a7c048400a2304c9a9c23bbbdac3a98ea3946340fdafbba34f", - "│ │ ├─ 7) 0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b", - "│ │ └─ 8) 0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c", - "│ └─ 4) 0x965b92c6cf08303cc4feb7f3e0819c436c2cec17c6f0688a6af139c9a368707c", - "│ ├─ 9) 0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e", - "│ └─ 10) 0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681", - "└─ 2) 0xfd3cf45654e88d1cc5d663578c82c76f4b5e3826bacaa1216441443504538f51", - " ├─ 5) 0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b", - " └─ 6) 0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848", - ].join("\n") + '0) 0x6deb52b5da8fd108f79fab00341f38d2587896634c646ee52e49f845680a70c8', + '├─ 1) 0x52426e0f1f65ff7e209a13b8c29cffe82e3acaf3dad0a9b9088f3b9a61a929c3', + '│ ├─ 3) 0x8076923e76cf01a7c048400a2304c9a9c23bbbdac3a98ea3946340fdafbba34f', + '│ │ ├─ 7) 0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b', + '│ │ └─ 8) 0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c', + '│ └─ 4) 0x965b92c6cf08303cc4feb7f3e0819c436c2cec17c6f0688a6af139c9a368707c', + '│ ├─ 9) 0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e', + '│ └─ 10) 0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681', + '└─ 2) 0xfd3cf45654e88d1cc5d663578c82c76f4b5e3826bacaa1216441443504538f51', + ' ├─ 5) 0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b', + ' └─ 6) 0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848', + ].join('\n'), ); }); - it("dump and load", () => { + it('dump and load', () => { const recoveredTree = StandardMerkleTree.load(tree.dump()); recoveredTree.validate(); @@ -150,58 +134,49 @@ describe("standard merkle tree", () => { } }); - it("reject out of bounds value index", () => { - assert.throws( - () => tree.getProof(leaves.length), - /^Error: Index out of bounds$/ - ); + it('reject out of bounds value index', () => { + assert.throws(() => tree.getProof(leaves.length), /^Error: Index out of bounds$/); }); }); } - describe("tree dumps", () => { - it("reject unrecognized tree dump", () => { + describe('tree dumps', () => { + it('reject unrecognized tree dump', () => { assert.throws( () => StandardMerkleTree.load({ - format: "nonstandard", - leafEncoding: ["string"], + format: 'nonstandard', + leafEncoding: ['string'], } as any), - /^Error: Unknown format 'nonstandard'$/ + /^Error: Unknown format 'nonstandard'$/, ); assert.throws( () => StandardMerkleTree.load({ - format: "simple-v1", - leafEncoding: ["string"], + format: 'simple-v1', + leafEncoding: ['string'], } as any), - /^Error: Unknown format 'simple-v1'$/ + /^Error: Unknown format 'simple-v1'$/, ); }); - it("reject malformed tree dump", () => { + it('reject malformed tree dump', () => { const loadedTree1 = StandardMerkleTree.load({ - format: "standard-v1", + format: 'standard-v1', tree: [zero], - values: [{ value: ["0"], treeIndex: 0 }], - leafEncoding: ["uint256"], + values: [{ value: ['0'], treeIndex: 0 }], + leafEncoding: ['uint256'], }); - assert.throws( - () => loadedTree1.getProof(0), - /^Error: Merkle tree does not contain the expected value$/ - ); + assert.throws(() => loadedTree1.getProof(0), /^Error: Merkle tree does not contain the expected value$/); const loadedTree2 = StandardMerkleTree.load({ - format: "standard-v1", + format: 'standard-v1', tree: [zero, zero, keccak256(keccak256(zero))], - values: [{ value: ["0"], treeIndex: 2 }], - leafEncoding: ["uint256"], + values: [{ value: ['0'], treeIndex: 2 }], + leafEncoding: ['uint256'], }); - assert.throws( - () => loadedTree2.getProof(0), - /^Error: Unable to prove value$/ - ); + assert.throws(() => loadedTree2.getProof(0), /^Error: Unable to prove value$/); }); }); }); diff --git a/src/standard.ts b/src/standard.ts index e23351e..8ba5344 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -1,12 +1,14 @@ -import { keccak256 } from "@ethersproject/keccak256"; -import { defaultAbiCoder } from "@ethersproject/abi"; -import { BytesLike, HexString, toHex } from "./bytes"; -import { MultiProof, processProof, processMultiProof } from "./core"; -import { MerkleTreeData, MerkleTreeImpl } from "./merkletree"; -import { MerkleTreeOptions } from "./options"; -import { throwError } from "./utils/throw-error"; +import { keccak256 } from '@ethersproject/keccak256'; +import { defaultAbiCoder } from '@ethersproject/abi'; +import { BytesLike, HexString, toHex } from './bytes'; +import { MultiProof, processProof, processMultiProof } from './core'; +import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; +import { MerkleTreeOptions } from './options'; +import { throwError } from './utils/throw-error'; -export type StandardMerkleTreeData = MerkleTreeData & { leafEncoding: string[]; }; +export type StandardMerkleTreeData = MerkleTreeData & { + leafEncoding: string[]; +}; export function standardLeafHasher(types: string[], value: T): HexString { return keccak256(keccak256(defaultAbiCoder.encode(types, value))); @@ -24,50 +26,44 @@ export class StandardMerkleTree extends MerkleTreeImpl { static of( values: T[], leafEncoding: string[], - options: MerkleTreeOptions = {} + options: MerkleTreeOptions = {}, ): StandardMerkleTree { - const [ tree, indexedValues ] = MerkleTreeImpl.prepare( - values, - options, - standardLeafHasher.bind(null, leafEncoding), - ); + const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, standardLeafHasher.bind(null, leafEncoding)); return new StandardMerkleTree(tree, indexedValues, leafEncoding); } static load(data: StandardMerkleTreeData): StandardMerkleTree { - if (data.format !== "standard-v1") { + if (data.format !== 'standard-v1') { throw new Error(`Unknown format '${data.format}'`); } if (data.leafEncoding === undefined) { - throwError("Expected leaf encoding"); + throwError('Expected leaf encoding'); } return new StandardMerkleTree(data.tree, data.values, data.leafEncoding); } - static verify( - root: BytesLike, - leafEncoding: string[], - leaf: T, - proof: BytesLike[], - ): boolean { + static verify(root: BytesLike, leafEncoding: string[], leaf: T, proof: BytesLike[]): boolean { return toHex(root) === processProof(standardLeafHasher(leafEncoding, leaf), proof); } static verifyMultiProof( root: BytesLike, leafEncoding: string[], - multiproof: MultiProof + multiproof: MultiProof, ): boolean { - return toHex(root) === processMultiProof({ - leaves: multiproof.leaves.map(leaf => standardLeafHasher(leafEncoding, leaf)), - proof: multiproof.proof, - proofFlags: multiproof.proofFlags, - }); + return ( + toHex(root) === + processMultiProof({ + leaves: multiproof.leaves.map(leaf => standardLeafHasher(leafEncoding, leaf)), + proof: multiproof.proof, + proofFlags: multiproof.proofFlags, + }) + ); } dump(): StandardMerkleTreeData { return { - format: "standard-v1", + format: 'standard-v1', tree: this.tree, values: this.values, leafEncoding: this.leafEncoding, From b2d34fc008ac7faf54b27cb3fcb6f20dff666064 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sat, 24 Feb 2024 09:50:39 +0100 Subject: [PATCH 10/36] more generic multiproof: input support all BytesLike, output is explicitelly HexString --- src/core.ts | 6 +++--- src/merkletree.ts | 8 ++++---- src/simple.ts | 2 +- src/standard.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/core.ts b/src/core.ts index 065fc0e..1a1cb4a 100644 --- a/src/core.ts +++ b/src/core.ts @@ -55,9 +55,9 @@ export function processProof(leaf: BytesLike, proof: BytesLike[]): HexString { return toHex(proof.reduce(hashPair, leaf)); } -export interface MultiProof { - leaves: T[]; - proof: HexString[]; +export interface MultiProof { + leaves: L[]; + proof: T[]; proofFlags: boolean[]; } diff --git a/src/merkletree.ts b/src/merkletree.ts index be67c04..890963a 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -29,9 +29,9 @@ export interface MerkleTree { validate(): void; leafLookup(leaf: T): number; getProof(leaf: number | T): HexString[]; - getMultiProof(leaves: (number | T)[]): MultiProof; + getMultiProof(leaves: (number | T)[]): MultiProof; verify(leaf: number | T, proof: HexString[]): boolean; - verifyMultiProof(multiproof: MultiProof): boolean; + verifyMultiProof(multiproof: MultiProof): boolean; } export class MerkleTreeImpl implements MerkleTree { @@ -128,7 +128,7 @@ export class MerkleTreeImpl implements MerkleTree { return proof; } - getMultiProof(leaves: (number | T)[]): MultiProof { + getMultiProof(leaves: (number | T)[]): MultiProof { // input validity const valueIndices = leaves.map(leaf => (typeof leaf === 'number' ? leaf : this.leafLookup(leaf))); for (const valueIndex of valueIndices) this.validateValue(valueIndex); @@ -154,7 +154,7 @@ export class MerkleTreeImpl implements MerkleTree { return this._verify(this.leafHash(leaf), proof); } - verifyMultiProof(multiproof: MultiProof): boolean { + verifyMultiProof(multiproof: MultiProof): boolean { return this._verifyMultiProof({ leaves: multiproof.leaves.map(l => this.leafHash(l)), proof: multiproof.proof, diff --git a/src/simple.ts b/src/simple.ts index c3b82bf..db83d02 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -26,7 +26,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { return toHex(root) === processProof(formatLeaf(leaf), proof); } - static verifyMultiProof(root: BytesLike, multiproof: MultiProof): boolean { + static verifyMultiProof(root: BytesLike, multiproof: MultiProof): boolean { return toHex(root) === processMultiProof(multiproof); } } diff --git a/src/standard.ts b/src/standard.ts index 8ba5344..681c1a3 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -49,7 +49,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { static verifyMultiProof( root: BytesLike, leafEncoding: string[], - multiproof: MultiProof, + multiproof: MultiProof, ): boolean { return ( toHex(root) === From 62ab1292ac8cbea5addf3bd2554b92be61eb761e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sat, 24 Feb 2024 09:58:23 +0100 Subject: [PATCH 11/36] remove empty file --- index.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 index.test.ts diff --git a/index.test.ts b/index.test.ts deleted file mode 100644 index e69de29..0000000 From ec339e185ffa6a7b1bfa987386d61a1d21c0e589 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 25 Feb 2024 17:06:50 -0600 Subject: [PATCH 12/36] Apply PR suggestions --- src/bytes.ts | 4 ++-- src/merkletree.ts | 13 +++---------- src/simple.ts | 18 +++++++++++++++--- src/standard.test.ts | 10 ---------- src/standard.ts | 9 ++++++--- 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/bytes.ts b/src/bytes.ts index ae4d0a8..938c013 100644 --- a/src/bytes.ts +++ b/src/bytes.ts @@ -1,7 +1,7 @@ import type { BytesLike } from '@ethersproject/bytes'; type HexString = string; -import { isBytesLike, arrayify as toBytes, hexlify as toHex, concat, isBytes } from '@ethersproject/bytes'; +import { arrayify as toBytes, hexlify as toHex, concat } from '@ethersproject/bytes'; function compare(a: BytesLike, b: BytesLike): number { const diff = BigInt(toHex(a)) - BigInt(toHex(b)); @@ -9,4 +9,4 @@ function compare(a: BytesLike, b: BytesLike): number { } export type { HexString, BytesLike }; -export { isBytesLike, toBytes, toHex, concat, compare, isBytes }; +export { toBytes, toHex, concat, compare }; diff --git a/src/merkletree.ts b/src/merkletree.ts index 890963a..229edd9 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -21,9 +21,8 @@ export type MerkleTreeData = { values: { value: T; treeIndex: number }[]; }; -export interface MerkleTree { +export interface MerkleTree { root: HexString; - dump(): MerkleTreeData; render(): string; entries(): Iterable<[number, T]>; validate(): void; @@ -34,7 +33,7 @@ export interface MerkleTree { verifyMultiProof(multiproof: MultiProof): boolean; } -export class MerkleTreeImpl implements MerkleTree { +export abstract class MerkleTreeImpl implements MerkleTree { private readonly hashLookup: { [hash: HexString]: number }; protected constructor( @@ -79,13 +78,7 @@ export class MerkleTreeImpl implements MerkleTree { return this.tree[0]!; } - dump(): MerkleTreeData { - return { - format: 'simple-v1', - tree: this.tree, - values: this.values, - }; - } + abstract dump(): MerkleTreeData; render() { return renderMerkleTree(this.tree); diff --git a/src/simple.ts b/src/simple.ts index db83d02..d08c92d 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -5,17 +5,21 @@ import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; import { throwError } from './utils/throw-error'; +export type StandardMerkleTreeData = MerkleTreeData & { + format: 'simple-v1'; +}; + export function formatLeaf(value: BytesLike): HexString { return defaultAbiCoder.encode(['bytes32'], [value]); } export class SimpleMerkleTree extends MerkleTreeImpl { - static of = (values: BytesLike[], options: MerkleTreeOptions = {}): SimpleMerkleTree => { + static of(values: BytesLike[], options: MerkleTreeOptions = {}): SimpleMerkleTree { const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, formatLeaf); return new SimpleMerkleTree(tree, indexedValues, formatLeaf); - }; + } - static load(data: MerkleTreeData): SimpleMerkleTree { + static load(data: StandardMerkleTreeData): SimpleMerkleTree { if (data.format !== 'simple-v1') { throwError(`Unknown format '${data.format}'`); } @@ -29,4 +33,12 @@ export class SimpleMerkleTree extends MerkleTreeImpl { static verifyMultiProof(root: BytesLike, multiproof: MultiProof): boolean { return toHex(root) === processMultiProof(multiproof); } + + dump(): StandardMerkleTreeData { + return { + format: 'simple-v1', + tree: this.tree, + values: this.values, + }; + } } diff --git a/src/standard.test.ts b/src/standard.test.ts index 01abf16..505ed65 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -69,11 +69,6 @@ describe('standard merkle tree', () => { assert(tree.verifyMultiProof(proof1)); assert(StandardMerkleTree.verifyMultiProof(tree.root, ['string'], proof1)); - // assert( - // StandardMerkleTree.verifyMultiProof(tree.root, proof1, { - // leafEncoding: ["string"], - // }) - // ); } }); @@ -82,11 +77,6 @@ describe('standard merkle tree', () => { assert(!tree.verifyMultiProof(multiProof)); assert(!StandardMerkleTree.verifyMultiProof(tree.root, ['string'], multiProof)); - // assert( - // !StandardMerkleTree.verifyMultiProof(tree.root, multiProof, { - // leafEncoding: ["string"], - // }) - // ); }); it('renders tree representation', () => { diff --git a/src/standard.ts b/src/standard.ts index 681c1a3..d3556e4 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -7,6 +7,7 @@ import { MerkleTreeOptions } from './options'; import { throwError } from './utils/throw-error'; export type StandardMerkleTreeData = MerkleTreeData & { + format: 'standard-v1'; leafEncoding: string[]; }; @@ -20,7 +21,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { protected readonly values: StandardMerkleTreeData['values'], protected readonly leafEncoding: string[], ) { - super(tree, values, standardLeafHasher.bind(null, leafEncoding)); + super(tree, values, leaf => standardLeafHasher(leafEncoding, leaf)); } static of( @@ -28,7 +29,9 @@ export class StandardMerkleTree extends MerkleTreeImpl { leafEncoding: string[], options: MerkleTreeOptions = {}, ): StandardMerkleTree { - const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, standardLeafHasher.bind(null, leafEncoding)); + const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, leaf => + standardLeafHasher(leafEncoding, leaf), + ); return new StandardMerkleTree(tree, indexedValues, leafEncoding); } @@ -64,9 +67,9 @@ export class StandardMerkleTree extends MerkleTreeImpl { dump(): StandardMerkleTreeData { return { format: 'standard-v1', + leafEncoding: this.leafEncoding, tree: this.tree, values: this.values, - leafEncoding: this.leafEncoding, }; } } From 826ffabc5843a06397545306b474c6a7fa1d42ab Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 25 Feb 2024 17:11:32 -0600 Subject: [PATCH 13/36] Unify all error interfaces --- src/core.ts | 14 +++++++------- src/standard.ts | 2 +- src/utils/check-bounds.ts | 4 +++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/core.ts b/src/core.ts index 1a1cb4a..64e4672 100644 --- a/src/core.ts +++ b/src/core.ts @@ -22,7 +22,7 @@ export function makeMerkleTree(leaves: BytesLike[]): HexString[] { leaves.forEach(checkValidMerkleNode); if (leaves.length === 0) { - throw new Error('Expected non-zero number of leaves'); + throwError('Expected non-zero number of leaves'); } const tree = new Array(2 * leaves.length - 1); @@ -66,7 +66,7 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof< indices.sort((a, b) => b - a); if (indices.slice(1).some((i, p) => i === indices[p])) { - throw new Error('Cannot prove duplicated index'); + throwError('Cannot prove duplicated index'); } const stack = indices.concat(); // copy @@ -104,11 +104,11 @@ export function processMultiProof(multiproof: MultiProof): HexString multiproof.proof.forEach(checkValidMerkleNode); if (multiproof.proof.length < multiproof.proofFlags.filter(b => !b).length) { - throw new Error('Invalid multiproof format'); + throwError('Invalid multiproof format'); } if (multiproof.leaves.length + multiproof.proof.length !== multiproof.proofFlags.length + 1) { - throw new Error('Provided leaves and multiproof are not compatible'); + throwError('Provided leaves and multiproof are not compatible'); } const stack = multiproof.leaves.concat(); // copy @@ -118,13 +118,13 @@ export function processMultiProof(multiproof: MultiProof): HexString const a = stack.shift(); const b = flag ? stack.shift() : proof.shift(); if (a === undefined || b === undefined) { - throw new Error('Broken invariant'); + throwError('Broken invariant'); } stack.push(hashPair(a, b)); } if (stack.length + proof.length !== 1) { - throw new Error('Broken invariant'); + throwError('Broken invariant'); } return toHex(stack.pop() ?? proof.shift()!); @@ -153,7 +153,7 @@ export function isValidMerkleTree(tree: BytesLike[]): boolean { export function renderMerkleTree(tree: BytesLike[]): HexString { if (tree.length === 0) { - throw new Error('Expected non-zero number of nodes'); + throwError('Expected non-zero number of nodes'); } const stack: [number, number[]][] = [[0, []]]; diff --git a/src/standard.ts b/src/standard.ts index d3556e4..c9b42d1 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -37,7 +37,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { static load(data: StandardMerkleTreeData): StandardMerkleTree { if (data.format !== 'standard-v1') { - throw new Error(`Unknown format '${data.format}'`); + throwError(`Unknown format '${data.format}'`); } if (data.leafEncoding === undefined) { throwError('Expected leaf encoding'); diff --git a/src/utils/check-bounds.ts b/src/utils/check-bounds.ts index 70d3c84..5834b51 100644 --- a/src/utils/check-bounds.ts +++ b/src/utils/check-bounds.ts @@ -1,5 +1,7 @@ +import { throwError } from './throw-error'; + export function checkBounds(array: unknown[], index: number) { if (index < 0 || index >= array.length) { - throw new Error('Index out of bounds'); + throwError('Index out of bounds'); } } From ada28f1bb405ca688f1a9787a58cf8e5fb870669 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 25 Feb 2024 17:57:12 -0600 Subject: [PATCH 14/36] Unify errors --- src/core.test.ts | 8 ++++---- src/core.ts | 37 ++++++++++++++----------------------- src/merkletree.ts | 27 ++++++++++----------------- src/simple.test.ts | 12 ++++++------ src/simple.ts | 6 ++---- src/standard.test.ts | 12 ++++++------ src/standard.ts | 10 +++------- src/utils/check-bounds.ts | 7 ------- src/utils/errors.ts | 29 +++++++++++++++++++++++++++++ src/utils/throw-error.ts | 3 --- 10 files changed, 74 insertions(+), 77 deletions(-) delete mode 100644 src/utils/check-bounds.ts create mode 100644 src/utils/errors.ts delete mode 100644 src/utils/throw-error.ts diff --git a/src/core.test.ts b/src/core.test.ts index 8822a46..c2059d7 100644 --- a/src/core.test.ts +++ b/src/core.test.ts @@ -55,12 +55,12 @@ describe('core properties', () => { describe('core error conditions', () => { it('zero leaves', () => { - assert.throws(() => makeMerkleTree([]), /^Error: Expected non-zero number of leaves$/); + assert.throws(() => makeMerkleTree([]), /^InvalidArgumentError: Expected non-zero number of leaves$/); }); it('multiproof duplicate index', () => { const tree = makeMerkleTree(new Array(2).fill(zero)); - assert.throws(() => getMultiProof(tree, [1, 1]), /^Error: Cannot prove duplicated index$/); + assert.throws(() => getMultiProof(tree, [1, 1]), /^InvalidArgumentError: Cannot prove duplicated index$/); }); it('tree validity', () => { @@ -68,7 +68,7 @@ describe('core error conditions', () => { assert(!isValidMerkleTree([zero, zero]), 'even number of nodes'); assert(!isValidMerkleTree([zero, zero, zero]), 'inner node not hash of children'); - assert.throws(() => renderMerkleTree([]), /^Error: Expected non-zero number of nodes$/); + assert.throws(() => renderMerkleTree([]), /^InvalidArgumentError: Expected non-zero number of nodes$/); }); it('multiproof invariants', () => { @@ -81,6 +81,6 @@ describe('core error conditions', () => { proofFlags: [true, true, false], }; - assert.throws(() => processMultiProof(badMultiProof), /^Error: Broken invariant$/); + assert.throws(() => processMultiProof(badMultiProof), /^InvariantError$/); }); }); diff --git a/src/core.ts b/src/core.ts index 64e4672..18d76a9 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,6 +1,6 @@ import { keccak256 } from '@ethersproject/keccak256'; import { BytesLike, HexString, toHex, toBytes, concat, compare } from './bytes'; -import { throwError } from './utils/throw-error'; +import { invariant, throwError, validateArgument } from './utils/errors'; const hashPair = (a: BytesLike, b: BytesLike): HexString => keccak256(concat([a, b].sort(compare))); @@ -21,9 +21,7 @@ const checkValidMerkleNode = (node: BytesLike) => export function makeMerkleTree(leaves: BytesLike[]): HexString[] { leaves.forEach(checkValidMerkleNode); - if (leaves.length === 0) { - throwError('Expected non-zero number of leaves'); - } + validateArgument(leaves.length !== 0, 'Expected non-zero number of leaves'); const tree = new Array(2 * leaves.length - 1); @@ -65,9 +63,7 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof< indices.forEach(i => checkLeafNode(tree, i)); indices.sort((a, b) => b - a); - if (indices.slice(1).some((i, p) => i === indices[p])) { - throwError('Cannot prove duplicated index'); - } + validateArgument(!indices.slice(1).some((i, p) => i === indices[p]), 'Cannot prove duplicated index'); const stack = indices.concat(); // copy const proof = []; @@ -103,13 +99,14 @@ export function processMultiProof(multiproof: MultiProof): HexString multiproof.leaves.forEach(checkValidMerkleNode); multiproof.proof.forEach(checkValidMerkleNode); - if (multiproof.proof.length < multiproof.proofFlags.filter(b => !b).length) { - throwError('Invalid multiproof format'); - } - - if (multiproof.leaves.length + multiproof.proof.length !== multiproof.proofFlags.length + 1) { - throwError('Provided leaves and multiproof are not compatible'); - } + validateArgument( + multiproof.proof.length >= multiproof.proofFlags.filter(b => !b).length, + 'Invalid multiproof format', + ); + validateArgument( + multiproof.leaves.length + multiproof.proof.length === multiproof.proofFlags.length + 1, + 'Provided leaves and multiproof are not compatible', + ); const stack = multiproof.leaves.concat(); // copy const proof = multiproof.proof.concat(); // copy @@ -117,15 +114,11 @@ export function processMultiProof(multiproof: MultiProof): HexString for (const flag of multiproof.proofFlags) { const a = stack.shift(); const b = flag ? stack.shift() : proof.shift(); - if (a === undefined || b === undefined) { - throwError('Broken invariant'); - } + invariant(a !== undefined && b !== undefined); stack.push(hashPair(a, b)); } - if (stack.length + proof.length !== 1) { - throwError('Broken invariant'); - } + invariant(stack.length + proof.length === 1); return toHex(stack.pop() ?? proof.shift()!); } @@ -152,9 +145,7 @@ export function isValidMerkleTree(tree: BytesLike[]): boolean { } export function renderMerkleTree(tree: BytesLike[]): HexString { - if (tree.length === 0) { - throwError('Expected non-zero number of nodes'); - } + validateArgument(tree.length !== 0, 'Expected non-zero number of nodes'); const stack: [number, number[]][] = [[0, []]]; diff --git a/src/merkletree.ts b/src/merkletree.ts index 229edd9..0f26cf8 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -12,8 +12,7 @@ import { } from './core'; import { MerkleTreeOptions, defaultOptions } from './options'; -import { checkBounds } from './utils/check-bounds'; -import { throwError } from './utils/throw-error'; +import { invariant } from './utils/errors'; export type MerkleTreeData = { format: string; @@ -94,13 +93,13 @@ export abstract class MerkleTreeImpl implements MerkleTree { for (let i = 0; i < this.values.length; i++) { this.validateValue(i); } - if (!isValidMerkleTree(this.tree)) { - throwError('Merkle tree is invalid'); - } + invariant(isValidMerkleTree(this.tree), 'Merkle tree is invalid'); } leafLookup(leaf: T): number { - return this.hashLookup[toHex(this.leafHash(leaf))] ?? throwError('Leaf is not in tree'); + const lookup = this.hashLookup[toHex(this.leafHash(leaf))]; + invariant(typeof lookup !== 'undefined', 'Leaf is not in tree'); + return lookup; } getProof(leaf: number | T): HexString[] { @@ -113,9 +112,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { const proof = getProof(this.tree, treeIndex); // sanity check proof - if (!this._verify(this.tree[treeIndex]!, proof)) { - throwError('Unable to prove value'); - } + invariant(this._verify(this.tree[treeIndex]!, proof), 'Unable to prove value'); // return proof in hex format return proof; @@ -131,9 +128,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { const proof = getMultiProof(this.tree, indices); // sanity check proof - if (!this._verifyMultiProof(proof)) { - throwError('Unable to prove values'); - } + invariant(this._verifyMultiProof(proof), 'Unable to prove values'); // return multiproof in hex format return { @@ -156,13 +151,11 @@ export abstract class MerkleTreeImpl implements MerkleTree { } protected validateValue(valueIndex: number): HexString { - checkBounds(this.values, valueIndex); + invariant(valueIndex >= 0 && valueIndex < this.values.length, 'Index out of bounds'); const { value: leaf, treeIndex } = this.values[valueIndex]!; - checkBounds(this.tree, treeIndex); + invariant(valueIndex >= 0 && valueIndex < this.values.length, 'Index out of bounds'); const hashedLeaf = this.leafHash(leaf); - if (hashedLeaf !== this.tree[treeIndex]!) { - throwError('Merkle tree does not contain the expected value'); - } + invariant(hashedLeaf === this.tree[treeIndex], 'Merkle tree does not contain the expected value'); return hashedLeaf; } diff --git a/src/simple.test.ts b/src/simple.test.ts index 1cb62c5..4a3e431 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -96,14 +96,14 @@ describe('simple merkle tree', () => { }); it('reject out of bounds value index', () => { - assert.throws(() => tree.getProof(leaves.length), /^Error: Index out of bounds$/); + assert.throws(() => tree.getProof(leaves.length), /^InvariantError: Index out of bounds$/); }); it('reject invalid leaf size', () => { const invalidLeaf = [zero + '00']; // 33 bytes (all zero) assert.throws( () => SimpleMerkleTree.of(invalidLeaf, opts), - `Error: ${invalidLeaf} is not a valid 32 bytes object (pos: 0)`, + `InvalidArgumentError: ${invalidLeaf} is not a valid 32 bytes object (pos: 0)`, ); }); }); @@ -113,12 +113,12 @@ describe('simple merkle tree', () => { it('reject unrecognized tree dump', () => { assert.throws( () => SimpleMerkleTree.load({ format: 'nonstandard' } as any), - /^Error: Unknown format 'nonstandard'$/, + /^InvalidArgumentError: Unknown format 'nonstandard'$/, ); assert.throws( () => SimpleMerkleTree.load({ format: 'standard-v1' } as any), - /^Error: Unknown format 'standard-v1'$/, + /^InvalidArgumentError: Unknown format 'standard-v1'$/, ); }); @@ -133,14 +133,14 @@ describe('simple merkle tree', () => { }, ], }); - assert.throws(() => loadedTree1.getProof(0), /^Error: Merkle tree does not contain the expected value$/); + assert.throws(() => loadedTree1.getProof(0), /^InvariantError: Merkle tree does not contain the expected value$/); const loadedTree2 = SimpleMerkleTree.load({ format: 'simple-v1', tree: [zero, zero, zero], values: [{ value: zero, treeIndex: 2 }], }); - assert.throws(() => loadedTree2.getProof(0), /^Error: Unable to prove value$/); + assert.throws(() => loadedTree2.getProof(0), /^InvariantError: Unable to prove value$/); }); }); }); diff --git a/src/simple.ts b/src/simple.ts index d08c92d..b228fe5 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -3,7 +3,7 @@ import { BytesLike, HexString, toHex } from './bytes'; import { MultiProof, processProof, processMultiProof } from './core'; import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; -import { throwError } from './utils/throw-error'; +import { validateArgument } from './utils/errors'; export type StandardMerkleTreeData = MerkleTreeData & { format: 'simple-v1'; @@ -20,9 +20,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { } static load(data: StandardMerkleTreeData): SimpleMerkleTree { - if (data.format !== 'simple-v1') { - throwError(`Unknown format '${data.format}'`); - } + validateArgument(data.format === 'simple-v1', `Unknown format '${data.format}'`); return new SimpleMerkleTree(data.tree, data.values, formatLeaf); } diff --git a/src/standard.test.ts b/src/standard.test.ts index 505ed65..b1409a0 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -31,7 +31,7 @@ describe('standard merkle tree', () => { tree: [zero], values: [{ value: ['0'], treeIndex: 0 }], } as StandardMerkleTreeData<[string]>), - /^Error: Expected leaf encoding$/, + /^InvalidArgumentError: Expected leaf encoding$/, ); }); @@ -125,7 +125,7 @@ describe('standard merkle tree', () => { }); it('reject out of bounds value index', () => { - assert.throws(() => tree.getProof(leaves.length), /^Error: Index out of bounds$/); + assert.throws(() => tree.getProof(leaves.length), /^InvariantError: Index out of bounds$/); }); }); } @@ -138,7 +138,7 @@ describe('standard merkle tree', () => { format: 'nonstandard', leafEncoding: ['string'], } as any), - /^Error: Unknown format 'nonstandard'$/, + /^InvalidArgumentError: Unknown format 'nonstandard'$/, ); assert.throws( @@ -147,7 +147,7 @@ describe('standard merkle tree', () => { format: 'simple-v1', leafEncoding: ['string'], } as any), - /^Error: Unknown format 'simple-v1'$/, + /^InvalidArgumentError: Unknown format 'simple-v1'$/, ); }); @@ -158,7 +158,7 @@ describe('standard merkle tree', () => { values: [{ value: ['0'], treeIndex: 0 }], leafEncoding: ['uint256'], }); - assert.throws(() => loadedTree1.getProof(0), /^Error: Merkle tree does not contain the expected value$/); + assert.throws(() => loadedTree1.getProof(0), /^InvariantError: Merkle tree does not contain the expected value$/); const loadedTree2 = StandardMerkleTree.load({ format: 'standard-v1', @@ -166,7 +166,7 @@ describe('standard merkle tree', () => { values: [{ value: ['0'], treeIndex: 2 }], leafEncoding: ['uint256'], }); - assert.throws(() => loadedTree2.getProof(0), /^Error: Unable to prove value$/); + assert.throws(() => loadedTree2.getProof(0), /^InvariantError: Unable to prove value$/); }); }); }); diff --git a/src/standard.ts b/src/standard.ts index c9b42d1..30c33cc 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -4,7 +4,7 @@ import { BytesLike, HexString, toHex } from './bytes'; import { MultiProof, processProof, processMultiProof } from './core'; import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; -import { throwError } from './utils/throw-error'; +import { validateArgument } from './utils/errors'; export type StandardMerkleTreeData = MerkleTreeData & { format: 'standard-v1'; @@ -36,12 +36,8 @@ export class StandardMerkleTree extends MerkleTreeImpl { } static load(data: StandardMerkleTreeData): StandardMerkleTree { - if (data.format !== 'standard-v1') { - throwError(`Unknown format '${data.format}'`); - } - if (data.leafEncoding === undefined) { - throwError('Expected leaf encoding'); - } + validateArgument(data.format === 'standard-v1', `Unknown format '${data.format}'`); + validateArgument(data.leafEncoding !== undefined, 'Expected leaf encoding'); return new StandardMerkleTree(data.tree, data.values, data.leafEncoding); } diff --git a/src/utils/check-bounds.ts b/src/utils/check-bounds.ts deleted file mode 100644 index 5834b51..0000000 --- a/src/utils/check-bounds.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { throwError } from './throw-error'; - -export function checkBounds(array: unknown[], index: number) { - if (index < 0 || index >= array.length) { - throwError('Index out of bounds'); - } -} diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..ee76786 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,29 @@ +export function throwError(message?: string): never { + throw new Error(message); +} + +export class InvariantError extends Error { + constructor(message?: string) { + super(message); + this.name = 'InvariantError'; + } +} + +export class InvalidArgumentError extends Error { + constructor(message?: string) { + super(message); + this.name = 'InvalidArgumentError'; + } +} + +export function validateArgument(condition: unknown, message?: string): asserts condition { + if (!condition) { + throw new InvalidArgumentError(message); + } +} + +export function invariant(condition: unknown, message?: string): asserts condition { + if (!condition) { + throw new InvariantError(message); + } +} diff --git a/src/utils/throw-error.ts b/src/utils/throw-error.ts deleted file mode 100644 index 503ccf6..0000000 --- a/src/utils/throw-error.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function throwError(message?: string): never { - throw new Error(message); -} From b73e3c41f71b88d271f03116074ff37e75caf76c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 10:28:28 +0100 Subject: [PATCH 15/36] fix naming --- src/simple.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/simple.ts b/src/simple.ts index b228fe5..870c3f2 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -5,7 +5,7 @@ import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; import { validateArgument } from './utils/errors'; -export type StandardMerkleTreeData = MerkleTreeData & { +export type SimpleMerkleTreeData = MerkleTreeData & { format: 'simple-v1'; }; @@ -19,7 +19,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { return new SimpleMerkleTree(tree, indexedValues, formatLeaf); } - static load(data: StandardMerkleTreeData): SimpleMerkleTree { + static load(data: SimpleMerkleTreeData): SimpleMerkleTree { validateArgument(data.format === 'simple-v1', `Unknown format '${data.format}'`); return new SimpleMerkleTree(data.tree, data.values, formatLeaf); } @@ -32,7 +32,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { return toHex(root) === processMultiProof(multiproof); } - dump(): StandardMerkleTreeData { + dump(): SimpleMerkleTreeData { return { format: 'simple-v1', tree: this.tree, From 72057f35580e6992a3fa272cd9eed91aff873738 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 10:56:54 +0100 Subject: [PATCH 16/36] Update src/merkletree.ts --- src/merkletree.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/merkletree.ts b/src/merkletree.ts index 0f26cf8..306ebc1 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -30,6 +30,7 @@ export interface MerkleTree { getMultiProof(leaves: (number | T)[]): MultiProof; verify(leaf: number | T, proof: HexString[]): boolean; verifyMultiProof(multiproof: MultiProof): boolean; + dump(): MerkleTreeData; } export abstract class MerkleTreeImpl implements MerkleTree { From de39370fd6eb704de7f3b68cdc531d6e573e05d5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 11:30:50 +0100 Subject: [PATCH 17/36] rename leafHasher to leafHash, and make mark it as public --- src/merkletree.ts | 54 ++++++++++++++++++++------------------------ src/simple.test.ts | 2 +- src/standard.test.ts | 2 +- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 306ebc1..f54ebc8 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -1,4 +1,4 @@ -import { BytesLike, HexString, toHex, compare } from './bytes'; +import { BytesLike, HexString, compare } from './bytes'; import { MultiProof, @@ -12,7 +12,7 @@ import { } from './core'; import { MerkleTreeOptions, defaultOptions } from './options'; -import { invariant } from './utils/errors'; +import { validateArgument, invariant } from './utils/errors'; export type MerkleTreeData = { format: string; @@ -25,6 +25,7 @@ export interface MerkleTree { render(): string; entries(): Iterable<[number, T]>; validate(): void; + leafHash(leaf: T): HexString; leafLookup(leaf: T): number; getProof(leaf: number | T): HexString[]; getMultiProof(leaves: (number | T)[]): MultiProof; @@ -39,7 +40,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { protected constructor( protected readonly tree: HexString[], protected readonly values: MerkleTreeData['values'], - protected readonly leafHasher: (leaf: T) => HexString, + public readonly leafHash: (leaf: T) => HexString, ) { this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree.at(treeIndex), valueIndex])); } @@ -50,12 +51,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { leafHasher: (value: T) => HexString, ): [tree: HexString[], indexedValues: MerkleTreeData['values']] { const sortLeaves = options.sortLeaves ?? defaultOptions.sortLeaves; - - const hashedValues = values.map((value, valueIndex) => ({ - value, - valueIndex, - hash: leafHasher(value), - })); + const hashedValues = values.map((value, valueIndex) => ({ value, valueIndex, hash: leafHasher(value) })); if (sortLeaves) { hashedValues.sort((a, b) => compare(a.hash, b.hash)); @@ -63,10 +59,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { const tree = makeMerkleTree(hashedValues.map(v => v.hash)); - const indexedValues = values.map(value => ({ - value, - treeIndex: 0, - })); + const indexedValues = values.map(value => ({ value, treeIndex: 0 })); for (const [leafIndex, { valueIndex }] of hashedValues.entries()) { indexedValues[valueIndex]!.treeIndex = tree.length - leafIndex - 1; } @@ -91,22 +84,23 @@ export abstract class MerkleTreeImpl implements MerkleTree { } validate() { - for (let i = 0; i < this.values.length; i++) { - this.validateValue(i); - } - invariant(isValidMerkleTree(this.tree), 'Merkle tree is invalid'); + // Check values are correct, and tree is hashed correctly + invariant( + this.values.every((_, i) => this._verifyAndHash(i)) && isValidMerkleTree(this.tree), + 'Merkle tree is invalid', + ); } leafLookup(leaf: T): number { - const lookup = this.hashLookup[toHex(this.leafHash(leaf))]; - invariant(typeof lookup !== 'undefined', 'Leaf is not in tree'); + const lookup = this.hashLookup[this.leafHash(leaf)]; + validateArgument(typeof lookup !== 'undefined', 'Leaf is not in tree'); return lookup; } getProof(leaf: number | T): HexString[] { // input validity const valueIndex = typeof leaf === 'number' ? leaf : this.leafLookup(leaf); - this.validateValue(valueIndex); + this._verifyAndHash(valueIndex); // rebuild tree index and generate proof const { treeIndex } = this.values[valueIndex]!; @@ -122,7 +116,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { getMultiProof(leaves: (number | T)[]): MultiProof { // input validity const valueIndices = leaves.map(leaf => (typeof leaf === 'number' ? leaf : this.leafLookup(leaf))); - for (const valueIndex of valueIndices) this.validateValue(valueIndex); + for (const valueIndex of valueIndices) this._verifyAndHash(valueIndex); // rebuild tree indices and generate proof const indices = valueIndices.map(i => this.values[i]!.treeIndex); @@ -140,31 +134,31 @@ export abstract class MerkleTreeImpl implements MerkleTree { } verify(leaf: number | T, proof: HexString[]): boolean { - return this._verify(this.leafHash(leaf), proof); + return this._verify(this._leafHash(leaf), proof); } verifyMultiProof(multiproof: MultiProof): boolean { return this._verifyMultiProof({ - leaves: multiproof.leaves.map(l => this.leafHash(l)), + leaves: multiproof.leaves.map(l => this._leafHash(l)), proof: multiproof.proof, proofFlags: multiproof.proofFlags, }); } - protected validateValue(valueIndex: number): HexString { - invariant(valueIndex >= 0 && valueIndex < this.values.length, 'Index out of bounds'); - const { value: leaf, treeIndex } = this.values[valueIndex]!; - invariant(valueIndex >= 0 && valueIndex < this.values.length, 'Index out of bounds'); + private _verifyAndHash(index: number): HexString { + validateArgument(index >= 0 && index < this.values.length, 'Index out of bounds'); + const { value: leaf, treeIndex } = this.values[index]!; + invariant(treeIndex >= 0 && treeIndex < this.tree.length, 'Index out of bounds'); const hashedLeaf = this.leafHash(leaf); invariant(hashedLeaf === this.tree[treeIndex], 'Merkle tree does not contain the expected value'); return hashedLeaf; } - protected leafHash(leaf: number | T): HexString { + private _leafHash(leaf: number | T): HexString { if (typeof leaf === 'number') { - return this.validateValue(leaf); + return this._verifyAndHash(leaf); } else { - return this.leafHasher(leaf); + return this.leafHash(leaf); } } diff --git a/src/simple.test.ts b/src/simple.test.ts index 4a3e431..af40c8b 100644 --- a/src/simple.test.ts +++ b/src/simple.test.ts @@ -96,7 +96,7 @@ describe('simple merkle tree', () => { }); it('reject out of bounds value index', () => { - assert.throws(() => tree.getProof(leaves.length), /^InvariantError: Index out of bounds$/); + assert.throws(() => tree.getProof(leaves.length), /^InvalidArgumentError: Index out of bounds$/); }); it('reject invalid leaf size', () => { diff --git a/src/standard.test.ts b/src/standard.test.ts index b1409a0..8294db3 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -125,7 +125,7 @@ describe('standard merkle tree', () => { }); it('reject out of bounds value index', () => { - assert.throws(() => tree.getProof(leaves.length), /^InvariantError: Index out of bounds$/); + assert.throws(() => tree.getProof(leaves.length), /^InvalidArgumentError: Index out of bounds$/); }); }); } From 1be7ece26ccd669b37505954d7aa8c330dcf5495 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 11:40:38 +0100 Subject: [PATCH 18/36] rewrite checks --- src/merkletree.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index f54ebc8..c06a29e 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -146,9 +146,9 @@ export abstract class MerkleTreeImpl implements MerkleTree { } private _verifyAndHash(index: number): HexString { - validateArgument(index >= 0 && index < this.values.length, 'Index out of bounds'); + validateArgument(this.values.at(index) !== undefined, 'Index out of bounds'); const { value: leaf, treeIndex } = this.values[index]!; - invariant(treeIndex >= 0 && treeIndex < this.tree.length, 'Index out of bounds'); + invariant(this.tree.at(treeIndex) !== undefined, 'Index out of bounds'); const hashedLeaf = this.leafHash(leaf); invariant(hashedLeaf === this.tree[treeIndex], 'Merkle tree does not contain the expected value'); return hashedLeaf; From 5e69b25dff3931cdaf01743fbb1eac19d493151f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 11:44:22 +0100 Subject: [PATCH 19/36] remove duplicate test --- src/merkletree.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index c06a29e..05c12b7 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -148,9 +148,8 @@ export abstract class MerkleTreeImpl implements MerkleTree { private _verifyAndHash(index: number): HexString { validateArgument(this.values.at(index) !== undefined, 'Index out of bounds'); const { value: leaf, treeIndex } = this.values[index]!; - invariant(this.tree.at(treeIndex) !== undefined, 'Index out of bounds'); const hashedLeaf = this.leafHash(leaf); - invariant(hashedLeaf === this.tree[treeIndex], 'Merkle tree does not contain the expected value'); + invariant(this.tree.at(treeIndex) === hashedLeaf, 'Merkle tree does not contain the expected value'); return hashedLeaf; } From de68246015becd07b9fd38e4670cf09a014e9e41 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 11:44:43 +0100 Subject: [PATCH 20/36] simplify --- src/merkletree.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 05c12b7..119543f 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -147,8 +147,8 @@ export abstract class MerkleTreeImpl implements MerkleTree { private _verifyAndHash(index: number): HexString { validateArgument(this.values.at(index) !== undefined, 'Index out of bounds'); - const { value: leaf, treeIndex } = this.values[index]!; - const hashedLeaf = this.leafHash(leaf); + const { value, treeIndex } = this.values[index]!; + const hashedLeaf = this.leafHash(value); invariant(this.tree.at(treeIndex) === hashedLeaf, 'Merkle tree does not contain the expected value'); return hashedLeaf; } From e13943e51426d6fdc17a950fb530686959a03027 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 11:48:07 +0100 Subject: [PATCH 21/36] naming consistency --- src/merkletree.ts | 4 ++-- src/standard.test.ts | 2 +- src/standard.ts | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 119543f..972f765 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -48,10 +48,10 @@ export abstract class MerkleTreeImpl implements MerkleTree { protected static prepare( values: T[], options: MerkleTreeOptions = {}, - leafHasher: (value: T) => HexString, + leafHash: (value: T) => HexString, ): [tree: HexString[], indexedValues: MerkleTreeData['values']] { const sortLeaves = options.sortLeaves ?? defaultOptions.sortLeaves; - const hashedValues = values.map((value, valueIndex) => ({ value, valueIndex, hash: leafHasher(value) })); + const hashedValues = values.map((value, valueIndex) => ({ value, valueIndex, hash: leafHash(value) })); if (sortLeaves) { hashedValues.sort((a, b) => compare(a.hash, b.hash)); diff --git a/src/standard.test.ts b/src/standard.test.ts index 8294db3..5729c55 100644 --- a/src/standard.test.ts +++ b/src/standard.test.ts @@ -119,7 +119,7 @@ describe('standard merkle tree', () => { // assert.deepEqual(tree, recoveredTree); for (const [key, value] of Object.entries(tree)) { - if (typeof value === 'function') continue; // LeafHasher is a function that is not reference-equal + if (typeof value === 'function') continue; // leafHash is a function that is not reference-equal assert.deepEqual(value, (recoveredTree as any)[key]); } }); diff --git a/src/standard.ts b/src/standard.ts index 30c33cc..3f03dcc 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -11,7 +11,7 @@ export type StandardMerkleTreeData = MerkleTreeData & { leafEncoding: string[]; }; -export function standardLeafHasher(types: string[], value: T): HexString { +export function standardLeafHash(types: string[], value: T): HexString { return keccak256(keccak256(defaultAbiCoder.encode(types, value))); } @@ -21,7 +21,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { protected readonly values: StandardMerkleTreeData['values'], protected readonly leafEncoding: string[], ) { - super(tree, values, leaf => standardLeafHasher(leafEncoding, leaf)); + super(tree, values, leaf => standardLeafHash(leafEncoding, leaf)); } static of( @@ -30,7 +30,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { options: MerkleTreeOptions = {}, ): StandardMerkleTree { const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, leaf => - standardLeafHasher(leafEncoding, leaf), + standardLeafHash(leafEncoding, leaf), ); return new StandardMerkleTree(tree, indexedValues, leafEncoding); } @@ -42,7 +42,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { } static verify(root: BytesLike, leafEncoding: string[], leaf: T, proof: BytesLike[]): boolean { - return toHex(root) === processProof(standardLeafHasher(leafEncoding, leaf), proof); + return toHex(root) === processProof(standardLeafHash(leafEncoding, leaf), proof); } static verifyMultiProof( @@ -53,7 +53,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { return ( toHex(root) === processMultiProof({ - leaves: multiproof.leaves.map(leaf => standardLeafHasher(leafEncoding, leaf)), + leaves: multiproof.leaves.map(leaf => standardLeafHash(leafEncoding, leaf)), proof: multiproof.proof, proofFlags: multiproof.proofFlags, }) From e5e035e18ea7f771ed09fe7d185769277687e853 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 11:49:27 +0100 Subject: [PATCH 22/36] refactor --- src/merkletree.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 972f765..4041691 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -84,11 +84,8 @@ export abstract class MerkleTreeImpl implements MerkleTree { } validate() { - // Check values are correct, and tree is hashed correctly - invariant( - this.values.every((_, i) => this._verifyAndHash(i)) && isValidMerkleTree(this.tree), - 'Merkle tree is invalid', - ); + this.values.forEach((_, i) => this._verifyAndHash(i)); + invariant(isValidMerkleTree(this.tree), 'Merkle tree is invalid'); } leafLookup(leaf: T): number { From 64b7dfa3abc39b5cf50f5fd882283827d6b4934a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 12:00:55 +0100 Subject: [PATCH 23/36] split value validation and leafHashing --- src/merkletree.ts | 20 +++++++++----------- src/standard.ts | 4 +--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 4041691..9906335 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -83,8 +83,8 @@ export abstract class MerkleTreeImpl implements MerkleTree { } } - validate() { - this.values.forEach((_, i) => this._verifyAndHash(i)); + validate(): void { + this.values.forEach((_, i) => this._validateValueAt(i)); invariant(isValidMerkleTree(this.tree), 'Merkle tree is invalid'); } @@ -97,7 +97,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { getProof(leaf: number | T): HexString[] { // input validity const valueIndex = typeof leaf === 'number' ? leaf : this.leafLookup(leaf); - this._verifyAndHash(valueIndex); + this._validateValueAt(valueIndex); // rebuild tree index and generate proof const { treeIndex } = this.values[valueIndex]!; @@ -113,7 +113,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { getMultiProof(leaves: (number | T)[]): MultiProof { // input validity const valueIndices = leaves.map(leaf => (typeof leaf === 'number' ? leaf : this.leafLookup(leaf))); - for (const valueIndex of valueIndices) this._verifyAndHash(valueIndex); + for (const valueIndex of valueIndices) this._validateValueAt(valueIndex); // rebuild tree indices and generate proof const indices = valueIndices.map(i => this.values[i]!.treeIndex); @@ -142,20 +142,18 @@ export abstract class MerkleTreeImpl implements MerkleTree { }); } - private _verifyAndHash(index: number): HexString { + private _validateValueAt(index: number): void { validateArgument(this.values.at(index) !== undefined, 'Index out of bounds'); const { value, treeIndex } = this.values[index]!; - const hashedLeaf = this.leafHash(value); - invariant(this.tree.at(treeIndex) === hashedLeaf, 'Merkle tree does not contain the expected value'); - return hashedLeaf; + invariant(this.tree.at(treeIndex) === this.leafHash(value), 'Merkle tree does not contain the expected value'); } private _leafHash(leaf: number | T): HexString { if (typeof leaf === 'number') { - return this._verifyAndHash(leaf); - } else { - return this.leafHash(leaf); + validateArgument(this.values.at(leaf) !== undefined, 'Index out of bounds'); + leaf = this.values[leaf]?.value; } + return this.leafHash(leaf); } private _verify(leafHash: BytesLike, proof: BytesLike[]): boolean { diff --git a/src/standard.ts b/src/standard.ts index 3f03dcc..e6740cd 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -29,9 +29,7 @@ export class StandardMerkleTree extends MerkleTreeImpl { leafEncoding: string[], options: MerkleTreeOptions = {}, ): StandardMerkleTree { - const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, leaf => - standardLeafHash(leafEncoding, leaf), - ); + const [tree, indexedValues] = MerkleTreeImpl.prepare(values, options, leaf => standardLeafHash(leafEncoding, leaf)); return new StandardMerkleTree(tree, indexedValues, leafEncoding); } From 5d5deb83df78926513c332084ccadd15660ed4d1 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 12:13:37 +0100 Subject: [PATCH 24/36] deduplicate type definition --- src/merkletree.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 9906335..4ba5e22 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -40,7 +40,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { protected constructor( protected readonly tree: HexString[], protected readonly values: MerkleTreeData['values'], - public readonly leafHash: (leaf: T) => HexString, + public readonly leafHash: MerkleTree['leafHash'] ) { this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree.at(treeIndex), valueIndex])); } @@ -48,7 +48,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { protected static prepare( values: T[], options: MerkleTreeOptions = {}, - leafHash: (value: T) => HexString, + leafHash: MerkleTree['leafHash'], ): [tree: HexString[], indexedValues: MerkleTreeData['values']] { const sortLeaves = options.sortLeaves ?? defaultOptions.sortLeaves; const hashedValues = values.map((value, valueIndex) => ({ value, valueIndex, hash: leafHash(value) })); From 2ce3d55804b5923a34bddf7d6b77ade83795df50 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 12:30:52 +0100 Subject: [PATCH 25/36] specify type --- src/simple.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/simple.ts b/src/simple.ts index 870c3f2..f388cbd 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -5,7 +5,7 @@ import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; import { validateArgument } from './utils/errors'; -export type SimpleMerkleTreeData = MerkleTreeData & { +export type SimpleMerkleTreeData = MerkleTreeData & { format: 'simple-v1'; }; @@ -19,7 +19,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { return new SimpleMerkleTree(tree, indexedValues, formatLeaf); } - static load(data: SimpleMerkleTreeData): SimpleMerkleTree { + static load(data: SimpleMerkleTreeData): SimpleMerkleTree { validateArgument(data.format === 'simple-v1', `Unknown format '${data.format}'`); return new SimpleMerkleTree(data.tree, data.values, formatLeaf); } @@ -32,7 +32,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { return toHex(root) === processMultiProof(multiproof); } - dump(): SimpleMerkleTreeData { + dump(): SimpleMerkleTreeData { return { format: 'simple-v1', tree: this.tree, From 1ad43093cc6cf24f7bbba29f493acec6ef5dc59d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 12:38:21 +0100 Subject: [PATCH 26/36] fix lint --- src/merkletree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 4ba5e22..f695aa5 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -40,7 +40,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { protected constructor( protected readonly tree: HexString[], protected readonly values: MerkleTreeData['values'], - public readonly leafHash: MerkleTree['leafHash'] + public readonly leafHash: MerkleTree['leafHash'], ) { this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree.at(treeIndex), valueIndex])); } From b13748ad085e8597c8fdf2c2de6b983e56db16ac Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 13:54:16 +0100 Subject: [PATCH 27/36] refactor --- src/merkletree.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index f695aa5..3a14c1b 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -42,7 +42,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { protected readonly values: MerkleTreeData['values'], public readonly leafHash: MerkleTree['leafHash'], ) { - this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree.at(treeIndex), valueIndex])); + this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree[treeIndex], valueIndex])); } protected static prepare( @@ -143,15 +143,19 @@ export abstract class MerkleTreeImpl implements MerkleTree { } private _validateValueAt(index: number): void { - validateArgument(this.values.at(index) !== undefined, 'Index out of bounds'); - const { value, treeIndex } = this.values[index]!; - invariant(this.tree.at(treeIndex) === this.leafHash(value), 'Merkle tree does not contain the expected value'); + const value = this.values[index]; + validateArgument(value !== undefined, 'Index out of bounds'); + invariant( + this.tree[value.treeIndex] === this.leafHash(value.value), + 'Merkle tree does not contain the expected value', + ); } private _leafHash(leaf: number | T): HexString { if (typeof leaf === 'number') { - validateArgument(this.values.at(leaf) !== undefined, 'Index out of bounds'); - leaf = this.values[leaf]?.value; + const lookup = this.values[leaf]; + validateArgument(lookup !== undefined, 'Index out of bounds'); + leaf = lookup.value; } return this.leafHash(leaf); } From fd1cf28c083b765c194b5cdd0505c31e31609c57 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 26 Feb 2024 13:24:20 -0600 Subject: [PATCH 28/36] Nits --- src/merkletree.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 3a14c1b..7403100 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -23,6 +23,7 @@ export type MerkleTreeData = { export interface MerkleTree { root: HexString; render(): string; + dump(): MerkleTreeData; entries(): Iterable<[number, T]>; validate(): void; leafHash(leaf: T): HexString; @@ -31,7 +32,6 @@ export interface MerkleTree { getMultiProof(leaves: (number | T)[]): MultiProof; verify(leaf: number | T, proof: HexString[]): boolean; verifyMultiProof(multiproof: MultiProof): boolean; - dump(): MerkleTreeData; } export abstract class MerkleTreeImpl implements MerkleTree { @@ -42,6 +42,10 @@ export abstract class MerkleTreeImpl implements MerkleTree { protected readonly values: MerkleTreeData['values'], public readonly leafHash: MerkleTree['leafHash'], ) { + validateArgument( + values.every(({ value }) => typeof value != 'number'), + 'Leaf values cannot be numbers', + ); this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree[treeIndex], valueIndex])); } From d2b6e9732846d8edefe92a25368ecc5ead998a8c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 23:31:53 +0100 Subject: [PATCH 29/36] Add dumps --- test/dumps.test.ts | 15 ++++++++++++--- test/dumps/simple-v1.sorted.1_0_6.json | 1 + test/dumps/simple-v1.unsorted.1_0_6.json | 1 + test/dumps/standard-v1.sorted.1_0_6 copy.json | 1 + test/dumps/standard-v1.unsorted.1_0_6.json | 1 + 5 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 test/dumps/simple-v1.sorted.1_0_6.json create mode 100644 test/dumps/simple-v1.unsorted.1_0_6.json create mode 100644 test/dumps/standard-v1.sorted.1_0_6 copy.json create mode 100644 test/dumps/standard-v1.unsorted.1_0_6.json diff --git a/test/dumps.test.ts b/test/dumps.test.ts index 08cb2ec..4a3a24c 100644 --- a/test/dumps.test.ts +++ b/test/dumps.test.ts @@ -1,7 +1,7 @@ import assert from 'assert/strict'; import fs from 'fs'; import path from 'path'; -import { StandardMerkleTree } from '../src/standard'; +import { StandardMerkleTree, SimpleMerkleTree } from '../src'; const DUMPS_DIR = 'test/dumps/'; @@ -9,8 +9,17 @@ describe('load dumped trees', () => { for (const file of fs.readdirSync(DUMPS_DIR).map(filename => path.join(DUMPS_DIR, filename))) { it(file, function () { const dump = JSON.parse(fs.readFileSync(file, 'utf-8')); - const tree = StandardMerkleTree.load(dump); - tree.validate(); + + switch (dump.format) { + case 'standard-v1': + StandardMerkleTree.load(dump).validate(); + break; + case 'simple-v1': + SimpleMerkleTree.load(dump).validate(); + break; + default: + assert.fail(`Unknown format '${dump.format}`); + } }); } }); diff --git a/test/dumps/simple-v1.sorted.1_0_6.json b/test/dumps/simple-v1.sorted.1_0_6.json new file mode 100644 index 0000000..db6b897 --- /dev/null +++ b/test/dumps/simple-v1.sorted.1_0_6.json @@ -0,0 +1 @@ +{"format":"simple-v1","tree":["0x1b404f199ea828ec5771fb30139c222d8417a82175fefad5cd42bc3a189bd8d5","0xec554bdfb01d31fa838d0830339b0e6e8a70e0d55a8f172ffa8bebbf8e8d5ba0","0xaf46af0745b433e1d5bed9a04b1fdf4002f67a733c20db2fca5b2af6120d9bcb","0x434d51cfeb80272378f4c3a8fd2824561c2cad9fce556ea600d46f20550976a6","0x7dea550f679f3caab547cbbc5ee1a4c978c8c039b572ba00af1baa6481b88360","0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3","0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483","0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510","0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761","0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb","0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2"],"values":[{"value":"0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb","treeIndex":9},{"value":"0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510","treeIndex":7},{"value":"0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2","treeIndex":10},{"value":"0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3","treeIndex":5},{"value":"0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761","treeIndex":8},{"value":"0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483","treeIndex":6}]} diff --git a/test/dumps/simple-v1.unsorted.1_0_6.json b/test/dumps/simple-v1.unsorted.1_0_6.json new file mode 100644 index 0000000..507cb32 --- /dev/null +++ b/test/dumps/simple-v1.unsorted.1_0_6.json @@ -0,0 +1 @@ +{"format":"simple-v1","tree":["0x9012f1e18a87790d2e01faace75aaaca38e53df437cdce2c0552464dda4af49c","0x68203f90e9d07dc5859259d7536e87a6ba9d345f2552b5b9de2999ddce9ce1bf","0xf0b49bb4b0d9396e0315755ceafaa280707b32e75e6c9053f5cdf2679dcd5c6a","0xd253a52d4cb00de2895e85f2529e2976e6aaaa5c18106b68ab66813e14415669","0x805b21d846b189efaeb0377d6bb0d201b3872a363e607c25088f025b0c6ae1f8","0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483","0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761","0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3","0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2","0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510","0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb"],"values":[{"value":"0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb","treeIndex":10},{"value":"0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510","treeIndex":9},{"value":"0x0b42b6393c1f53060fe3ddbfcd7aadcca894465a5a438f69c87d790b2299b9b2","treeIndex":8},{"value":"0xf1918e8562236eb17adc8502332f4c9c82bc14e19bfc0aa10ab674ff75b3d2f3","treeIndex":7},{"value":"0xa8982c89d80987fb9a510e25981ee9170206be21af3c8e0eb312ef1d3382e761","treeIndex":6},{"value":"0xd1e8aeb79500496ef3dc2e57ba746a8315d048b7a664a2bf948db4fa91960483","treeIndex":5}]} diff --git a/test/dumps/standard-v1.sorted.1_0_6 copy.json b/test/dumps/standard-v1.sorted.1_0_6 copy.json new file mode 100644 index 0000000..18091c2 --- /dev/null +++ b/test/dumps/standard-v1.sorted.1_0_6 copy.json @@ -0,0 +1 @@ +{"format":"standard-v1","leafEncoding":["string"],"tree":["0x6deb52b5da8fd108f79fab00341f38d2587896634c646ee52e49f845680a70c8","0x52426e0f1f65ff7e209a13b8c29cffe82e3acaf3dad0a9b9088f3b9a61a929c3","0xfd3cf45654e88d1cc5d663578c82c76f4b5e3826bacaa1216441443504538f51","0x8076923e76cf01a7c048400a2304c9a9c23bbbdac3a98ea3946340fdafbba34f","0x965b92c6cf08303cc4feb7f3e0819c436c2cec17c6f0688a6af139c9a368707c","0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b","0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848","0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b","0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c","0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e","0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681"],"values":[{"value":["a"],"treeIndex":8},{"value":["b"],"treeIndex":10},{"value":["c"],"treeIndex":7},{"value":["d"],"treeIndex":5},{"value":["e"],"treeIndex":9},{"value":["f"],"treeIndex":6}]} diff --git a/test/dumps/standard-v1.unsorted.1_0_6.json b/test/dumps/standard-v1.unsorted.1_0_6.json new file mode 100644 index 0000000..602a600 --- /dev/null +++ b/test/dumps/standard-v1.unsorted.1_0_6.json @@ -0,0 +1 @@ +{"format":"standard-v1","leafEncoding":["string"],"tree":["0x23be0977360f08bb0bd7f709a7d543d2cd779c79c66d74e0441919871647de2b","0x8f7234e8cfe39c08ca84a3a3e3274f574af26fd15165fe29e09cbab742daccd9","0x7b0c6cd04b82bfc0e250030a5d2690c52585e0cc6a4f3bc7909d7723b0236ece","0x03707d7802a71ca56a8ad8028da98c4f1dbec55b31b4a25d536b5309cc20eda9","0xfa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016","0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848","0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e","0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b","0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b","0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681","0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c"],"values":[{"value":["a"],"treeIndex":10},{"value":["b"],"treeIndex":9},{"value":["c"],"treeIndex":8},{"value":["d"],"treeIndex":7},{"value":["e"],"treeIndex":6},{"value":["f"],"treeIndex":5}]} From 84e96d5b5594c96a1950d7aca1eb9d9d10894978 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 23:33:08 +0100 Subject: [PATCH 30/36] fix name --- ...rd-v1.sorted.1_0_6 copy.json => standard-v1.sorted.1_0_6.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/dumps/{standard-v1.sorted.1_0_6 copy.json => standard-v1.sorted.1_0_6.json} (100%) diff --git a/test/dumps/standard-v1.sorted.1_0_6 copy.json b/test/dumps/standard-v1.sorted.1_0_6.json similarity index 100% rename from test/dumps/standard-v1.sorted.1_0_6 copy.json rename to test/dumps/standard-v1.sorted.1_0_6.json From 640e37857233ab4d4ce61660e1d1894286e095c0 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 23:34:38 +0100 Subject: [PATCH 31/36] remove duplicate --- test/dumps/standard-v1.1_0_6.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test/dumps/standard-v1.1_0_6.json diff --git a/test/dumps/standard-v1.1_0_6.json b/test/dumps/standard-v1.1_0_6.json deleted file mode 100644 index 223302e..0000000 --- a/test/dumps/standard-v1.1_0_6.json +++ /dev/null @@ -1 +0,0 @@ -{"format":"standard-v1","tree":["0x23be0977360f08bb0bd7f709a7d543d2cd779c79c66d74e0441919871647de2b","0x8f7234e8cfe39c08ca84a3a3e3274f574af26fd15165fe29e09cbab742daccd9","0x7b0c6cd04b82bfc0e250030a5d2690c52585e0cc6a4f3bc7909d7723b0236ece","0x03707d7802a71ca56a8ad8028da98c4f1dbec55b31b4a25d536b5309cc20eda9","0xfa914d99a18dc32d9725b3ef1c50426deb40ec8d0885dac8edcc5bfd6d030016","0xc62a8cfa41edc0ef6f6ae27a2985b7d39c7fea770787d7e104696c6e81f64848","0x9a4f64e953595df82d1b4f570d34c4f4f0cfaf729a61e9d60e83e579e1aa283e","0xeba909cf4bb90c6922771d7f126ad0fd11dfde93f3937a196274e1ac20fd2f5b","0x9cf5a63718145ba968a01c1d557020181c5b252f665cf7386d370eddb176517b","0x19ba6c6333e0e9a15bf67523e0676e2f23eb8e574092552d5e888c64a4bb3681","0x9c15a6a0eaeed500fd9eed4cbeab71f797cefcc67bfd46683e4d2e6ff7f06d1c"],"values":[{"value":["a"],"treeIndex":10},{"value":["b"],"treeIndex":9},{"value":["c"],"treeIndex":8},{"value":["d"],"treeIndex":7},{"value":["e"],"treeIndex":6},{"value":["f"],"treeIndex":5}],"leafEncoding":["string"]} From 657829315a4667cc952dc61744ee5daed1a04748 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 26 Feb 2024 23:36:08 +0100 Subject: [PATCH 32/36] up --- test/dumps.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dumps.test.ts b/test/dumps.test.ts index 4a3a24c..5a63381 100644 --- a/test/dumps.test.ts +++ b/test/dumps.test.ts @@ -18,7 +18,7 @@ describe('load dumped trees', () => { SimpleMerkleTree.load(dump).validate(); break; default: - assert.fail(`Unknown format '${dump.format}`); + assert.fail(`Unknown format '${dump.format}'`); } }); } From 1b587e8a1d65500a9bd458fb86f010636ff6a434 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 26 Feb 2024 18:11:47 -0600 Subject: [PATCH 33/36] Exclude number type support from MerkleTree --- src/merkletree.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/merkletree.ts b/src/merkletree.ts index 7403100..8b52d79 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -14,13 +14,13 @@ import { import { MerkleTreeOptions, defaultOptions } from './options'; import { validateArgument, invariant } from './utils/errors'; -export type MerkleTreeData = { +export type MerkleTreeData> = { format: string; tree: HexString[]; values: { value: T; treeIndex: number }[]; }; -export interface MerkleTree { +export interface MerkleTree> { root: HexString; render(): string; dump(): MerkleTreeData; @@ -34,7 +34,7 @@ export interface MerkleTree { verifyMultiProof(multiproof: MultiProof): boolean; } -export abstract class MerkleTreeImpl implements MerkleTree { +export abstract class MerkleTreeImpl> implements MerkleTree { private readonly hashLookup: { [hash: HexString]: number }; protected constructor( @@ -49,7 +49,7 @@ export abstract class MerkleTreeImpl implements MerkleTree { this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree[treeIndex], valueIndex])); } - protected static prepare( + protected static prepare>( values: T[], options: MerkleTreeOptions = {}, leafHash: MerkleTree['leafHash'], From a0e8b20bf48b5b79c85c32504bc9eb3cb3ea6488 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 Feb 2024 09:38:52 +0100 Subject: [PATCH 34/36] Address comments from @frangio --- src/core.ts | 16 ++++++++-------- src/merkletree.ts | 8 ++++---- src/simple.ts | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/core.ts b/src/core.ts index 18d76a9..e6eb56c 100644 --- a/src/core.ts +++ b/src/core.ts @@ -40,10 +40,10 @@ export function getProof(tree: BytesLike[], index: number): HexString[] { const proof = []; while (index > 0) { - proof.push(tree[siblingIndex(index)]!); + proof.push(toHex(tree[siblingIndex(index)]!)); index = parentIndex(index); } - return proof.map(node => toHex(node)); + return proof; } export function processProof(leaf: BytesLike, proof: BytesLike[]): HexString { @@ -63,7 +63,7 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof< indices.forEach(i => checkLeafNode(tree, i)); indices.sort((a, b) => b - a); - validateArgument(!indices.slice(1).some((i, p) => i === indices[p]), 'Cannot prove duplicated index'); + validateArgument(indices.slice(1).every((i, p) => i !== indices[p]), 'Cannot prove duplicated index'); const stack = indices.concat(); // copy const proof = []; @@ -79,18 +79,18 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof< stack.shift(); // consume from the stack } else { proofFlags.push(false); - proof.push(tree[s]!); + proof.push(toHex(tree[s]!)); } stack.push(p); } if (indices.length === 0) { - proof.push(tree[0]!); + proof.push(toHex(tree[0]!)); } return { - leaves: indices.map(i => tree[i]!).map(node => toHex(node)), - proof: proof.map(node => toHex(node)), + leaves: indices.map(i => toHex(tree[i]!)), + proof, proofFlags, }; } @@ -136,7 +136,7 @@ export function isValidMerkleTree(tree: BytesLike[]): boolean { if (l < tree.length) { return false; } - } else if (node !== hashPair(tree[l]!, tree[r]!)) { + } else if (compare(node, hashPair(tree[l]!, tree[r]!))) { return false; } } diff --git a/src/merkletree.ts b/src/merkletree.ts index 8b52d79..c76857b 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -14,13 +14,13 @@ import { import { MerkleTreeOptions, defaultOptions } from './options'; import { validateArgument, invariant } from './utils/errors'; -export type MerkleTreeData> = { +export interface MerkleTreeData { format: string; tree: HexString[]; values: { value: T; treeIndex: number }[]; }; -export interface MerkleTree> { +export interface MerkleTree { root: HexString; render(): string; dump(): MerkleTreeData; @@ -34,7 +34,7 @@ export interface MerkleTree> { verifyMultiProof(multiproof: MultiProof): boolean; } -export abstract class MerkleTreeImpl> implements MerkleTree { +export abstract class MerkleTreeImpl implements MerkleTree { private readonly hashLookup: { [hash: HexString]: number }; protected constructor( @@ -49,7 +49,7 @@ export abstract class MerkleTreeImpl> implements this.hashLookup = Object.fromEntries(values.map(({ treeIndex }, valueIndex) => [tree[treeIndex], valueIndex])); } - protected static prepare>( + protected static prepare( values: T[], options: MerkleTreeOptions = {}, leafHash: MerkleTree['leafHash'], diff --git a/src/simple.ts b/src/simple.ts index f388cbd..4698971 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -5,7 +5,7 @@ import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; import { validateArgument } from './utils/errors'; -export type SimpleMerkleTreeData = MerkleTreeData & { +export type SimpleMerkleTreeData = MerkleTreeData & { format: 'simple-v1'; }; @@ -36,7 +36,7 @@ export class SimpleMerkleTree extends MerkleTreeImpl { return { format: 'simple-v1', tree: this.tree, - values: this.values, + values: this.values.map(({ value, treeIndex }) => ({ value: toHex(value), treeIndex })), }; } } From f48455e6314fd3b4676d444a58b8f4e3dab05f54 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 Feb 2024 09:39:33 +0100 Subject: [PATCH 35/36] fix lint --- src/core.ts | 5 ++++- src/merkletree.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core.ts b/src/core.ts index e6eb56c..b523ba7 100644 --- a/src/core.ts +++ b/src/core.ts @@ -63,7 +63,10 @@ export function getMultiProof(tree: BytesLike[], indices: number[]): MultiProof< indices.forEach(i => checkLeafNode(tree, i)); indices.sort((a, b) => b - a); - validateArgument(indices.slice(1).every((i, p) => i !== indices[p]), 'Cannot prove duplicated index'); + validateArgument( + indices.slice(1).every((i, p) => i !== indices[p]), + 'Cannot prove duplicated index', + ); const stack = indices.concat(); // copy const proof = []; diff --git a/src/merkletree.ts b/src/merkletree.ts index c76857b..5d79c5e 100644 --- a/src/merkletree.ts +++ b/src/merkletree.ts @@ -18,7 +18,7 @@ export interface MerkleTreeData { format: string; tree: HexString[]; values: { value: T; treeIndex: number }[]; -}; +} export interface MerkleTree { root: HexString; From 5d1ceb7475bfbbf72ad97f9c40d625dcaea1d522 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 Feb 2024 09:42:25 +0100 Subject: [PATCH 36/36] =?UTF-8?q?types=20=E2=86=92=20interfaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/simple.ts | 4 ++-- src/standard.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/simple.ts b/src/simple.ts index 4698971..94f87e8 100644 --- a/src/simple.ts +++ b/src/simple.ts @@ -5,9 +5,9 @@ import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; import { validateArgument } from './utils/errors'; -export type SimpleMerkleTreeData = MerkleTreeData & { +export interface SimpleMerkleTreeData extends MerkleTreeData { format: 'simple-v1'; -}; +} export function formatLeaf(value: BytesLike): HexString { return defaultAbiCoder.encode(['bytes32'], [value]); diff --git a/src/standard.ts b/src/standard.ts index e6740cd..e15ef77 100644 --- a/src/standard.ts +++ b/src/standard.ts @@ -6,10 +6,10 @@ import { MerkleTreeData, MerkleTreeImpl } from './merkletree'; import { MerkleTreeOptions } from './options'; import { validateArgument } from './utils/errors'; -export type StandardMerkleTreeData = MerkleTreeData & { +export interface StandardMerkleTreeData extends MerkleTreeData { format: 'standard-v1'; leafEncoding: string[]; -}; +} export function standardLeafHash(types: string[], value: T): HexString { return keccak256(keccak256(defaultAbiCoder.encode(types, value)));