diff --git a/.cspell.json b/.cspell.json index 1b262f3a0d..da0e8d0f65 100644 --- a/.cspell.json +++ b/.cspell.json @@ -7,6 +7,8 @@ "Addrs", "ahadns", "Alives", + "alphabeta", + "Arraylike", "asym", "autoshard", "autosharding", @@ -17,9 +19,11 @@ "bufbuild", "chainsafe", "cimg", + "cipherparams", "ciphertext", "circleci", "codecov", + "codegen", "commitlint", "dependabot", "dialable", @@ -57,6 +61,7 @@ "iwant", "jdev", "jswaku", + "kdfparams", "keccak", "keypair", "lastpub", @@ -87,6 +92,7 @@ "proto", "protobuf", "protoc", + "proxiable", "reactjs", "recid", "rlnrelay", @@ -116,16 +122,18 @@ "upgrader", "vacp", "varint", - "weboko", + "vkey", "waku", "wakuconnect", "wakunode", "wakuorg", "wakuv", "webfonts", + "weboko", "websockets", "wifi", "xsalsa20", + "zerokit", "Привет", "مرحبا" ], diff --git a/package-lock.json b/package-lock.json index 8915a33e9d..25f3e890e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,12 @@ "packages/browser-tests", "packages/build-utils", "packages/react-native-polyfills", - "packages/sds" + "packages/sds", + "packages/rln" ], + "dependencies": { + "@waku/utils": "^0.0.21" + }, "devDependencies": { "@size-limit/preset-big-lib": "^11.0.2", "@typescript-eslint/eslint-plugin": "^6.6.0", @@ -3721,6 +3725,164 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@ethersproject/abi": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", + "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@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" + } + }, + "node_modules/@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==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@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" + } + }, + "node_modules/@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==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@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" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", + "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@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" + } + }, + "node_modules/@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==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.7.0.tgz", + "integrity": "sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/properties": "^5.7.0" + } + }, + "node_modules/@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==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "bn.js": "^5.2.1" + } + }, "node_modules/@ethersproject/bytes": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", @@ -3740,6 +3902,173 @@ "@ethersproject/logger": "^5.7.0" } }, + "node_modules/@ethersproject/constants": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", + "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.7.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.7.0.tgz", + "integrity": "sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/transactions": "^5.7.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", + "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@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" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.7.0.tgz", + "integrity": "sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/basex": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/pbkdf2": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wordlists": "^5.7.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz", + "integrity": "sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hdnode": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/pbkdf2": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/json-wallets/node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "license": "MIT" + }, + "node_modules/@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==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/keccak256/node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, "node_modules/@ethersproject/logger": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", @@ -3756,6 +4085,143 @@ ], "license": "MIT" }, + "node_modules/@ethersproject/networks": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", + "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz", + "integrity": "sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/sha2": "^5.7.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", + "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz", + "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/basex": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0", + "bech32": "1.1.4", + "ws": "7.4.6" + } + }, + "node_modules/@ethersproject/providers/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@ethersproject/random": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.7.0.tgz", + "integrity": "sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, "node_modules/@ethersproject/rlp": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", @@ -3776,6 +4242,223 @@ "@ethersproject/logger": "^5.7.0" } }, + "node_modules/@ethersproject/sha2": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.7.0.tgz", + "integrity": "sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@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==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@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" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz", + "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", + "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", + "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@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" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.7.0.tgz", + "integrity": "sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz", + "integrity": "sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/hdnode": "^5.7.0", + "@ethersproject/json-wallets": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wordlists": "^5.7.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", + "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@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" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.7.0.tgz", + "integrity": "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, "node_modules/@expo/bunyan": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@expo/bunyan/-/bunyan-4.0.1.tgz", @@ -9020,6 +9703,42 @@ } } }, + "node_modules/@scure/base": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.4.tgz", + "integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -10003,6 +10722,16 @@ "@types/chai": "*" } }, + "node_modules/@types/chai-spies": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/chai-spies/-/chai-spies-1.0.6.tgz", + "integrity": "sha512-xkk4HmhBB9OQeTAifa9MJ+6R5/Rq9+ungDe4JidZD+vqZVeiWZwc2i7/pd1ZKjyGlSBIQePoWdyUyFUGT0rv5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/chai-string": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@types/chai-string/-/chai-string-1.4.5.tgz", @@ -10040,6 +10769,20 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/deep-equal-in-any-order": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/deep-equal-in-any-order/-/deep-equal-in-any-order-1.0.4.tgz", + "integrity": "sha512-nkgl+KTkRWeybgMSBCrWrc1YHFrLADVxZ6Kllj40p9U499NGsCrFUjzgHgQY+Upsa/VWOc3YlsFEvhg+drUmkA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/dns-packet": { "version": "5.6.5", "resolved": "https://registry.npmjs.org/@types/dns-packet/-/dns-packet-5.6.5.tgz", @@ -10101,6 +10844,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -10161,6 +10925,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/markdown-it": { "version": "14.1.2", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", @@ -10906,6 +11677,10 @@ "resolved": "packages/relay", "link": true }, + "node_modules/@waku/rln": { + "resolved": "packages/rln", + "link": true + }, "node_modules/@waku/sdk": { "resolved": "packages/sdk", "link": true @@ -10922,6 +11697,12 @@ "resolved": "packages/utils", "link": true }, + "node_modules/@waku/zerokit-rln-wasm": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz", + "integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA==", + "license": "MIT or Apache2" + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -12877,6 +13658,12 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "license": "MIT" + }, "node_modules/before-after-hook": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", @@ -12960,6 +13747,12 @@ "dev": true, "license": "MIT" }, + "node_modules/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==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -13207,6 +14000,12 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -13812,6 +14611,19 @@ "integrity": "sha512-pdBOsH31vzWKYHr8JYTlsP+TFx7RTTm/2hQYbpxFd1WQ/X58ryrLBINRL2C1OWje8bi42NQqNZl2RooFPrsBqA==", "license": "MIT" }, + "node_modules/chai-spies": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/chai-spies/-/chai-spies-1.1.0.tgz", + "integrity": "sha512-ikaUhQvQWchRYj2K54itFp3nrcxaFRpSDQxDlRzSn9aWgu9Pi7lD8yFxTso4WnQ39+WZ69oB/qOvqp+isJIIWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + }, + "peerDependencies": { + "chai": "*" + } + }, "node_modules/chai-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/chai-string/-/chai-string-1.5.0.tgz", @@ -15592,6 +16404,17 @@ "node": ">=6" } }, + "node_modules/deep-equal-in-any-order": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.0.6.tgz", + "integrity": "sha512-RfnWHQzph10YrUjvWwhd15Dne8ciSJcZ3U6OD7owPwiVwsdE5IFSoZGg8rlwJD11ES+9H5y8j3fCofviRHOqLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.mapvalues": "^4.6.0", + "sort-any": "^2.0.0" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -16696,6 +17519,27 @@ "node": ">=0.10.0" } }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "license": "MIT", + "dependencies": { + "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" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "license": "MIT" + }, "node_modules/email-addresses": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", @@ -18376,6 +19220,35 @@ "node": ">= 0.6" } }, + "node_modules/ethereum-cryptography": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-3.1.0.tgz", + "integrity": "sha512-ZqHd92eOIH9RExpBUOgzpAgflyFv9/+Ca39G8V+oCjJPGjJUihQcG/Gl67I/Xn2HGS87dgnrCG3kb1jNClLi6g==", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "1.2.1", + "@noble/curves": "1.8.1", + "@noble/hashes": "1.7.1", + "@scure/bip32": "1.6.2", + "@scure/bip39": "1.5.4" + }, + "engines": { + "node": "^14.21.3 || >=16", + "npm": ">=9" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/ciphers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", + "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/event-iterator": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/event-iterator/-/event-iterator-2.0.0.tgz", @@ -20107,6 +20980,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -20226,6 +21109,17 @@ "node": "*" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -23985,6 +24879,13 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -26171,6 +27072,18 @@ "node": ">=4" } }, + "node_modules/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==", + "license": "ISC" + }, + "node_modules/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==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -35383,6 +36296,95 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-copy": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz", + "integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/rollup-plugin-copy/node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup-plugin-copy/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/rollup-plugin-copy/node_modules/globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-copy/node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup-plugin-copy/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/rollup-plugin-copy/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -35550,6 +36552,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", + "license": "MIT" + }, "node_modules/selfsigned": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", @@ -37362,6 +38370,16 @@ "node": ">= 14" } }, + "node_modules/sort-any": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-2.0.0.tgz", + "integrity": "sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -41345,6 +42363,275 @@ } } }, + "packages/rln": { + "name": "@waku/rln", + "version": "0.0.1", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@chainsafe/bls-keystore": "3.0.0", + "@waku/core": "^0.0.33", + "@waku/utils": "^0.0.21", + "@waku/zerokit-rln-wasm": "^0.0.13", + "ethereum-cryptography": "^3.1.0", + "ethers": "^5.7.2", + "lodash": "^4.17.21", + "uuid": "^11.0.5" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@types/chai": "^5.0.1", + "@types/chai-spies": "^1.0.6", + "@types/deep-equal-in-any-order": "^1.0.4", + "@types/lodash": "^4.17.15", + "@waku/build-utils": "^1.0.0", + "@waku/message-encryption": "^0.0.31", + "chai": "^5.1.2", + "chai-as-promised": "^8.0.1", + "chai-spies": "^1.1.0", + "chai-subset": "^1.6.0", + "deep-equal-in-any-order": "^2.0.6", + "fast-check": "^3.23.2", + "rollup-plugin-copy": "^3.5.0" + }, + "engines": { + "node": ">=20" + } + }, + "packages/rln/node_modules/@chainsafe/bls-keystore": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@chainsafe/bls-keystore/-/bls-keystore-3.0.0.tgz", + "integrity": "sha512-vlRIIXnn555wq2emhqnSR7btno17M0sCcfdQ+Dhgr7IH6n0CMoTGw9qcrpnNYwM+9OPm3matSYeZc9mNlXf7fQ==", + "license": "MIT", + "dependencies": { + "ethereum-cryptography": "^1.0.0", + "uuid": "8.3.2" + } + }, + "packages/rln/node_modules/@chainsafe/bls-keystore/node_modules/ethereum-cryptography": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" + } + }, + "packages/rln/node_modules/@chainsafe/bls-keystore/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "packages/rln/node_modules/@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "packages/rln/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/rln/node_modules/@scure/bip32": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + }, + "packages/rln/node_modules/@scure/bip39": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.2.0", + "@scure/base": "~1.1.0" + } + }, + "packages/rln/node_modules/@types/chai": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.0.1.tgz", + "integrity": "sha512-5T8ajsg3M/FOncpLYW7sdOcD6yf4+722sze/tc4KQV0P8Z2rAr3SAuHCIkYmYpt8VbcQlnz8SxlOlPQYefe4cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "packages/rln/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "packages/rln/node_modules/chai": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "packages/rln/node_modules/chai-as-promised": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.1.tgz", + "integrity": "sha512-OIEJtOL8xxJSH8JJWbIoRjybbzR52iFuDHuF8eb+nTPD6tgXLjRqsgnUGqQfFODxYvq5QdirT0pN9dZ0+Gz6rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "check-error": "^2.0.0" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 6" + } + }, + "packages/rln/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "packages/rln/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "packages/rln/node_modules/ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" + } + }, + "packages/rln/node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "packages/rln/node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "packages/rln/node_modules/uuid": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "packages/sdk": { "name": "@waku/sdk", "version": "0.0.29", diff --git a/package.json b/package.json index 2bfb2b3cc3..a43b9a681b 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "packages/browser-tests", "packages/build-utils", "packages/react-native-polyfills", - "packages/sds" + "packages/sds", + "packages/rln" ], "scripts": { "prepare": "husky", @@ -70,5 +71,8 @@ "*.{ts,js}": [ "eslint --fix" ] + }, + "dependencies": { + "@waku/utils": "^0.0.21" } } diff --git a/packages/rln/.eslintrc.cjs b/packages/rln/.eslintrc.cjs new file mode 100644 index 0000000000..c49a7edc19 --- /dev/null +++ b/packages/rln/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: "./tsconfig.dev.json" + }, + ignorePatterns: ["src/resources/**/*"] +}; diff --git a/packages/rln/.mocharc.cjs b/packages/rln/.mocharc.cjs new file mode 100644 index 0000000000..268cf0c611 --- /dev/null +++ b/packages/rln/.mocharc.cjs @@ -0,0 +1,27 @@ +const config = { + extension: ['ts'], + spec: 'src/**/*.spec.ts', + require: ['ts-node/register', 'isomorphic-fetch'], + loader: 'ts-node/esm', + 'node-option': [ + 'experimental-specifier-resolution=node', + 'loader=ts-node/esm' + ], + exit: true, + retries: 4 +}; + +if (process.env.CI) { + console.log("Running tests in parallel"); + config.parallel = true; + config.jobs = 6; + console.log("Activating allure reporting"); + config.reporter = 'mocha-multi-reporters'; + config.reporterOptions = { + configFile: '.mocha.reporters.json' + }; +} else { + console.log("Running tests serially. To enable parallel execution update mocha config"); +} + +module.exports = config; \ No newline at end of file diff --git a/packages/rln/README.md b/packages/rln/README.md new file mode 100644 index 0000000000..9148245514 --- /dev/null +++ b/packages/rln/README.md @@ -0,0 +1,25 @@ +# @waku/rln + +Rate Limiting Nullifier (RLN) implementation for Waku. + +## Description + +This package provides RLN functionality for the Waku protocol, enabling rate-limiting capabilities while preserving privacy. + +## Installation + +```bash +npm install @waku/rln +``` + +## Usage + +```typescript +import { RLN } from '@waku/rln'; + +// Usage examples coming soon +``` + +## License + +MIT OR Apache-2.0 \ No newline at end of file diff --git a/packages/rln/karma.conf.cjs b/packages/rln/karma.conf.cjs new file mode 100644 index 0000000000..b4db5d6c2e --- /dev/null +++ b/packages/rln/karma.conf.cjs @@ -0,0 +1,167 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const path = require("path"); + +const webpack = require("webpack"); + +const rootConfig = require("../../karma.conf.cjs"); + +module.exports = function (config) { + rootConfig(config); + + const configuration = { + frameworks: ["mocha", "webpack"], + + files: [ + { + pattern: "src/**/*.spec.ts", + type: "js" + }, + { + pattern: "src/resources/**/*.wasm", + included: false, + served: true, + watched: false, + type: "wasm", + nocache: true + }, + { + pattern: "src/resources/**/*.zkey", + included: false, + served: true, + watched: false, + nocache: true + }, + { + pattern: "../../node_modules/@waku/zerokit-rln-wasm/*.wasm", + included: false, + served: true, + watched: false, + type: "wasm", + nocache: true + } + ], + + preprocessors: { + "src/**/*.spec.ts": ["webpack"] + }, + + client: { + mocha: { + timeout: 180000 // 3 minutes + } + }, + + browserDisconnectTimeout: 180000, // 3 minutes + browserDisconnectTolerance: 3, // Number of tries before failing + browserNoActivityTimeout: 180000, // 3 minutes + captureTimeout: 300000, // 5 minutes + + mime: { + "application/wasm": ["wasm"], + "application/octet-stream": ["zkey"] + }, + + customHeaders: [ + { + match: ".*\\.wasm$", + name: "Content-Type", + value: "application/wasm" + }, + { + match: ".*\\.zkey$", + name: "Content-Type", + value: "application/octet-stream" + } + ], + + proxies: { + "/base/rln_wasm_bg.wasm": + "/absolute" + + path.resolve( + __dirname, + "../../node_modules/@waku/zerokit-rln-wasm/rln_wasm_bg.wasm" + ), + "/base/rln.wasm": + "/absolute" + path.resolve(__dirname, "src/resources/rln.wasm"), + "/base/rln_final.zkey": + "/absolute" + path.resolve(__dirname, "src/resources/rln_final.zkey") + }, + + webpack: { + mode: "development", + experiments: { + asyncWebAssembly: true, + syncWebAssembly: true, + topLevelAwait: true + }, + output: { + wasmLoading: "fetch", + path: path.resolve(__dirname, "dist"), + publicPath: "/base/", + clean: true + }, + module: { + rules: [ + { + test: /\.ts$/, + use: "ts-loader", + exclude: /node_modules/ + }, + { + test: /\.wasm$/, + type: "asset/resource", + generator: { + filename: "[name][ext]" + } + }, + { + test: /\.zkey$/, + type: "asset/resource", + generator: { + filename: "[name][ext]" + } + } + ] + }, + plugins: [ + new webpack.DefinePlugin({ + "process.env.CI": process.env.CI || false, + "process.env.DISPLAY": "Browser" + }), + new webpack.ProvidePlugin({ + process: "process/browser.js" + }) + ], + resolve: { + extensions: [".ts", ".js", ".wasm"], + modules: ["node_modules", "../../node_modules"], + alias: { + "@waku/zerokit-rln-wasm": path.resolve( + __dirname, + "../../node_modules/@waku/zerokit-rln-wasm/rln_wasm.js" + ) + }, + fallback: { + crypto: false, + fs: false, + path: false, + stream: false + } + }, + stats: { warnings: false }, + devtool: "inline-source-map" + }, + + reporters: ["progress"], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: process.env.CI ? ["ChromeHeadlessCI"] : ["ChromeHeadless"], + singleRun: true, + concurrency: 1, // Reduce concurrency to avoid memory pressure + browserSocketTimeout: 180000 // 3 minutes + }; + + config.set(configuration); +}; diff --git a/packages/rln/package.json b/packages/rln/package.json new file mode 100644 index 0000000000..48afe9f18d --- /dev/null +++ b/packages/rln/package.json @@ -0,0 +1,89 @@ +{ + "name": "@waku/rln", + "version": "0.0.1", + "description": "RLN (Rate Limiting Nullifier) implementation for Waku", + "types": "./dist/index.d.ts", + "module": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "type": "module", + "homepage": "https://github.com/waku-org/js-waku/tree/master/packages/rln#readme", + "repository": { + "type": "git", + "url": "https://github.com/waku-org/js-waku.git" + }, + "bugs": { + "url": "https://github.com/waku-org/js-waku/issues" + }, + "license": "MIT OR Apache-2.0", + "keywords": [ + "waku", + "rln", + "rate-limiting", + "privacy", + "web3" + ], + "scripts": { + "build": "run-s build:**", + "build:copy": "mkdir -p dist/resources && cp -r src/resources/* dist/resources/", + "build:esm": "tsc", + "build:bundle": "rollup --config rollup.config.js", + "fix": "run-s fix:*", + "fix:lint": "eslint src *.js --fix", + "check": "run-s check:*", + "check:tsc": "tsc -p tsconfig.dev.json", + "check:lint": "eslint \"src/!(resources)/**/*.{ts,js}\" *.js", + "check:spelling": "cspell \"{README.md,src/**/*.ts}\"", + "test": "NODE_ENV=test run-s test:*", + "test:browser": "karma start karma.conf.cjs", + "watch:build": "tsc -p tsconfig.json -w", + "watch:test": "mocha --watch", + "prepublish": "npm run build", + "reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build" + }, + "engines": { + "node": ">=20" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@types/chai": "^5.0.1", + "@types/chai-spies": "^1.0.6", + "@types/deep-equal-in-any-order": "^1.0.4", + "@types/lodash": "^4.17.15", + "@waku/build-utils": "^1.0.0", + "@waku/message-encryption": "^0.0.31", + "chai": "^5.1.2", + "chai-as-promised": "^8.0.1", + "chai-spies": "^1.1.0", + "chai-subset": "^1.6.0", + "deep-equal-in-any-order": "^2.0.6", + "fast-check": "^3.23.2", + "rollup-plugin-copy": "^3.5.0" + }, + "files": [ + "dist", + "bundle", + "src/**/*.ts", + "!**/*.spec.*", + "!**/*.json", + "CHANGELOG.md", + "LICENSE", + "README.md" + ], + "dependencies": { + "@chainsafe/bls-keystore": "3.0.0", + "@waku/core": "^0.0.33", + "@waku/utils": "^0.0.21", + "@waku/zerokit-rln-wasm": "^0.0.13", + "ethereum-cryptography": "^3.1.0", + "ethers": "^5.7.2", + "lodash": "^4.17.21", + "uuid": "^11.0.5" + } +} diff --git a/packages/rln/rollup.config.js b/packages/rln/rollup.config.js new file mode 100644 index 0000000000..43b7957fdc --- /dev/null +++ b/packages/rln/rollup.config.js @@ -0,0 +1,35 @@ +import commonjs from "@rollup/plugin-commonjs"; +import json from "@rollup/plugin-json"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; +import { extractExports } from "@waku/build-utils"; +import copy from "rollup-plugin-copy"; + +import * as packageJson from "./package.json" assert { type: "json" }; + +const input = extractExports(packageJson); + +export default { + input, + output: { + dir: "bundle", + format: "esm", + preserveModules: true + }, + plugins: [ + copy({ + targets: [ + { + src: ["src/resources/*"], + dest: "bundle/resources" + } + ], + copyOnce: true + }), + commonjs(), + json(), + nodeResolve({ + browser: true, + preferBuiltins: false + }) + ] +}; diff --git a/packages/rln/src/codec.spec.ts b/packages/rln/src/codec.spec.ts new file mode 100644 index 0000000000..df41f676c2 --- /dev/null +++ b/packages/rln/src/codec.spec.ts @@ -0,0 +1,470 @@ +import { + createDecoder, + createEncoder, + DecodedMessage +} from "@waku/core/lib/message/version_0"; +import type { IProtoMessage } from "@waku/interfaces"; +import { + generatePrivateKey, + generateSymmetricKey, + getPublicKey +} from "@waku/message-encryption"; +import { + createDecoder as createAsymDecoder, + createEncoder as createAsymEncoder +} from "@waku/message-encryption/ecies"; +import { + createDecoder as createSymDecoder, + createEncoder as createSymEncoder +} from "@waku/message-encryption/symmetric"; +import { expect } from "chai"; + +import { + createRLNDecoder, + createRLNEncoder, + RLNDecoder, + RLNEncoder +} from "./codec.js"; +import { createRLN } from "./create.js"; +import { RlnMessage } from "./message.js"; +import { epochBytesToInt } from "./utils/index.js"; + +const TestContentTopic = "/test/1/waku-message/utf8"; +const EMPTY_PUBSUB_TOPIC = ""; + +const EMPTY_PROTO_MESSAGE = { + timestamp: undefined, + contentTopic: "", + ephemeral: undefined, + meta: undefined, + rateLimitProof: undefined, + version: undefined +}; + +describe("RLN codec with version 0", () => { + it("toWire", async function () { + const rlnInstance = await createRLN(); + const credential = rlnInstance.zerokit.generateIdentityCredentials(); + const index = 0; + const payload = new Uint8Array([1, 2, 3, 4, 5]); + + rlnInstance.zerokit.insertMember(credential.IDCommitment); + + const rlnEncoder = createRLNEncoder({ + encoder: createEncoder({ contentTopic: TestContentTopic }), + rlnInstance, + index, + credential + }); + const rlnDecoder = createRLNDecoder({ + rlnInstance, + decoder: createDecoder(TestContentTopic) + }); + + const bytes = await rlnEncoder.toWire({ payload }); + + expect(bytes).to.not.be.undefined; + const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!); + expect(protoResult).to.not.be.undefined; + const msg = (await rlnDecoder.fromProtoObj( + EMPTY_PUBSUB_TOPIC, + protoResult! + ))!; + + expect(msg.rateLimitProof).to.not.be.undefined; + expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true; + expect(msg.verifyNoRoot()).to.be.true; + expect(msg.epoch).to.not.be.undefined; + expect(msg.epoch).to.be.gt(0); + + expect(msg.contentTopic).to.eq(TestContentTopic); + expect(msg.msg.version).to.eq(0); + expect(msg.payload).to.deep.eq(payload); + expect(msg.timestamp).to.not.be.undefined; + }); + + it("toProtoObj", async function () { + const rlnInstance = await createRLN(); + const credential = rlnInstance.zerokit.generateIdentityCredentials(); + const index = 0; + const payload = new Uint8Array([1, 2, 3, 4, 5]); + + rlnInstance.zerokit.insertMember(credential.IDCommitment); + + const rlnEncoder = new RLNEncoder( + createEncoder({ contentTopic: TestContentTopic }), + rlnInstance, + index, + credential + ); + const rlnDecoder = new RLNDecoder( + rlnInstance, + createDecoder(TestContentTopic) + ); + + const proto = await rlnEncoder.toProtoObj({ payload }); + + expect(proto).to.not.be.undefined; + const msg = (await rlnDecoder.fromProtoObj( + EMPTY_PUBSUB_TOPIC, + proto! + )) as RlnMessage; + + expect(msg).to.not.be.undefined; + expect(msg.rateLimitProof).to.not.be.undefined; + + expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true; + expect(msg.verifyNoRoot()).to.be.true; + expect(msg.epoch).to.not.be.undefined; + expect(msg.epoch).to.be.gt(0); + + expect(msg.contentTopic).to.eq(TestContentTopic); + expect(msg.msg.version).to.eq(0); + expect(msg.payload).to.deep.eq(payload); + expect(msg.timestamp).to.not.be.undefined; + }); +}); + +describe("RLN codec with version 1", () => { + it("Symmetric, toWire", async function () { + const rlnInstance = await createRLN(); + const credential = rlnInstance.zerokit.generateIdentityCredentials(); + const index = 0; + const payload = new Uint8Array([1, 2, 3, 4, 5]); + + rlnInstance.zerokit.insertMember(credential.IDCommitment); + + const symKey = generateSymmetricKey(); + + const rlnEncoder = new RLNEncoder( + createSymEncoder({ + contentTopic: TestContentTopic, + symKey + }), + rlnInstance, + index, + credential + ); + const rlnDecoder = new RLNDecoder( + rlnInstance, + createSymDecoder(TestContentTopic, symKey) + ); + + const bytes = await rlnEncoder.toWire({ payload }); + + expect(bytes).to.not.be.undefined; + const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!); + + expect(protoResult).to.not.be.undefined; + const msg = (await rlnDecoder.fromProtoObj( + EMPTY_PUBSUB_TOPIC, + protoResult! + ))!; + + expect(msg.rateLimitProof).to.not.be.undefined; + expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true; + expect(msg.verifyNoRoot()).to.be.true; + expect(msg.epoch).to.not.be.undefined; + expect(msg.epoch).to.be.gt(0); + + expect(msg.contentTopic).to.eq(TestContentTopic); + expect(msg.msg.version).to.eq(1); + expect(msg.payload).to.deep.eq(payload); + expect(msg.timestamp).to.not.be.undefined; + }); + + it("Symmetric, toProtoObj", async function () { + const rlnInstance = await createRLN(); + const credential = rlnInstance.zerokit.generateIdentityCredentials(); + const index = 0; + const payload = new Uint8Array([1, 2, 3, 4, 5]); + + rlnInstance.zerokit.insertMember(credential.IDCommitment); + + const symKey = generateSymmetricKey(); + + const rlnEncoder = new RLNEncoder( + createSymEncoder({ + contentTopic: TestContentTopic, + symKey + }), + rlnInstance, + index, + credential + ); + const rlnDecoder = new RLNDecoder( + rlnInstance, + createSymDecoder(TestContentTopic, symKey) + ); + + const proto = await rlnEncoder.toProtoObj({ payload }); + + expect(proto).to.not.be.undefined; + const msg = (await rlnDecoder.fromProtoObj( + EMPTY_PUBSUB_TOPIC, + proto! + )) as RlnMessage; + + expect(msg).to.not.be.undefined; + expect(msg.rateLimitProof).to.not.be.undefined; + + expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true; + expect(msg.verifyNoRoot()).to.be.true; + expect(msg.epoch).to.not.be.undefined; + expect(msg.epoch).to.be.gt(0); + + expect(msg.contentTopic).to.eq(TestContentTopic); + expect(msg.msg.version).to.eq(1); + expect(msg.payload).to.deep.eq(payload); + expect(msg.timestamp).to.not.be.undefined; + }); + + it("Asymmetric, toWire", async function () { + const rlnInstance = await createRLN(); + const credential = rlnInstance.zerokit.generateIdentityCredentials(); + const index = 0; + const payload = new Uint8Array([1, 2, 3, 4, 5]); + + rlnInstance.zerokit.insertMember(credential.IDCommitment); + + const privateKey = generatePrivateKey(); + const publicKey = getPublicKey(privateKey); + + const rlnEncoder = new RLNEncoder( + createAsymEncoder({ + contentTopic: TestContentTopic, + publicKey + }), + rlnInstance, + index, + credential + ); + const rlnDecoder = new RLNDecoder( + rlnInstance, + createAsymDecoder(TestContentTopic, privateKey) + ); + + const bytes = await rlnEncoder.toWire({ payload }); + + expect(bytes).to.not.be.undefined; + const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!); + + expect(protoResult).to.not.be.undefined; + const msg = (await rlnDecoder.fromProtoObj( + EMPTY_PUBSUB_TOPIC, + protoResult! + ))!; + + expect(msg.rateLimitProof).to.not.be.undefined; + expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true; + expect(msg.verifyNoRoot()).to.be.true; + expect(msg.epoch).to.not.be.undefined; + expect(msg.epoch).to.be.gt(0); + + expect(msg.contentTopic).to.eq(TestContentTopic); + expect(msg.msg.version).to.eq(1); + expect(msg.payload).to.deep.eq(payload); + expect(msg.timestamp).to.not.be.undefined; + }); + + it("Asymmetric, toProtoObj", async function () { + const rlnInstance = await createRLN(); + const credential = rlnInstance.zerokit.generateIdentityCredentials(); + const index = 0; + const payload = new Uint8Array([1, 2, 3, 4, 5]); + + rlnInstance.zerokit.insertMember(credential.IDCommitment); + + const privateKey = generatePrivateKey(); + const publicKey = getPublicKey(privateKey); + + const rlnEncoder = new RLNEncoder( + createAsymEncoder({ + contentTopic: TestContentTopic, + publicKey + }), + rlnInstance, + index, + credential + ); + const rlnDecoder = new RLNDecoder( + rlnInstance, + createAsymDecoder(TestContentTopic, privateKey) + ); + + const proto = await rlnEncoder.toProtoObj({ payload }); + + expect(proto).to.not.be.undefined; + const msg = (await rlnDecoder.fromProtoObj( + EMPTY_PUBSUB_TOPIC, + proto! + )) as RlnMessage; + + expect(msg).to.not.be.undefined; + expect(msg.rateLimitProof).to.not.be.undefined; + + expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true; + expect(msg.verifyNoRoot()).to.be.true; + expect(msg.epoch).to.not.be.undefined; + expect(msg.epoch).to.be.gt(0); + + expect(msg.contentTopic).to.eq(TestContentTopic); + expect(msg.msg.version).to.eq(1); + expect(msg.payload).to.deep.eq(payload); + expect(msg.timestamp).to.not.be.undefined; + }); +}); + +describe("RLN Codec - epoch", () => { + it("toProtoObj", async function () { + const rlnInstance = await createRLN(); + const credential = rlnInstance.zerokit.generateIdentityCredentials(); + const index = 0; + const payload = new Uint8Array([1, 2, 3, 4, 5]); + + rlnInstance.zerokit.insertMember(credential.IDCommitment); + + const rlnEncoder = new RLNEncoder( + createEncoder({ contentTopic: TestContentTopic }), + rlnInstance, + index, + credential + ); + const rlnDecoder = new RLNDecoder( + rlnInstance, + createDecoder(TestContentTopic) + ); + + const proto = await rlnEncoder.toProtoObj({ payload }); + + expect(proto).to.not.be.undefined; + const msg = (await rlnDecoder.fromProtoObj( + EMPTY_PUBSUB_TOPIC, + proto! + )) as RlnMessage; + + const epochBytes = proto!.rateLimitProof!.epoch; + const epoch = epochBytesToInt(epochBytes); + + expect(msg).to.not.be.undefined; + expect(msg.rateLimitProof).to.not.be.undefined; + + expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true; + expect(msg.verifyNoRoot()).to.be.true; + expect(msg.epoch).to.not.be.undefined; + expect(msg.epoch!.toString(10).length).to.eq(9); + expect(msg.epoch).to.eq(epoch); + + expect(msg.contentTopic).to.eq(TestContentTopic); + expect(msg.msg.version).to.eq(0); + expect(msg.payload).to.deep.eq(payload); + expect(msg.timestamp).to.not.be.undefined; + }); +}); + +describe("RLN codec with version 0 and meta setter", () => { + // Encode the length of the payload + // Not a relevant real life example + const metaSetter = (msg: IProtoMessage & { meta: undefined }): Uint8Array => { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + view.setUint32(0, msg.payload.length, false); + return new Uint8Array(buffer); + }; + + it("toWire", async function () { + const rlnInstance = await createRLN(); + const credential = rlnInstance.zerokit.generateIdentityCredentials(); + const index = 0; + const payload = new Uint8Array([1, 2, 3, 4, 5]); + + rlnInstance.zerokit.insertMember(credential.IDCommitment); + + const rlnEncoder = createRLNEncoder({ + encoder: createEncoder({ contentTopic: TestContentTopic, metaSetter }), + rlnInstance, + index, + credential + }); + const rlnDecoder = createRLNDecoder({ + rlnInstance, + decoder: createDecoder(TestContentTopic) + }); + + const bytes = await rlnEncoder.toWire({ payload }); + + expect(bytes).to.not.be.undefined; + const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!); + expect(protoResult).to.not.be.undefined; + const msg = (await rlnDecoder.fromProtoObj( + EMPTY_PUBSUB_TOPIC, + protoResult! + ))!; + + const expectedMeta = metaSetter({ + ...EMPTY_PROTO_MESSAGE, + payload: protoResult!.payload + }); + + expect(msg!.meta).to.deep.eq(expectedMeta); + + expect(msg.rateLimitProof).to.not.be.undefined; + expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true; + expect(msg.verifyNoRoot()).to.be.true; + expect(msg.epoch).to.not.be.undefined; + expect(msg.epoch).to.be.gt(0); + + expect(msg.contentTopic).to.eq(TestContentTopic); + expect(msg.msg.version).to.eq(0); + expect(msg.payload).to.deep.eq(payload); + expect(msg.timestamp).to.not.be.undefined; + }); + + it("toProtoObj", async function () { + const rlnInstance = await createRLN(); + const credential = rlnInstance.zerokit.generateIdentityCredentials(); + const index = 0; + const payload = new Uint8Array([1, 2, 3, 4, 5]); + + rlnInstance.zerokit.insertMember(credential.IDCommitment); + + const rlnEncoder = new RLNEncoder( + createEncoder({ contentTopic: TestContentTopic, metaSetter }), + rlnInstance, + index, + credential + ); + const rlnDecoder = new RLNDecoder( + rlnInstance, + createDecoder(TestContentTopic) + ); + + const proto = await rlnEncoder.toProtoObj({ payload }); + + expect(proto).to.not.be.undefined; + const msg = (await rlnDecoder.fromProtoObj( + EMPTY_PUBSUB_TOPIC, + proto! + )) as RlnMessage; + + const expectedMeta = metaSetter({ + ...EMPTY_PROTO_MESSAGE, + payload: msg!.payload + }); + + expect(msg!.meta).to.deep.eq(expectedMeta); + + expect(msg).to.not.be.undefined; + expect(msg.rateLimitProof).to.not.be.undefined; + + expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true; + expect(msg.verifyNoRoot()).to.be.true; + expect(msg.epoch).to.not.be.undefined; + expect(msg.epoch).to.be.gt(0); + + expect(msg.contentTopic).to.eq(TestContentTopic); + expect(msg.msg.version).to.eq(0); + expect(msg.payload).to.deep.eq(payload); + expect(msg.timestamp).to.not.be.undefined; + }); +}); diff --git a/packages/rln/src/codec.ts b/packages/rln/src/codec.ts new file mode 100644 index 0000000000..3a9036d4b1 --- /dev/null +++ b/packages/rln/src/codec.ts @@ -0,0 +1,134 @@ +import type { + IDecodedMessage, + IDecoder, + IEncoder, + IMessage, + IProtoMessage, + IRateLimitProof +} from "@waku/interfaces"; +import { Logger } from "@waku/utils"; + +import type { IdentityCredential } from "./identity.js"; +import { RlnMessage, toRLNSignal } from "./message.js"; +import { RLNInstance } from "./rln.js"; + +const log = new Logger("waku:rln:encoder"); + +export class RLNEncoder implements IEncoder { + private readonly idSecretHash: Uint8Array; + + public constructor( + private readonly encoder: IEncoder, + private readonly rlnInstance: RLNInstance, + private readonly index: number, + identityCredential: IdentityCredential + ) { + if (index < 0) throw new Error("Invalid membership index"); + this.idSecretHash = identityCredential.IDSecretHash; + } + + public async toWire(message: IMessage): Promise { + message.rateLimitProof = await this.generateProof(message); + log.info("Proof generated", message.rateLimitProof); + return this.encoder.toWire(message); + } + + public async toProtoObj( + message: IMessage + ): Promise { + const protoMessage = await this.encoder.toProtoObj(message); + if (!protoMessage) return; + + protoMessage.contentTopic = this.contentTopic; + protoMessage.rateLimitProof = await this.generateProof(message); + log.info("Proof generated", protoMessage.rateLimitProof); + return protoMessage; + } + + private async generateProof(message: IMessage): Promise { + const signal = toRLNSignal(this.contentTopic, message); + const proof = await this.rlnInstance.zerokit.generateRLNProof( + signal, + this.index, + message.timestamp, + this.idSecretHash + ); + return proof; + } + + public get pubsubTopic(): string { + return this.encoder.pubsubTopic; + } + + public get contentTopic(): string { + return this.encoder.contentTopic; + } + + public get ephemeral(): boolean { + return this.encoder.ephemeral; + } +} + +type RLNEncoderOptions = { + encoder: IEncoder; + rlnInstance: RLNInstance; + index: number; + credential: IdentityCredential; +}; + +export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => { + return new RLNEncoder( + options.encoder, + options.rlnInstance, + options.index, + options.credential + ); +}; + +export class RLNDecoder + implements IDecoder> +{ + public constructor( + private readonly rlnInstance: RLNInstance, + private readonly decoder: IDecoder + ) {} + + public get pubsubTopic(): string { + return this.decoder.pubsubTopic; + } + + public get contentTopic(): string { + return this.decoder.contentTopic; + } + + public fromWireToProtoObj( + bytes: Uint8Array + ): Promise { + const protoMessage = this.decoder.fromWireToProtoObj(bytes); + log.info("Message decoded", protoMessage); + return Promise.resolve(protoMessage); + } + + public async fromProtoObj( + pubsubTopic: string, + proto: IProtoMessage + ): Promise | undefined> { + const msg: T | undefined = await this.decoder.fromProtoObj( + pubsubTopic, + proto + ); + if (!msg) return; + return new RlnMessage(this.rlnInstance, msg, proto.rateLimitProof); + } +} + +type RLNDecoderOptions = { + decoder: IDecoder; + rlnInstance: RLNInstance; +}; + +export const createRLNDecoder = ( + options: RLNDecoderOptions +): RLNDecoder => { + return new RLNDecoder(options.rlnInstance, options.decoder); +}; diff --git a/packages/rln/src/contract/constants.ts b/packages/rln/src/contract/constants.ts new file mode 100644 index 0000000000..f884ce7033 --- /dev/null +++ b/packages/rln/src/contract/constants.ts @@ -0,0 +1,68 @@ +// ref https://github.com/waku-org/waku-rln-contract/blob/19fded82bca07e7b535b429dc507cfb83f10dfcf/deployments/sepolia/WakuRlnRegistry_Implementation.json#L3 +export const RLN_REGISTRY_ABI = [ + "error IncompatibleStorage()", + "error IncompatibleStorageIndex()", + "error NoStorageContractAvailable()", + "error StorageAlreadyExists(address storageAddress)", + "event AdminChanged(address previousAdmin, address newAdmin)", + "event BeaconUpgraded(address indexed beacon)", + "event Initialized(uint8 version)", + "event NewStorageContract(uint16 index, address storageAddress)", + "event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)", + "event Upgraded(address indexed implementation)", + "function forceProgress()", + "function initialize(address _poseidonHasher)", + "function newStorage()", + "function nextStorageIndex() view returns (uint16)", + "function owner() view returns (address)", + "function poseidonHasher() view returns (address)", + "function proxiableUUID() view returns (bytes32)", + "function register(uint16 storageIndex, uint256 commitment)", + "function register(uint256[] commitments)", + "function register(uint16 storageIndex, uint256[] commitments)", + "function registerStorage(address storageAddress)", + "function renounceOwnership()", + "function storages(uint16) view returns (address)", + "function transferOwnership(address newOwner)", + "function upgradeTo(address newImplementation)", + "function upgradeToAndCall(address newImplementation, bytes data) payable", + "function usingStorageIndex() view returns (uint16)" +]; + +// ref https://github.com/waku-org/waku-rln-contract/blob/19fded82bca07e7b535b429dc507cfb83f10dfcf/deployments/sepolia/WakuRlnStorage_0.json#L3 +export const RLN_STORAGE_ABI = [ + "constructor(address _poseidonHasher, uint16 _contractIndex)", + "error DuplicateIdCommitment()", + "error FullTree()", + "error InvalidIdCommitment(uint256 idCommitment)", + "error NotImplemented()", + "event MemberRegistered(uint256 idCommitment, uint256 index)", + "event MemberWithdrawn(uint256 idCommitment, uint256 index)", + "event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)", + "function DEPTH() view returns (uint256)", + "function MEMBERSHIP_DEPOSIT() view returns (uint256)", + "function SET_SIZE() view returns (uint256)", + "function contractIndex() view returns (uint16)", + "function deployedBlockNumber() view returns (uint32)", + "function idCommitmentIndex() view returns (uint256)", + "function isValidCommitment(uint256 idCommitment) view returns (bool)", + "function memberExists(uint256) view returns (bool)", + "function members(uint256) view returns (uint256)", + "function owner() view returns (address)", + "function poseidonHasher() view returns (address)", + "function register(uint256[] idCommitments)", + "function register(uint256 idCommitment) payable", + "function renounceOwnership()", + "function slash(uint256 idCommitment, address receiver, uint256[8] proof) pure", + "function stakedAmounts(uint256) view returns (uint256)", + "function transferOwnership(address newOwner)", + "function verifier() view returns (address)", + "function withdraw() pure", + "function withdrawalBalance(address) view returns (uint256)" +]; + +export const SEPOLIA_CONTRACT = { + chainId: 11155111, + address: "0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4", + abi: RLN_REGISTRY_ABI +}; diff --git a/packages/rln/src/contract/index.ts b/packages/rln/src/contract/index.ts new file mode 100644 index 0000000000..caa0fecbef --- /dev/null +++ b/packages/rln/src/contract/index.ts @@ -0,0 +1,2 @@ +export { RLNContract } from "./rln_contract.js"; +export * from "./constants.js"; diff --git a/packages/rln/src/contract/rln_contract.spec.ts b/packages/rln/src/contract/rln_contract.spec.ts new file mode 100644 index 0000000000..69412b8a64 --- /dev/null +++ b/packages/rln/src/contract/rln_contract.spec.ts @@ -0,0 +1,82 @@ +import { expect } from "chai"; +import * as ethers from "ethers"; + +import { createRLN } from "../create.js"; + +import { SEPOLIA_CONTRACT } from "./constants.js"; +import { RLNContract } from "./rln_contract.js"; + +describe("RLN Contract abstraction", () => { + it("should be able to fetch members from events and store to rln instance", async () => { + const rlnInstance = await createRLN(); + let insertMemberCalled = false; + + // Track if insertMember was called + const originalInsertMember = rlnInstance.zerokit.insertMember; + rlnInstance.zerokit.insertMember = function ( + this: any, + ...args: Parameters + ) { + insertMemberCalled = true; + return originalInsertMember.apply(this, args); + }; + + const voidSigner = new ethers.VoidSigner(SEPOLIA_CONTRACT.address); + const rlnContract = new RLNContract(rlnInstance, { + registryAddress: SEPOLIA_CONTRACT.address, + signer: voidSigner + }); + + rlnContract["storageContract"] = { + queryFilter: () => Promise.resolve([mockEvent()]) + } as unknown as ethers.Contract; + rlnContract["_membersFilter"] = { + address: "", + topics: [] + } as unknown as ethers.EventFilter; + + await rlnContract.fetchMembers(rlnInstance); + + expect(insertMemberCalled).to.be.true; + }); + + it("should register a member", async () => { + const mockSignature = + "0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c"; + + const rlnInstance = await createRLN(); + const voidSigner = new ethers.VoidSigner(SEPOLIA_CONTRACT.address); + const rlnContract = new RLNContract(rlnInstance, { + registryAddress: SEPOLIA_CONTRACT.address, + signer: voidSigner + }); + + let registerCalled = false; + rlnContract["storageIndex"] = 1; + rlnContract["_membersFilter"] = { + address: "", + topics: [] + } as unknown as ethers.EventFilter; + rlnContract["registryContract"] = { + "register(uint16,uint256)": () => { + registerCalled = true; + return Promise.resolve({ wait: () => Promise.resolve(undefined) }); + } + } as unknown as ethers.Contract; + + const identity = + rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature); + await rlnContract.registerWithIdentity(identity); + + expect(registerCalled).to.be.true; + }); +}); + +function mockEvent(): ethers.Event { + return { + args: { + idCommitment: { _hex: "0xb3df1c4e5600ef2b" }, + index: ethers.BigNumber.from(1) + } + } as unknown as ethers.Event; +} diff --git a/packages/rln/src/contract/rln_contract.ts b/packages/rln/src/contract/rln_contract.ts new file mode 100644 index 0000000000..1c35c3fa14 --- /dev/null +++ b/packages/rln/src/contract/rln_contract.ts @@ -0,0 +1,353 @@ +import { Logger } from "@waku/utils"; +import { hexToBytes } from "@waku/utils/bytes"; +import { ethers } from "ethers"; + +import type { IdentityCredential } from "../identity.js"; +import type { DecryptedCredentials } from "../keystore/index.js"; +import type { RLNInstance } from "../rln.js"; +import { MerkleRootTracker } from "../root_tracker.js"; +import { zeroPadLE } from "../utils/index.js"; + +import { RLN_REGISTRY_ABI, RLN_STORAGE_ABI } from "./constants.js"; + +const log = new Logger("waku:rln:contract"); + +type Member = { + idCommitment: string; + index: ethers.BigNumber; +}; + +type Signer = ethers.Signer; + +type RLNContractOptions = { + signer: Signer; + registryAddress: string; +}; + +type RLNStorageOptions = { + storageIndex?: number; +}; + +type RLNContractInitOptions = RLNContractOptions & RLNStorageOptions; + +type FetchMembersOptions = { + fromBlock?: number; + fetchRange?: number; + fetchChunks?: number; +}; + +export class RLNContract { + private registryContract: ethers.Contract; + private merkleRootTracker: MerkleRootTracker; + + private deployBlock: undefined | number; + private storageIndex: undefined | number; + private storageContract: undefined | ethers.Contract; + private _membersFilter: undefined | ethers.EventFilter; + + private _members: Map = new Map(); + + public static async init( + rlnInstance: RLNInstance, + options: RLNContractInitOptions + ): Promise { + const rlnContract = new RLNContract(rlnInstance, options); + + await rlnContract.initStorageContract(options.signer); + await rlnContract.fetchMembers(rlnInstance); + rlnContract.subscribeToMembers(rlnInstance); + + return rlnContract; + } + + public constructor( + rlnInstance: RLNInstance, + { registryAddress, signer }: RLNContractOptions + ) { + const initialRoot = rlnInstance.zerokit.getMerkleRoot(); + + this.registryContract = new ethers.Contract( + registryAddress, + RLN_REGISTRY_ABI, + signer + ); + this.merkleRootTracker = new MerkleRootTracker(5, initialRoot); + } + + private async initStorageContract( + signer: Signer, + options: RLNStorageOptions = {} + ): Promise { + const storageIndex = options?.storageIndex + ? options.storageIndex + : await this.registryContract.usingStorageIndex(); + const storageAddress = await this.registryContract.storages(storageIndex); + + if (!storageAddress || storageAddress === ethers.constants.AddressZero) { + throw Error("No RLN Storage initialized on registry contract."); + } + + this.storageIndex = storageIndex; + this.storageContract = new ethers.Contract( + storageAddress, + RLN_STORAGE_ABI, + signer + ); + this._membersFilter = this.storageContract.filters.MemberRegistered(); + + this.deployBlock = await this.storageContract.deployedBlockNumber(); + } + + public get registry(): ethers.Contract { + if (!this.registryContract) { + throw Error("Registry contract was not initialized"); + } + return this.registryContract as ethers.Contract; + } + + public get contract(): ethers.Contract { + if (!this.storageContract) { + throw Error("Storage contract was not initialized"); + } + return this.storageContract as ethers.Contract; + } + + public get members(): Member[] { + const sortedMembers = Array.from(this._members.values()).sort( + (left, right) => left.index.toNumber() - right.index.toNumber() + ); + return sortedMembers; + } + + private get membersFilter(): ethers.EventFilter { + if (!this._membersFilter) { + throw Error("Members filter was not initialized."); + } + return this._membersFilter as ethers.EventFilter; + } + + public async fetchMembers( + rlnInstance: RLNInstance, + options: FetchMembersOptions = {} + ): Promise { + const registeredMemberEvents = await queryFilter(this.contract, { + fromBlock: this.deployBlock, + ...options, + membersFilter: this.membersFilter + }); + this.processEvents(rlnInstance, registeredMemberEvents); + } + + public processEvents(rlnInstance: RLNInstance, events: ethers.Event[]): void { + const toRemoveTable = new Map(); + const toInsertTable = new Map(); + + events.forEach((evt) => { + if (!evt.args) { + return; + } + + if (evt.removed) { + const index: ethers.BigNumber = evt.args.index; + const toRemoveVal = toRemoveTable.get(evt.blockNumber); + if (toRemoveVal != undefined) { + toRemoveVal.push(index.toNumber()); + toRemoveTable.set(evt.blockNumber, toRemoveVal); + } else { + toRemoveTable.set(evt.blockNumber, [index.toNumber()]); + } + } else { + let eventsPerBlock = toInsertTable.get(evt.blockNumber); + if (eventsPerBlock == undefined) { + eventsPerBlock = []; + } + + eventsPerBlock.push(evt); + toInsertTable.set(evt.blockNumber, eventsPerBlock); + } + }); + + this.removeMembers(rlnInstance, toRemoveTable); + this.insertMembers(rlnInstance, toInsertTable); + } + + private insertMembers( + rlnInstance: RLNInstance, + toInsert: Map + ): void { + toInsert.forEach((events: ethers.Event[], blockNumber: number) => { + events.forEach((evt) => { + const _idCommitment = evt?.args?.idCommitment; + const index: ethers.BigNumber = evt?.args?.index; + + if (!_idCommitment || !index) { + return; + } + + const idCommitment = zeroPadLE(hexToBytes(_idCommitment?._hex), 32); + rlnInstance.zerokit.insertMember(idCommitment); + this._members.set(index.toNumber(), { + index, + idCommitment: _idCommitment?._hex + }); + }); + + const currentRoot = rlnInstance.zerokit.getMerkleRoot(); + this.merkleRootTracker.pushRoot(blockNumber, currentRoot); + }); + } + + private removeMembers( + rlnInstance: RLNInstance, + toRemove: Map + ): void { + const removeDescending = new Map([...toRemove].sort().reverse()); + removeDescending.forEach((indexes: number[], blockNumber: number) => { + indexes.forEach((index) => { + if (this._members.has(index)) { + this._members.delete(index); + rlnInstance.zerokit.deleteMember(index); + } + }); + + this.merkleRootTracker.backFill(blockNumber); + }); + } + + public subscribeToMembers(rlnInstance: RLNInstance): void { + this.contract.on(this.membersFilter, (_pubkey, _index, event) => + this.processEvents(rlnInstance, [event]) + ); + } + + public async registerWithIdentity( + identity: IdentityCredential + ): Promise { + if (this.storageIndex === undefined) { + throw Error( + "Cannot register credential, no storage contract index found." + ); + } + const txRegisterResponse: ethers.ContractTransaction = + await this.registryContract["register(uint16,uint256)"]( + this.storageIndex, + identity.IDCommitmentBigInt, + { gasLimit: 100000 } + ); + const txRegisterReceipt = await txRegisterResponse.wait(); + + // assumption: register(uint16,uint256) emits one event + const memberRegistered = txRegisterReceipt?.events?.[0]; + + if (!memberRegistered) { + return undefined; + } + + const decodedData = this.contract.interface.decodeEventLog( + "MemberRegistered", + memberRegistered.data + ); + + const network = await this.registryContract.provider.getNetwork(); + const address = this.registryContract.address; + const membershipId = decodedData.index.toNumber(); + + return { + identity, + membership: { + address, + treeIndex: membershipId, + chainId: network.chainId + } + }; + } + + public roots(): Uint8Array[] { + return this.merkleRootTracker.roots(); + } +} + +type CustomQueryOptions = FetchMembersOptions & { + membersFilter: ethers.EventFilter; +}; + +// these value should be tested on other networks +const FETCH_CHUNK = 5; +const BLOCK_RANGE = 3000; + +async function queryFilter( + contract: ethers.Contract, + options: CustomQueryOptions +): Promise { + const { + fromBlock, + membersFilter, + fetchRange = BLOCK_RANGE, + fetchChunks = FETCH_CHUNK + } = options; + + if (!fromBlock) { + return contract.queryFilter(membersFilter); + } + + if (!contract.signer.provider) { + throw Error("No provider found on the contract's signer."); + } + + const toBlock = await contract.signer.provider.getBlockNumber(); + + if (toBlock - fromBlock < fetchRange) { + return contract.queryFilter(membersFilter); + } + + const events: ethers.Event[][] = []; + const chunks = splitToChunks(fromBlock, toBlock, fetchRange); + + for (const portion of takeN<[number, number]>(chunks, fetchChunks)) { + const promises = portion.map(([left, right]) => + ignoreErrors(contract.queryFilter(membersFilter, left, right), []) + ); + const fetchedEvents = await Promise.all(promises); + events.push(fetchedEvents.flatMap((v) => v)); + } + + return events.flatMap((v) => v); +} + +function splitToChunks( + from: number, + to: number, + step: number +): Array<[number, number]> { + const chunks = []; + + let left = from; + while (left < to) { + const right = left + step < to ? left + step : to; + + chunks.push([left, right] as [number, number]); + + left = right; + } + + return chunks; +} + +function* takeN(array: T[], size: number): Iterable { + let start = 0; + + while (start < array.length) { + const portion = array.slice(start, start + size); + + yield portion; + + start += size; + } +} + +function ignoreErrors(promise: Promise, defaultValue: T): Promise { + return promise.catch((err) => { + log.info(`Ignoring an error during query: ${err?.message}`); + return defaultValue; + }); +} diff --git a/packages/rln/src/create.spec.ts b/packages/rln/src/create.spec.ts new file mode 100644 index 0000000000..7dc2e4fd5e --- /dev/null +++ b/packages/rln/src/create.spec.ts @@ -0,0 +1,137 @@ +import { assert, expect } from "chai"; + +import { createRLN } from "./create.js"; + +describe("js-rln", () => { + it("should verify a proof", async function () { + const rlnInstance = await createRLN(); + + const credential = rlnInstance.zerokit.generateIdentityCredentials(); + + //peer's index in the Merkle Tree + const index = 5; + + // Create a Merkle tree with random members + for (let i = 0; i < 10; i++) { + if (i == index) { + // insert the current peer's pk + rlnInstance.zerokit.insertMember(credential.IDCommitment); + } else { + // create a new key pair + rlnInstance.zerokit.insertMember( + rlnInstance.zerokit.generateIdentityCredentials().IDCommitment + ); + } + } + + // prepare the message + const uint8Msg = Uint8Array.from( + "Hello World".split("").map((x) => x.charCodeAt(0)) + ); + + // setting up the epoch + const epoch = new Date(); + + // generating proof + const proof = await rlnInstance.zerokit.generateRLNProof( + uint8Msg, + index, + epoch, + credential.IDSecretHash + ); + + try { + // verify the proof + const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg); + expect(verifResult).to.be.true; + } catch (err) { + assert.fail(0, 1, "should not have failed proof verification"); + } + + try { + // Modifying the signal so it's invalid + uint8Msg[4] = 4; + // verify the proof + const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg); + expect(verifResult).to.be.false; + } catch (err) { + console.log(err); + } + }); + it("should verify a proof with a seeded membership key generation", async function () { + const rlnInstance = await createRLN(); + const seed = "This is a test seed"; + const credential = + rlnInstance.zerokit.generateSeededIdentityCredential(seed); + + //peer's index in the Merkle Tree + const index = 5; + + // Create a Merkle tree with random members + for (let i = 0; i < 10; i++) { + if (i == index) { + // insert the current peer's pk + rlnInstance.zerokit.insertMember(credential.IDCommitment); + } else { + // create a new key pair + rlnInstance.zerokit.insertMember( + rlnInstance.zerokit.generateIdentityCredentials().IDCommitment + ); + } + } + + // prepare the message + const uint8Msg = Uint8Array.from( + "Hello World".split("").map((x) => x.charCodeAt(0)) + ); + + // setting up the epoch + const epoch = new Date(); + + // generating proof + const proof = await rlnInstance.zerokit.generateRLNProof( + uint8Msg, + index, + epoch, + credential.IDSecretHash + ); + + try { + // verify the proof + const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg); + expect(verifResult).to.be.true; + } catch (err) { + assert.fail(0, 1, "should not have failed proof verification"); + } + + try { + // Modifying the signal so it's invalid + uint8Msg[4] = 4; + // verify the proof + const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg); + expect(verifResult).to.be.false; + } catch (err) { + console.log(err); + } + }); + + it("should generate the same membership key if the same seed is provided", async function () { + const rlnInstance = await createRLN(); + const seed = "This is a test seed"; + const memKeys1 = rlnInstance.zerokit.generateSeededIdentityCredential(seed); + const memKeys2 = rlnInstance.zerokit.generateSeededIdentityCredential(seed); + + memKeys1.IDCommitment.forEach((element, index) => { + expect(element).to.equal(memKeys2.IDCommitment[index]); + }); + memKeys1.IDNullifier.forEach((element, index) => { + expect(element).to.equal(memKeys2.IDNullifier[index]); + }); + memKeys1.IDSecretHash.forEach((element, index) => { + expect(element).to.equal(memKeys2.IDSecretHash[index]); + }); + memKeys1.IDTrapdoor.forEach((element, index) => { + expect(element).to.equal(memKeys2.IDTrapdoor[index]); + }); + }); +}); diff --git a/packages/rln/src/create.ts b/packages/rln/src/create.ts new file mode 100644 index 0000000000..3b32302832 --- /dev/null +++ b/packages/rln/src/create.ts @@ -0,0 +1,9 @@ +import type { RLNInstance } from "./rln.js"; + +export async function createRLN(): Promise { + // A dependency graph that contains any wasm must all be imported + // asynchronously. This file does the single async import, so + // that no one else needs to worry about it again. + const rlnModule = await import("./rln.js"); + return rlnModule.create(); +} diff --git a/packages/rln/src/identity.ts b/packages/rln/src/identity.ts new file mode 100644 index 0000000000..aff76bcd41 --- /dev/null +++ b/packages/rln/src/identity.ts @@ -0,0 +1,31 @@ +import { buildBigIntFromUint8Array } from "./utils/index.js"; + +export class IdentityCredential { + public constructor( + public readonly IDTrapdoor: Uint8Array, + public readonly IDNullifier: Uint8Array, + public readonly IDSecretHash: Uint8Array, + public readonly IDCommitment: Uint8Array, + public readonly IDCommitmentBigInt: bigint + ) {} + + public static fromBytes(memKeys: Uint8Array): IdentityCredential { + if (memKeys.length < 128) { + throw new Error("Invalid memKeys length - must be at least 128 bytes"); + } + + const idTrapdoor = memKeys.subarray(0, 32); + const idNullifier = memKeys.subarray(32, 64); + const idSecretHash = memKeys.subarray(64, 96); + const idCommitment = memKeys.subarray(96, 128); + const idCommitmentBigInt = buildBigIntFromUint8Array(idCommitment, 32); + + return new IdentityCredential( + idTrapdoor, + idNullifier, + idSecretHash, + idCommitment, + idCommitmentBigInt + ); + } +} diff --git a/packages/rln/src/index.ts b/packages/rln/src/index.ts new file mode 100644 index 0000000000..efc7c3ebd4 --- /dev/null +++ b/packages/rln/src/index.ts @@ -0,0 +1,30 @@ +import { RLNDecoder, RLNEncoder } from "./codec.js"; +import { + RLN_REGISTRY_ABI, + RLN_STORAGE_ABI, + SEPOLIA_CONTRACT +} from "./contract/index.js"; +import { RLNContract } from "./contract/index.js"; +import { createRLN } from "./create.js"; +import { IdentityCredential } from "./identity.js"; +import { Keystore } from "./keystore/index.js"; +import { Proof } from "./proof.js"; +import { RLNInstance } from "./rln.js"; +import { MerkleRootTracker } from "./root_tracker.js"; +import { extractMetaMaskSigner } from "./utils/index.js"; + +export { + createRLN, + Keystore, + RLNInstance, + IdentityCredential, + Proof, + RLNEncoder, + RLNDecoder, + MerkleRootTracker, + RLNContract, + RLN_STORAGE_ABI, + RLN_REGISTRY_ABI, + SEPOLIA_CONTRACT, + extractMetaMaskSigner +}; diff --git a/packages/rln/src/keystore/cipher.ts b/packages/rln/src/keystore/cipher.ts new file mode 100644 index 0000000000..016da4a1b6 --- /dev/null +++ b/packages/rln/src/keystore/cipher.ts @@ -0,0 +1,54 @@ +import type { IKeystore as IEipKeystore } from "@chainsafe/bls-keystore"; +import { cipherDecrypt } from "@chainsafe/bls-keystore/lib/cipher"; +import { kdf } from "@chainsafe/bls-keystore/lib/kdf"; +import { normalizePassword } from "@chainsafe/bls-keystore/lib/password"; +import { keccak256 } from "ethereum-cryptography/keccak"; +import { + bytesToHex, + concatBytes, + hexToBytes +} from "ethereum-cryptography/utils"; + +import type { Keccak256Hash, Password } from "./types.js"; + +// eipKeystore supports only sha256 checksum so we just make an assumption it is keccak256 +const validateChecksum = async ( + password: Password, + eipKeystore: IEipKeystore +): Promise => { + const computedChecksum = await keccak256Checksum(password, eipKeystore); + return computedChecksum === eipKeystore.crypto.checksum.message; +}; + +// decrypt from @chainsafe/bls-keystore supports only sha256 +// but nwaku uses keccak256 +// https://github.com/waku-org/nwaku/blob/25d6e52e3804d15f9b61bc4cc6dd448540c072a1/waku/waku_keystore/keyfile.nim#L367 +export const decryptEipKeystore = async ( + password: Password, + eipKeystore: IEipKeystore +): Promise => { + const decryptionKey = await kdf( + eipKeystore.crypto.kdf, + normalizePassword(password) + ); + const isChecksumValid = await validateChecksum(password, eipKeystore); + + if (!isChecksumValid) { + throw Error("Password is invalid."); + } + + return cipherDecrypt(eipKeystore.crypto.cipher, decryptionKey.slice(0, 16)); +}; + +export const keccak256Checksum = async ( + password: Password, + eipKeystore: IEipKeystore +): Promise => { + const key = await kdf(eipKeystore.crypto.kdf, normalizePassword(password)); + const payload = concatBytes( + key.slice(16), + hexToBytes(eipKeystore.crypto.cipher.message) + ); + const ciphertext = keccak256(payload); + return bytesToHex(ciphertext); +}; diff --git a/packages/rln/src/keystore/credential_validation_generated.ts b/packages/rln/src/keystore/credential_validation_generated.ts new file mode 100644 index 0000000000..856b1eed81 --- /dev/null +++ b/packages/rln/src/keystore/credential_validation_generated.ts @@ -0,0 +1,7 @@ +/* eslint eslint-comments/no-unlimited-disable: "off" */ +// This file was generated by /scripts/schema-validation-codegen.ts +// Do not modify this file by hand. + +/* eslint-disable */ +// @ts-ignore +"use strict";export const Credential = validate11;const schema12 = {"type":"object","properties":{"crypto":{"type":"object","properties":{"cipher":{"type":"string"},"cipherparams":{"type":"object"},"ciphertext":{"type":"string"},"kdf":{"type":"string"},"kdfparams":{"type":"object"},"mac":{"type":"string"}},"required":["cipher","cipherparams","ciphertext","kdf","kdfparams","mac"]}},"required":["crypto"]};function validate11(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((data.crypto === undefined) && (missing0 = "crypto")){validate11.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {if(data.crypto !== undefined){let data0 = data.crypto;const _errs1 = errors;if(errors === _errs1){if(data0 && typeof data0 == "object" && !Array.isArray(data0)){let missing1;if(((((((data0.cipher === undefined) && (missing1 = "cipher")) || ((data0.cipherparams === undefined) && (missing1 = "cipherparams"))) || ((data0.ciphertext === undefined) && (missing1 = "ciphertext"))) || ((data0.kdf === undefined) && (missing1 = "kdf"))) || ((data0.kdfparams === undefined) && (missing1 = "kdfparams"))) || ((data0.mac === undefined) && (missing1 = "mac"))){validate11.errors = [{instancePath:instancePath+"/crypto",schemaPath:"#/properties/crypto/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"}];return false;}else {if(data0.cipher !== undefined){const _errs3 = errors;if(typeof data0.cipher !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/cipher",schemaPath:"#/properties/crypto/properties/cipher/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs3 === errors;}else {var valid1 = true;}if(valid1){if(data0.cipherparams !== undefined){let data2 = data0.cipherparams;const _errs5 = errors;if(!(data2 && typeof data2 == "object" && !Array.isArray(data2))){validate11.errors = [{instancePath:instancePath+"/crypto/cipherparams",schemaPath:"#/properties/crypto/properties/cipherparams/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid1 = _errs5 === errors;}else {var valid1 = true;}if(valid1){if(data0.ciphertext !== undefined){const _errs7 = errors;if(typeof data0.ciphertext !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/ciphertext",schemaPath:"#/properties/crypto/properties/ciphertext/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs7 === errors;}else {var valid1 = true;}if(valid1){if(data0.kdf !== undefined){const _errs9 = errors;if(typeof data0.kdf !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/kdf",schemaPath:"#/properties/crypto/properties/kdf/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs9 === errors;}else {var valid1 = true;}if(valid1){if(data0.kdfparams !== undefined){let data5 = data0.kdfparams;const _errs11 = errors;if(!(data5 && typeof data5 == "object" && !Array.isArray(data5))){validate11.errors = [{instancePath:instancePath+"/crypto/kdfparams",schemaPath:"#/properties/crypto/properties/kdfparams/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid1 = _errs11 === errors;}else {var valid1 = true;}if(valid1){if(data0.mac !== undefined){const _errs13 = errors;if(typeof data0.mac !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/mac",schemaPath:"#/properties/crypto/properties/mac/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs13 === errors;}else {var valid1 = true;}}}}}}}}else {validate11.errors = [{instancePath:instancePath+"/crypto",schemaPath:"#/properties/crypto/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}}}}else {validate11.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate11.errors = vErrors;return errors === 0;} \ No newline at end of file diff --git a/packages/rln/src/keystore/index.ts b/packages/rln/src/keystore/index.ts new file mode 100644 index 0000000000..e5d4abc5d1 --- /dev/null +++ b/packages/rln/src/keystore/index.ts @@ -0,0 +1,5 @@ +import { Keystore } from "./keystore.js"; +import type { DecryptedCredentials, EncryptedCredentials } from "./types.js"; + +export { Keystore }; +export type { EncryptedCredentials, DecryptedCredentials }; diff --git a/packages/rln/src/keystore/keystore.spec.ts b/packages/rln/src/keystore/keystore.spec.ts new file mode 100644 index 0000000000..bda14ec9e0 --- /dev/null +++ b/packages/rln/src/keystore/keystore.spec.ts @@ -0,0 +1,313 @@ +import * as chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import chaiSubset from "chai-subset"; +import deepEqualInAnyOrder from "deep-equal-in-any-order"; + +const { expect } = chai; + +chai.use(chaiSubset); +chai.use(deepEqualInAnyOrder); +chai.use(chaiAsPromised); + +import { IdentityCredential } from "../identity.js"; +import { buildBigIntFromUint8Array } from "../utils/bytes.js"; + +import { Keystore } from "./keystore.js"; +import type { MembershipInfo } from "./types.js"; + +const DEFAULT_PASSWORD = "sup3rsecure"; +const NWAKU_KEYSTORE = { + application: "waku-rln-relay", + appIdentifier: "01234567890abcdef", + version: "0.2", + credentials: { + "9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548": { + crypto: { + cipher: "aes-128-ctr", + cipherparams: { + iv: "fd6b39eb71d44c59f6bf5ff3d8945c80" + }, + ciphertext: + "9c72f47ce95de03ed34502d0288e7576b66b51b9e7d5ae882c27bd89f94e6a03c2c44c2ddf0c982e72003d67212105f1b64614f57cabb0ceadab7e07be165eee1121ad6b81951368a9f3be2dd99ea294515f6013d5f2bd4702a40e36cfde2ea298b23b31e5ce719d8040c3331f73d6bf44f88bca39bac0e917d8bf545500e4f40d321c235426a80f315ac70666acbd3bdf803fbc1e7e7103fed466525ed332b25d72b2dbedf6fa383b2305987c1fe276b029570519b3e79930edf08c1029868d05c2c08ab61d7c64f63c054b4f6a5a12d43cdc79751b6fe58d3ed26b69443eb7c9f7efce27912340129c91b6b813ac94efd5776a40b1dda896d61357de208c7c47a14af911cc231355c8093ee6626e89c07e1037f9e0b22c690e3e049014399ca0212c509cb04c71c7860d1b17a0c47711c490c27bad2825926148a1f15a507f36ba2cdaa04897fce2914e53caed0beaf1bebd2a83af76511cc15bff2165ff0860ad6eca1f30022d7739b2a6b6a72f2feeef0f5941183cda015b4631469e1f4cf27003cab9a90920301cb30d95e4554686922dc5a05c13dfb575cdf113c700d607896011970e6ee7d6edb61210ab28ac8f0c84c606c097e3e300f0a5f5341edfd15432bef6225a498726b62a98283829ad51023b2987f30686cfb4ea3951f3957654035ec291f9b0964a3a8665d81b16cec20fb40f944d5f9bf03ac1e444ad45bae3fa85e7465ce620c0966d8148d6e2856f676c4fbbe3ebe470453efb4bbda1866680037917e37765f680e3da96ef3991f3fe5cda80c523996c2234758bf5f7b6d052dc6942f5a92c8b8eec5d2d8940203bbb6b1cba7b7ebc1334334ca69cdb509a5ea58ec6b2ebaea52307589eaae9430eb15ad234c0c39c83accdf3b77e52a616e345209c5bc9b442f9f0fa96836d9342f983a7", + kdf: "pbkdf2", + kdfparams: { + dklen: 32, + c: 1000000, + prf: "hmac-sha256", + salt: "60f0aa92fbf63a8356dfdbed2ab18058" + }, + mac: "51a227ac6db7f2797c63925880b3db664e034231a4c68daa919ab42d8df38bc6" + } + }, + "263335559F0578FD785F9CDFEDBB45CFF276799A27580B8F580CDFDCB990257C": { + crypto: { + cipher: "aes-128-ctr", + cipherparams: { + iv: "69f95461f811ac35a21987b1fdaa605e" + }, + ciphertext: + "edfe844f8e2aedd62f26753e7247554920352b6b167f54ea4f728cd715577e9d2b7192b782471914870794205e77c2708b6db2d0ada19fec6b3533098cb2b7350bbaf81526d6bde7f1d0e83c366e3a2ddcced942cfb09a3c7704db7041132c3b511fed2f6d8599e6cddf649250b240687c2c335bf0aa75c892bc97f81c537898aefed20d1488e816d54eec72572acf36f140dc98cba0430cdeb8a00b8e8c8edf9b1292ca0e9c9a606acec51ea3dbe46438cb74b95d708cec18f8f126aecabbff11dd068d9194b25803f959f0bb62d49785dbc694486754f46bfe084cfa780cae27eca48cdcc88f4083d166d1747b8e2e637619e5d3848b9b6cdf7c7161eda8e476edfc083d417691d47b84fb224bfd26bf7713958893b934388e50783e49c5c84999971538ccda14c54b48b0d4aa37503e2a40212e9a1407d5a1ea4e96760de3d87e1b2287465a4e51cf330b7f1d14e3f2fb6521d10d32c798856464927b1e0286086a78f07a8f6f436d8c0c7b530f585320515e276d82c7b1f244702fa9ca6e6ad164fd2b1d9badcbdc17e01e95abf58e6825d8eeba5bc22db3a66dd41c64887d4c862298e921b3bae17d9fb7be1f619c60c82bd60dee351b77514d36e25d4092d6cde8ab613c40a117f7b784c80d65310e5b9cf1a31ba555f848e6984cc0c2d48315167d60131f3ffaaca5c81e359134bbfc81fa217f29b533868604ced4a2c5da8c89bd1238147b9f348168864ebea40c36a6abbf3d59d43086f26777104ce0a9f60cbf350058a337bc66abd5e4976950e5908192f98a9a8c1913abbc0d918479aeaa99e89a0e5cd65fd84a347d73df1d9c829863728a6fcd90150e52ecdec48bd07802110384f6c0aff0ca05ad42feb521223b58719fd4fc4ae88df8225ea58e303e4c61e8288e80f854bf0b", + kdf: "pbkdf2", + kdfparams: { + dklen: 32, + c: 1000000, + prf: "hmac-sha256", + salt: "3cf796e4857f296bef3bdb9ca844b1bf" + }, + mac: "3d6cb0492afcf89c891365f097ae8989dc50038010c419b18228be6816c24c32" + } + }, + F62A7FCE85E5B796AFCB38F54A44210515CB688EA0224E9A436CCA0A542F2C9D: { + crypto: { + cipher: "aes-128-ctr", + cipherparams: { + iv: "49816dacf881c85db9f11f7f068dcf71" + }, + ciphertext: + "d7d805ee24dd34368e3d1829aa6d0856ca2ea54d9bcdc2655f8f197af0d293aff56c6e06de3137b0eddffbcad7cc0b8e3f6ba761ac7983d8e59ce04c8936868297b9f70238cb295e17567f2404b278b93c985496a6e1e46185965491449ccbc1e7155224acdba354ed18b1b9867ff6f1a833a77c9b21e2e9c4b6af27d5bd6303efd574465920928e5c467bd3c7888c3f31e8bece6af2e0c35fa03661399e9b420eeecd4376cb2b3266692f46c03161bb32cc2c79521f7b19cb0e6ec911213e105967f8887d94c73e793b18e4c14ee045dca13fcfb62ae267d3175f8a4fefd0e8bd636bd9431cc0cc7119e75f116a16dcbdcac1c15a3dcec57e1c49dbf5dccd1c75c0cfcb3473e81e8546048ce5231a4d4c8dd5d66311354e9ab70ad5745d5be27746954a08b0b29218562bfb632ae0a498cf09d7955a27377ed7a50fc1b4adaa0a3fb3e87a3b4d923136be0767a1428050944b9fd247332dea1b5016dfa1ec4da167e70e11e07cd58034b8470366dc16d77978b49a61e213ab5a7817fd69af26c2a8c3cd3a488d6e1491e0215071e1f3e9d49d0dfab3a7e324644c98a088e20259980495dcc379dcdce2e61752711bdf8abf057a2e696624078601245828193d838cc806065ee3f2bb138302ec72c70f34f14c0ab816211011f0ac55423732875e220175c717f6bc86f071bb4fab51c1963eb5c5d70d504c1e4d2307a8c8c4b8b5a84566a4606deb3fc6d7a420adc2b2b37c0ef3018f82a3ce0044e082407e8e7cb6214a3abc139b7f75b2c36c6902080e7696c730ab062e75e597274e0c945b6a7a366d20bd210dd02b097071142d033597e2fc4174be683a866510fa1c2fe150a2fb81dbd2b5da25da27f29367fb22dd4e9d4785856e4deea56219f9495fb3ab772f7867db11cb14026b", + kdf: "pbkdf2", + kdfparams: { + dklen: 32, + c: 1000000, + prf: "hmac-sha256", + salt: "2dcb5ba5c98fe5e46d961dad36e79a5b" + }, + mac: "2d6e9de6440f52c5db64b13f80399967c8770e82616294e14f40a2e213e7d925" + } + }, + "8479C6B9125D43E7B7739F1BAB41779F2F5A4D27FF0E2B6F6CA353032010A22C": { + crypto: { + cipher: "aes-128-ctr", + cipherparams: { + iv: "b0eef2c385a04909c4ae9b318e179fa7" + }, + ciphertext: + "90b982222072366566fa194be5c170506888e184cadbd52aa38f184ac4e9bc160cc719d809fb6a128e0cbd908e70a71efdc5d51c4dab8aab71e3e6a2ebd9ea4238cb47585137990e896dfa53961bb2b328abfbba82f49db6a9b6e3790cf9e29c145796c6dbf409dc875e7998db827c944a835a29ab4192a11ad1efde5ebdd1a775ecbfefd139c50fbdcbebd6c124d9d65ba6ddaaa83e57695293e7c85dfd6f418d58fa5ffb9ab9b2395c84b57da796d31b6351fde3f1dbab29da6c3f259859bd0719c34f5111a9a12075b53ee91b4598fb2f452dbea823ec094cb757f370b5386a8e5db25cf732681d0cd9bda651ae55cdd125138fd2c8f1ffe87a5eea14df7d355762b37e3e71c33c6fe46a10c2083538910fec12e294de84ff587cab2dd268203699cb180e481f4a3a093b86854cea64341dd9482305abd4a9d7bb304b078bc255bf7cde78689225f17006f24c2cd82d38a59f1e0899965c38fcfd1ec67069143ee05a34922963a527549a002e3221e1461463f573e5f66ba87dcb83a63cb8e3a721c13cd9d4d0c9a0334a558f32027424a5bc9fc12b91981a3f74ac4b62eea3aae8be6c44504696b96afadce5d9222bb67dddf5a7d98dd43d544d79f8720a946c37eba8eb5ae6d70f4bdbbe554cbd4b3abb35ed357c8cb8f55e016ab83bef12bf5c0cdf26c7624c86f16437f545d796addb1aa7370de329930c68b174c871706e7afdf78cc07e0f0c58e45495d0d3bcf3faf9fb6d20369b0adc89766b0c9132677e52112770d017da7658f2a0c0eaeac57416f203700f98bf7b30119407733d4f0bd4322c622120cdf81646c4a1adfb80e757954e41ba0e7816c403b2e4b9ceb2d36e4198921ea719a410ae6f6983e49e7b99c266deb0465af716799e36a5bab70923291da808edeba54267e31e8b64c37123fd45d86e0638", + kdf: "pbkdf2", + kdfparams: { + dklen: 32, + c: 1000000, + prf: "hmac-sha256", + salt: "142a0a65b7f6f480546cc4ef743d7ef9" + }, + mac: "7119b7b78598850de5f6af742e42748a3b005394b6b8b272490f24527ebd8b15" + } + } + } +}; + +describe("Keystore", () => { + it("shoud create empty store with predefined values", () => { + const store = Keystore.create(); + + expect(store.toObject()).to.deep.eq({ + application: "waku-rln-relay", + appIdentifier: "01234567890abcdef", + version: "0.2", + credentials: {} + }); + }); + + // TODO: add more thorow credentials testing + [ + { + some: "3123", + dadw: "1212" + }, + { + application: 123, + version: "01234567890abcdef", + appIdentifier: "0.2", + credentials: {} + }, + { + application: "waku-rln-relay", + version: 213, + appIdentifier: "0.2", + credentials: {} + }, + { + application: "waku-rln-relay", + version: "01234567890abcdef", + appIdentifier: 12, + credentials: {} + }, + { + application: "waku-rln-relay", + version: "01234567890abcdef", + appIdentifier: "12", + credentials: [] + }, + { + application: "waku-rln-relay", + version: "01234567890abcdef", + appIdentifier: "12", + credentials: 123 + }, + { + application: "waku-rln-relay", + version: "01234567890abcdef", + appIdentifier: "12", + credentials: "123" + }, + { + application: "waku-rln-relay", + version: "01234567890abcdef", + appIdentifier: "12", + credentials: { + hash: { + invalid: "here" + } + } + } + ].map((options) => { + it("should fail to create store from invalid object", () => { + expect(() => Keystore.fromObject(options as any)).to.throw( + "Invalid object, does not match Nwaku Keystore format." + ); + }); + }); + + it("shoud create store from valid object", () => { + const store = Keystore.fromObject(NWAKU_KEYSTORE as any); + expect(store.toObject()).to.deep.eq(NWAKU_KEYSTORE); + }); + + it("should fail to create store from invalid string", () => { + expect(Keystore.fromString("/asdq}")).to.eq(undefined); + expect(Keystore.fromString('{ "name": "it" }')).to.eq(undefined); + }); + + it("shoud create store from valid string", async () => { + const store = Keystore.fromObject(NWAKU_KEYSTORE as any); + expect(store.toObject()).to.deep.eq(NWAKU_KEYSTORE); + }); + + it("should convert keystore to string", async () => { + let store = Keystore.create(); + + expect(store.toString()).to.eq( + JSON.stringify({ + application: "waku-rln-relay", + appIdentifier: "01234567890abcdef", + version: "0.2", + credentials: {} + }) + ); + + store = Keystore.fromObject(NWAKU_KEYSTORE as any); + + expect(store.toString()).to.eq(JSON.stringify(NWAKU_KEYSTORE)); + }); + + it("shoud add / read new credentials", async () => { + const expectedHash = + "9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548"; + const identity = { + IDTrapdoor: new Uint8Array([ + 211, 23, 66, 42, 179, 130, 131, 111, 201, 205, 244, 34, 27, 238, 244, + 216, 131, 240, 188, 45, 193, 172, 4, 168, 225, 225, 43, 197, 114, 176, + 126, 9 + ]), + IDNullifier: new Uint8Array([ + 238, 168, 239, 65, 73, 63, 105, 19, 132, 62, 213, 205, 191, 255, 209, 9, + 178, 155, 239, 201, 131, 125, 233, 136, 246, 217, 9, 237, 55, 89, 81, 42 + ]), + IDSecretHash: new Uint8Array([ + 150, 54, 194, 28, 18, 216, 138, 253, 95, 139, 120, 109, 98, 129, 146, + 101, 41, 194, 36, 36, 96, 152, 152, 89, 151, 160, 118, 15, 222, 124, + 187, 4 + ]), + IDCommitment: new Uint8Array([ + 112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229, + 58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15 + ]), + IDCommitmentBigInt: buildBigIntFromUint8Array( + new Uint8Array([ + 112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229, + 58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, + 15 + ]) + ) + } as unknown as IdentityCredential; + const membership = { + chainId: "0xAA36A7", + treeIndex: 8, + address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71" + } as unknown as MembershipInfo; + + const store = Keystore.create(); + const hash = await store.addCredential( + { identity, membership }, + DEFAULT_PASSWORD + ); + + expect(hash).to.eq(expectedHash); + + const actualCredentials = await store.readCredential( + expectedHash, + DEFAULT_PASSWORD + ); + expect(actualCredentials).to.deep.equalInAnyOrder({ + identity, + membership + }); + }); + + it("shoud fail to add credentials if already exist", async () => { + const identity = { + IDTrapdoor: [ + 211, 23, 66, 42, 179, 130, 131, 111, 201, 205, 244, 34, 27, 238, 244, + 216, 131, 240, 188, 45, 193, 172, 4, 168, 225, 225, 43, 197, 114, 176, + 126, 9 + ], + IDNullifier: [ + 238, 168, 239, 65, 73, 63, 105, 19, 132, 62, 213, 205, 191, 255, 209, 9, + 178, 155, 239, 201, 131, 125, 233, 136, 246, 217, 9, 237, 55, 89, 81, 42 + ], + IDSecretHash: [ + 150, 54, 194, 28, 18, 216, 138, 253, 95, 139, 120, 109, 98, 129, 146, + 101, 41, 194, 36, 36, 96, 152, 152, 89, 151, 160, 118, 15, 222, 124, + 187, 4 + ], + IDCommitment: [ + 112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229, + 58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15 + ] + } as unknown as IdentityCredential; + const membership = { + chainId: "0xAA36A7", + treeIndex: 8, + address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71" + } as unknown as MembershipInfo; + + const store = Keystore.fromObject(NWAKU_KEYSTORE as any); + + try { + await store.addCredential({ identity, membership }, DEFAULT_PASSWORD); + } catch (e) { + expect((e as Error).message).to.eq( + "Credential already exists in the store." + ); + } + }); + + it("shoud fail to read credentials with wrong password", async () => { + const expectedHash = + "9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548"; + const store = Keystore.fromObject(NWAKU_KEYSTORE as any); + + try { + await store.readCredential(expectedHash, "wrong-password"); + } catch (e) { + expect((e as Error).message).to.eq("Password is invalid."); + } + }); + + it("shoud fail to read missing credentials", async () => { + const store = Keystore.fromObject(NWAKU_KEYSTORE as any); + + const result = await store.readCredential("wrong-hash", "wrong-password"); + expect(result).to.eq(undefined); + }); +}); diff --git a/packages/rln/src/keystore/keystore.ts b/packages/rln/src/keystore/keystore.ts new file mode 100644 index 0000000000..84836186bf --- /dev/null +++ b/packages/rln/src/keystore/keystore.ts @@ -0,0 +1,330 @@ +import type { + ICipherModule, + IKeystore as IEipKeystore, + IPbkdf2KdfModule +} from "@chainsafe/bls-keystore"; +import { create as createEipKeystore } from "@chainsafe/bls-keystore"; +import { Logger } from "@waku/utils"; +import { sha256 } from "ethereum-cryptography/sha256"; +import { + bytesToHex, + bytesToUtf8, + utf8ToBytes +} from "ethereum-cryptography/utils"; +import _ from "lodash"; +import { v4 as uuidV4 } from "uuid"; + +import { buildBigIntFromUint8Array } from "../utils/bytes.js"; + +import { decryptEipKeystore, keccak256Checksum } from "./cipher.js"; +import { isCredentialValid, isKeystoreValid } from "./schema_validator.js"; +import type { + Keccak256Hash, + KeystoreEntity, + MembershipHash, + MembershipInfo, + Password, + Sha256Hash +} from "./types.js"; + +const log = new Logger("waku:rln:keystore"); + +type NwakuCredential = { + crypto: { + cipher: ICipherModule["function"]; + cipherparams: ICipherModule["params"]; + ciphertext: ICipherModule["message"]; + kdf: IPbkdf2KdfModule["function"]; + kdfparams: IPbkdf2KdfModule["params"]; + mac: Sha256Hash; + }; +}; + +// examples from nwaku +// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/tests/test_waku_keystore.nim#L43 +// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/keystore.nim#L154C35-L154C38 +// important: each credential has it's own password +// important: not compatible with https://eips.ethereum.org/EIPS/eip-2335 +interface NwakuKeystore { + application: string; + version: string; + appIdentifier: string; + credentials: { + [key: MembershipHash]: NwakuCredential; + }; +} + +type KeystoreCreateOptions = { + application?: string; + version?: string; + appIdentifier?: string; +}; + +export class Keystore { + private data: NwakuKeystore; + + private constructor(options: KeystoreCreateOptions | NwakuKeystore) { + this.data = Object.assign( + { + application: "waku-rln-relay", + appIdentifier: "01234567890abcdef", + version: "0.2", + credentials: {} + }, + options + ); + } + + public static create(options: KeystoreCreateOptions = {}): Keystore { + return new Keystore(options); + } + + // should be valid JSON string that contains Keystore file + // https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/keyfile.nim#L376 + public static fromString(str: string): undefined | Keystore { + try { + const obj = JSON.parse(str); + + if (!Keystore.isValidNwakuStore(obj)) { + throw Error("Invalid string, does not match Nwaku Keystore format."); + } + + return new Keystore(obj); + } catch (err) { + log.error("Cannot create Keystore from string:", err); + return; + } + } + + public static fromObject(obj: NwakuKeystore): Keystore { + if (!Keystore.isValidNwakuStore(obj)) { + throw Error("Invalid object, does not match Nwaku Keystore format."); + } + + return new Keystore(obj); + } + + public async addCredential( + options: KeystoreEntity, + password: Password + ): Promise { + const membershipHash: MembershipHash = Keystore.computeMembershipHash( + options.membership + ); + + if (this.data.credentials[membershipHash]) { + throw Error("Credential already exists in the store."); + } + + // these are not important + const stubPath = "/stub/path"; + const stubPubkey = new Uint8Array([0]); + const secret = Keystore.fromIdentityToBytes(options); + + const eipKeystore = await createEipKeystore( + password, + secret, + stubPubkey, + stubPath + ); + // need to re-compute checksum since nwaku uses keccak256 instead of sha256 + const checksum = await keccak256Checksum(password, eipKeystore); + const nwakuCredential = Keystore.fromEipToCredential(eipKeystore, checksum); + + this.data.credentials[membershipHash] = nwakuCredential; + return membershipHash; + } + + public async readCredential( + membershipHash: MembershipHash, + password: Password + ): Promise { + const nwakuCredential = this.data.credentials[membershipHash]; + + if (!nwakuCredential) { + return; + } + + const eipKeystore = Keystore.fromCredentialToEip(nwakuCredential); + const bytes = await decryptEipKeystore(password, eipKeystore); + + return Keystore.fromBytesToIdentity(bytes); + } + + public removeCredential(hash: MembershipHash): void { + if (!this.data.credentials[hash]) { + return; + } + + delete this.data.credentials[hash]; + } + + public toString(): string { + return JSON.stringify(this.data); + } + + public toObject(): NwakuKeystore { + return this.data; + } + + /** + * Read array of hashes of current credentials + * @returns array of keys of credentials in current Keystore + */ + public keys(): string[] { + return Object.keys(this.toObject().credentials || {}); + } + + private static isValidNwakuStore(obj: unknown): boolean { + if (!isKeystoreValid(obj)) { + return false; + } + + const areCredentialsValid = Object.values(_.get(obj, "credentials", {})) + .map((c) => isCredentialValid(c)) + .every((v) => v); + + return areCredentialsValid; + } + + private static fromCredentialToEip( + credential: NwakuCredential + ): IEipKeystore { + const nwakuCrypto = credential.crypto; + const eipCrypto: IEipKeystore["crypto"] = { + kdf: { + function: nwakuCrypto.kdf, + params: nwakuCrypto.kdfparams, + message: "" + }, + cipher: { + function: nwakuCrypto.cipher, + params: nwakuCrypto.cipherparams, + message: nwakuCrypto.ciphertext + }, + checksum: { + // @chainsafe/bls-keystore supports only sha256 + // but nwaku uses keccak256 + // https://github.com/waku-org/nwaku/blob/25d6e52e3804d15f9b61bc4cc6dd448540c072a1/waku/waku_keystore/keyfile.nim#L367 + function: "sha256", + params: {}, + message: nwakuCrypto.mac + } + }; + + return { + version: 4, + uuid: uuidV4(), + description: undefined, + path: "safe to ignore, not important for decrypt", + pubkey: "safe to ignore, not important for decrypt", + crypto: eipCrypto + }; + } + + private static fromEipToCredential( + eipKeystore: IEipKeystore, + checksum: Keccak256Hash + ): NwakuCredential { + const eipCrypto = eipKeystore.crypto; + const eipKdf = eipCrypto.kdf as IPbkdf2KdfModule; + return { + crypto: { + cipher: eipCrypto.cipher.function, + cipherparams: eipCrypto.cipher.params, + ciphertext: eipCrypto.cipher.message, + kdf: eipKdf.function, + kdfparams: eipKdf.params, + // @chainsafe/bls-keystore generates only sha256 + // but nwaku uses keccak256 + // https://github.com/waku-org/nwaku/blob/25d6e52e3804d15f9b61bc4cc6dd448540c072a1/waku/waku_keystore/keyfile.nim#L367 + mac: checksum + } + }; + } + + private static fromBytesToIdentity( + bytes: Uint8Array + ): undefined | KeystoreEntity { + try { + const str = bytesToUtf8(bytes); + const obj = JSON.parse(str); + + // TODO: add runtime validation of nwaku credentials + return { + identity: { + IDCommitment: Keystore.fromArraylikeToBytes( + _.get(obj, "identityCredential.idCommitment", []) + ), + IDTrapdoor: Keystore.fromArraylikeToBytes( + _.get(obj, "identityCredential.idTrapdoor", []) + ), + IDNullifier: Keystore.fromArraylikeToBytes( + _.get(obj, "identityCredential.idNullifier", []) + ), + IDCommitmentBigInt: buildBigIntFromUint8Array( + Keystore.fromArraylikeToBytes( + _.get(obj, "identityCredential.idCommitment", []) + ) + ), + IDSecretHash: Keystore.fromArraylikeToBytes( + _.get(obj, "identityCredential.idSecretHash", []) + ) + }, + membership: { + treeIndex: _.get(obj, "treeIndex"), + chainId: _.get(obj, "membershipContract.chainId"), + address: _.get(obj, "membershipContract.address") + } + }; + } catch (err) { + log.error("Cannot parse bytes to Nwaku Credentials:", err); + return; + } + } + + private static fromArraylikeToBytes(obj: { + [key: number]: number; + }): Uint8Array { + const bytes = []; + + let index = 0; + let lastElement = obj[index]; + + while (lastElement !== undefined) { + bytes.push(lastElement); + index += 1; + lastElement = obj[index]; + } + + return new Uint8Array(bytes); + } + + // follows nwaku implementation + // https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L111 + private static computeMembershipHash(info: MembershipInfo): MembershipHash { + return bytesToHex( + sha256(utf8ToBytes(`${info.chainId}${info.address}${info.treeIndex}`)) + ).toUpperCase(); + } + + // follows nwaku implementation + // https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L98 + private static fromIdentityToBytes(options: KeystoreEntity): Uint8Array { + return utf8ToBytes( + JSON.stringify({ + treeIndex: options.membership.treeIndex, + identityCredential: { + idCommitment: options.identity.IDCommitment, + idNullifier: options.identity.IDNullifier, + idSecretHash: options.identity.IDSecretHash, + idTrapdoor: options.identity.IDTrapdoor + }, + membershipContract: { + chainId: options.membership.chainId, + address: options.membership.address + } + }) + ); + } +} diff --git a/packages/rln/src/keystore/keystore_validation_generated.ts b/packages/rln/src/keystore/keystore_validation_generated.ts new file mode 100644 index 0000000000..4d88251d0a --- /dev/null +++ b/packages/rln/src/keystore/keystore_validation_generated.ts @@ -0,0 +1,7 @@ +/* eslint eslint-comments/no-unlimited-disable: "off" */ +// This file was generated by /scripts/schema-validation-codegen.ts +// Do not modify this file by hand. + +/* eslint-disable */ +// @ts-ignore +"use strict";export const Keystore = validate11;const schema12 = {"type":"object","properties":{"credentials":{"type":"object"},"appIdentifier":{"type":"string"},"version":{"type":"string"},"application":{"type":"string"}},"required":["application","appIdentifier","credentials","version"]};function validate11(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((((data.application === undefined) && (missing0 = "application")) || ((data.appIdentifier === undefined) && (missing0 = "appIdentifier"))) || ((data.credentials === undefined) && (missing0 = "credentials"))) || ((data.version === undefined) && (missing0 = "version"))){validate11.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {if(data.credentials !== undefined){let data0 = data.credentials;const _errs1 = errors;if(!(data0 && typeof data0 == "object" && !Array.isArray(data0))){validate11.errors = [{instancePath:instancePath+"/credentials",schemaPath:"#/properties/credentials/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid0 = _errs1 === errors;}else {var valid0 = true;}if(valid0){if(data.appIdentifier !== undefined){const _errs3 = errors;if(typeof data.appIdentifier !== "string"){validate11.errors = [{instancePath:instancePath+"/appIdentifier",schemaPath:"#/properties/appIdentifier/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.version !== undefined){const _errs5 = errors;if(typeof data.version !== "string"){validate11.errors = [{instancePath:instancePath+"/version",schemaPath:"#/properties/version/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.application !== undefined){const _errs7 = errors;if(typeof data.application !== "string"){validate11.errors = [{instancePath:instancePath+"/application",schemaPath:"#/properties/application/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs7 === errors;}else {var valid0 = true;}}}}}}else {validate11.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate11.errors = vErrors;return errors === 0;} \ No newline at end of file diff --git a/packages/rln/src/keystore/schema_validator.ts b/packages/rln/src/keystore/schema_validator.ts new file mode 100644 index 0000000000..d33895d160 --- /dev/null +++ b/packages/rln/src/keystore/schema_validator.ts @@ -0,0 +1,34 @@ +import { Credential as _validateCredentialGenerated } from "./credential_validation_generated.js"; +import { Keystore as _validateKeystoreGenerated } from "./keystore_validation_generated.js"; + +type ErrorObject = { + instancePath: string; + schemaPath: string; + keyword: string; + params: object; + message: string; +}; + +type ValidatorFn = ((data: unknown) => boolean) & { errors: ErrorObject[] }; + +const _validateKeystore = _validateKeystoreGenerated as ValidatorFn; +const _validateCredential = _validateCredentialGenerated as ValidatorFn; + +function schemaValidationErrors( + validator: ValidatorFn, + data: unknown +): ErrorObject[] | null { + const validated = validator(data); + if (validated) { + return null; + } + return validator.errors; +} + +export function isKeystoreValid(keystore: unknown): boolean { + return !schemaValidationErrors(_validateKeystore, keystore); +} + +export function isCredentialValid(credential: unknown): boolean { + return !schemaValidationErrors(_validateCredential, credential); +} diff --git a/packages/rln/src/keystore/types.ts b/packages/rln/src/keystore/types.ts new file mode 100644 index 0000000000..9d8e86a891 --- /dev/null +++ b/packages/rln/src/keystore/types.ts @@ -0,0 +1,36 @@ +import type { IdentityCredential } from "../identity.js"; + +export type MembershipHash = string; +export type Sha256Hash = string; +export type Keccak256Hash = string; +export type Password = string | Uint8Array; + +// see reference +// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L111 +export type MembershipInfo = { + chainId: number; + address: string; + treeIndex: number; +}; + +export type KeystoreEntity = { + identity: IdentityCredential; + membership: MembershipInfo; +}; + +export type DecryptedCredentials = KeystoreEntity; + +export type EncryptedCredentials = { + /** + * Valid JSON string that contains Keystore + */ + keystore: string; + /** + * ID of credentials from provided Keystore to use + */ + id: string; + /** + * Password to decrypt credentials provided + */ + password: Password; +}; diff --git a/packages/rln/src/message.ts b/packages/rln/src/message.ts new file mode 100644 index 0000000000..20a65502f6 --- /dev/null +++ b/packages/rln/src/message.ts @@ -0,0 +1,70 @@ +import type { + IDecodedMessage, + IMessage, + IRateLimitProof +} from "@waku/interfaces"; +import * as utils from "@waku/utils/bytes"; + +import { RLNInstance } from "./rln.js"; +import { epochBytesToInt } from "./utils/index.js"; + +export function toRLNSignal(contentTopic: string, msg: IMessage): Uint8Array { + const contentTopicBytes = utils.utf8ToBytes(contentTopic ?? ""); + return new Uint8Array([...(msg.payload ?? []), ...contentTopicBytes]); +} + +export class RlnMessage implements IDecodedMessage { + public pubsubTopic = ""; + + public constructor( + public rlnInstance: RLNInstance, + public msg: T, + public rateLimitProof: IRateLimitProof | undefined + ) {} + + public verify(roots: Uint8Array[]): boolean | undefined { + return this.rateLimitProof + ? this.rlnInstance.zerokit.verifyWithRoots( + this.rateLimitProof, + toRLNSignal(this.msg.contentTopic, this.msg), + ...roots + ) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed + : undefined; + } + + public verifyNoRoot(): boolean | undefined { + return this.rateLimitProof + ? this.rlnInstance.zerokit.verifyWithNoRoot( + this.rateLimitProof, + toRLNSignal(this.msg.contentTopic, this.msg) + ) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed + : undefined; + } + + public get payload(): Uint8Array { + return this.msg.payload; + } + + public get contentTopic(): string { + return this.msg.contentTopic; + } + + public get timestamp(): Date | undefined { + return this.msg.timestamp; + } + + public get ephemeral(): boolean | undefined { + return this.msg.ephemeral; + } + + public get meta(): Uint8Array | undefined { + return this.msg.meta; + } + + public get epoch(): number | undefined { + const bytes = this.rateLimitProof?.epoch; + if (!bytes) return undefined; + + return epochBytesToInt(bytes); + } +} diff --git a/packages/rln/src/proof.ts b/packages/rln/src/proof.ts new file mode 100644 index 0000000000..23a75acc37 --- /dev/null +++ b/packages/rln/src/proof.ts @@ -0,0 +1,69 @@ +import type { IRateLimitProof } from "@waku/interfaces"; + +import { concatenate, poseidonHash } from "./utils/index.js"; + +const proofOffset = 128; +const rootOffset = proofOffset + 32; +const epochOffset = rootOffset + 32; +const shareXOffset = epochOffset + 32; +const shareYOffset = shareXOffset + 32; +const nullifierOffset = shareYOffset + 32; +const rlnIdentifierOffset = nullifierOffset + 32; + +class ProofMetadata { + public constructor( + public readonly nullifier: Uint8Array, + public readonly shareX: Uint8Array, + public readonly shareY: Uint8Array, + public readonly externalNullifier: Uint8Array + ) {} +} + +export class Proof implements IRateLimitProof { + public readonly proof: Uint8Array; + public readonly merkleRoot: Uint8Array; + public readonly epoch: Uint8Array; + public readonly shareX: Uint8Array; + public readonly shareY: Uint8Array; + public readonly nullifier: Uint8Array; + public readonly rlnIdentifier: Uint8Array; + + public constructor(proofBytes: Uint8Array) { + if (proofBytes.length < rlnIdentifierOffset) { + throw new Error("invalid proof"); + } + // parse the proof as proof<128> | share_y<32> | nullifier<32> | root<32> | epoch<32> | share_x<32> | rln_identifier<32> + this.proof = proofBytes.subarray(0, proofOffset); + this.merkleRoot = proofBytes.subarray(proofOffset, rootOffset); + this.epoch = proofBytes.subarray(rootOffset, epochOffset); + this.shareX = proofBytes.subarray(epochOffset, shareXOffset); + this.shareY = proofBytes.subarray(shareXOffset, shareYOffset); + this.nullifier = proofBytes.subarray(shareYOffset, nullifierOffset); + this.rlnIdentifier = proofBytes.subarray( + nullifierOffset, + rlnIdentifierOffset + ); + } + + public extractMetadata(): ProofMetadata { + const externalNullifier = poseidonHash(this.epoch, this.rlnIdentifier); + return new ProofMetadata( + this.nullifier, + this.shareX, + this.shareY, + externalNullifier + ); + } +} + +export function proofToBytes(p: IRateLimitProof): Uint8Array { + return concatenate( + p.proof, + p.merkleRoot, + p.epoch, + p.shareX, + p.shareY, + p.nullifier, + p.rlnIdentifier + ); +} diff --git a/packages/rln/src/resources/rln.wasm b/packages/rln/src/resources/rln.wasm new file mode 100644 index 0000000000..04aaeef783 Binary files /dev/null and b/packages/rln/src/resources/rln.wasm differ diff --git a/packages/rln/src/resources/rln_final.zkey b/packages/rln/src/resources/rln_final.zkey new file mode 100644 index 0000000000..c6cc7d491a Binary files /dev/null and b/packages/rln/src/resources/rln_final.zkey differ diff --git a/packages/rln/src/resources/verification_key.d.ts b/packages/rln/src/resources/verification_key.d.ts new file mode 100644 index 0000000000..99ec4f67d1 --- /dev/null +++ b/packages/rln/src/resources/verification_key.d.ts @@ -0,0 +1,13 @@ +declare const verificationKey: { + protocol: string; + curve: string; + nPublic: number; + vk_alpha_1: string[]; + vk_beta_2: string[][]; + vk_gamma_2: string[][]; + vk_delta_2: string[][]; + vk_alphabeta_12: string[][][]; + IC: string[][]; +}; + +export default verificationKey; diff --git a/packages/rln/src/resources/verification_key.js b/packages/rln/src/resources/verification_key.js new file mode 100644 index 0000000000..15425bef8c --- /dev/null +++ b/packages/rln/src/resources/verification_key.js @@ -0,0 +1,112 @@ +const verificationKey = { + protocol: "groth16", + curve: "bn128", + nPublic: 6, + vk_alpha_1: [ + "20124996762962216725442980738609010303800849578410091356605067053491763969391", + "9118593021526896828671519912099489027245924097793322973632351264852174143923", + "1" + ], + vk_beta_2: [ + [ + "4693952934005375501364248788849686435240706020501681709396105298107971354382", + "14346958885444710485362620645446987998958218205939139994511461437152241966681" + ], + [ + "16851772916911573982706166384196538392731905827088356034885868448550849804972", + "823612331030938060799959717749043047845343400798220427319188951998582076532" + ], + ["1", "0"] + ], + vk_gamma_2: [ + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634" + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531" + ], + ["1", "0"] + ], + vk_delta_2: [ + [ + "8353516066399360694538747105302262515182301251524941126222712285088022964076", + "9329524012539638256356482961742014315122377605267454801030953882967973561832" + ], + [ + "16805391589556134376869247619848130874761233086443465978238468412168162326401", + "10111259694977636294287802909665108497237922060047080343914303287629927847739" + ], + ["1", "0"] + ], + vk_alphabeta_12: [ + [ + [ + "12608968655665301215455851857466367636344427685631271961542642719683786103711", + "9849575605876329747382930567422916152871921500826003490242628251047652318086" + ], + [ + "6322029441245076030714726551623552073612922718416871603535535085523083939021", + "8700115492541474338049149013125102281865518624059015445617546140629435818912" + ], + [ + "10674973475340072635573101639867487770811074181475255667220644196793546640210", + "2926286967251299230490668407790788696102889214647256022788211245826267484824" + ] + ], + [ + [ + "9660441540778523475944706619139394922744328902833875392144658911530830074820", + "19548113127774514328631808547691096362144426239827206966690021428110281506546" + ], + [ + "1870837942477655969123169532603615788122896469891695773961478956740992497097", + "12536105729661705698805725105036536744930776470051238187456307227425796690780" + ], + [ + "21811903352654147452884857281720047789720483752548991551595462057142824037334", + "19021616763967199151052893283384285352200445499680068407023236283004353578353" + ] + ] + ], + IC: [ + [ + "11992897507809711711025355300535923222599547639134311050809253678876341466909", + "17181525095924075896332561978747020491074338784673526378866503154966799128110", + "1" + ], + [ + "17018665030246167677911144513385572506766200776123272044534328594850561667818", + "18601114175490465275436712413925513066546725461375425769709566180981674884464", + "1" + ], + [ + "18799470100699658367834559797874857804183288553462108031963980039244731716542", + "13064227487174191981628537974951887429496059857753101852163607049188825592007", + "1" + ], + [ + "17432501889058124609368103715904104425610382063762621017593209214189134571156", + "13406815149699834788256141097399354592751313348962590382887503595131085938635", + "1" + ], + [ + "10320964835612716439094703312987075811498239445882526576970512041988148264481", + "9024164961646353611176283204118089412001502110138072989569118393359029324867", + "1" + ], + [ + "718355081067365548229685160476620267257521491773976402837645005858953849298", + "14635482993933988261008156660773180150752190597753512086153001683711587601974", + "1" + ], + [ + "11777720285956632126519898515392071627539405001940313098390150593689568177535", + "8483603647274280691250972408211651407952870456587066148445913156086740744515", + "1" + ] + ] +}; + +export default verificationKey; diff --git a/packages/rln/src/resources/witness_calculator.d.ts b/packages/rln/src/resources/witness_calculator.d.ts new file mode 100644 index 0000000000..eb6f86aab9 --- /dev/null +++ b/packages/rln/src/resources/witness_calculator.d.ts @@ -0,0 +1,11 @@ +export async function builder( + code: Uint8Array, + sanityCheck: boolean +): Promise; + +export class WitnessCalculator { + public calculateWitness( + input: unknown, + sanityCheck: boolean + ): Promise>; +} diff --git a/packages/rln/src/resources/witness_calculator.js b/packages/rln/src/resources/witness_calculator.js new file mode 100644 index 0000000000..47b218d881 --- /dev/null +++ b/packages/rln/src/resources/witness_calculator.js @@ -0,0 +1,328 @@ +// File generated with https://github.com/iden3/circom +// following the instructions from: +// https://github.com/vacp2p/zerokit/tree/master/rln#compiling-circuits + +export async function builder(code, options) { + options = options || {}; + + let wasmModule; + try { + wasmModule = await WebAssembly.compile(code); + } catch (err) { + console.log(err); + console.log( + "\nTry to run circom --c in order to generate c++ code instead\n" + ); + throw new Error(err); + } + + let wc; + + let errStr = ""; + let msgStr = ""; + + const instance = await WebAssembly.instantiate(wasmModule, { + runtime: { + exceptionHandler: function (code) { + let err; + if (code == 1) { + err = "Signal not found.\n"; + } else if (code == 2) { + err = "Too many signals set.\n"; + } else if (code == 3) { + err = "Signal already set.\n"; + } else if (code == 4) { + err = "Assert Failed.\n"; + } else if (code == 5) { + err = "Not enough memory.\n"; + } else if (code == 6) { + err = "Input signal array access exceeds the size.\n"; + } else { + err = "Unknown error.\n"; + } + throw new Error(err + errStr); + }, + printErrorMessage: function () { + errStr += getMessage() + "\n"; + // console.error(getMessage()); + }, + writeBufferMessage: function () { + const msg = getMessage(); + // Any calls to `log()` will always end with a `\n`, so that's when we print and reset + if (msg === "\n") { + console.log(msgStr); + msgStr = ""; + } else { + // If we've buffered other content, put a space in between the items + if (msgStr !== "") { + msgStr += " "; + } + // Then append the message to the message we are creating + msgStr += msg; + } + }, + showSharedRWMemory: function () { + printSharedRWMemory(); + } + } + }); + + const sanityCheck = options; + // options && + // ( + // options.sanityCheck || + // options.logGetSignal || + // options.logSetSignal || + // options.logStartComponent || + // options.logFinishComponent + // ); + + wc = new WitnessCalculator(instance, sanityCheck); + return wc; + + function getMessage() { + var message = ""; + var c = instance.exports.getMessageChar(); + while (c != 0) { + message += String.fromCharCode(c); + c = instance.exports.getMessageChar(); + } + return message; + } + + function printSharedRWMemory() { + const shared_rw_memory_size = instance.exports.getFieldNumLen32(); + const arr = new Uint32Array(shared_rw_memory_size); + for (let j = 0; j < shared_rw_memory_size; j++) { + arr[shared_rw_memory_size - 1 - j] = + instance.exports.readSharedRWMemory(j); + } + + // If we've buffered other content, put a space in between the items + if (msgStr !== "") { + msgStr += " "; + } + // Then append the value to the message we are creating + msgStr += fromArray32(arr).toString(); + } +} + +class WitnessCalculator { + constructor(instance, sanityCheck) { + this.instance = instance; + + this.version = this.instance.exports.getVersion(); + this.n32 = this.instance.exports.getFieldNumLen32(); + + this.instance.exports.getRawPrime(); + const arr = new Uint32Array(this.n32); + for (let i = 0; i < this.n32; i++) { + arr[this.n32 - 1 - i] = this.instance.exports.readSharedRWMemory(i); + } + this.prime = fromArray32(arr); + + this.witnessSize = this.instance.exports.getWitnessSize(); + + this.sanityCheck = sanityCheck; + } + + circom_version() { + return this.instance.exports.getVersion(); + } + + async _doCalculateWitness(input, sanityCheck) { + //input is assumed to be a map from signals to arrays of bigints + this.instance.exports.init(this.sanityCheck || sanityCheck ? 1 : 0); + const keys = Object.keys(input); + var input_counter = 0; + keys.forEach((k) => { + const h = fnvHash(k); + const hMSB = parseInt(h.slice(0, 8), 16); + const hLSB = parseInt(h.slice(8, 16), 16); + const fArr = flatArray(input[k]); + let signalSize = this.instance.exports.getInputSignalSize(hMSB, hLSB); + if (signalSize < 0) { + throw new Error(`Signal ${k} not found\n`); + } + if (fArr.length < signalSize) { + throw new Error(`Not enough values for input signal ${k}\n`); + } + if (fArr.length > signalSize) { + throw new Error(`Too many values for input signal ${k}\n`); + } + for (let i = 0; i < fArr.length; i++) { + const arrFr = toArray32(BigInt(fArr[i]) % this.prime, this.n32); + for (let j = 0; j < this.n32; j++) { + this.instance.exports.writeSharedRWMemory(j, arrFr[this.n32 - 1 - j]); + } + try { + this.instance.exports.setInputSignal(hMSB, hLSB, i); + input_counter++; + } catch (err) { + // console.log(`After adding signal ${i} of ${k}`) + throw new Error(err); + } + } + }); + if (input_counter < this.instance.exports.getInputSize()) { + throw new Error( + `Not all inputs have been set. Only ${input_counter} out of ${this.instance.exports.getInputSize()}` + ); + } + } + + async calculateWitness(input, sanityCheck) { + const w = []; + + await this._doCalculateWitness(input, sanityCheck); + + for (let i = 0; i < this.witnessSize; i++) { + this.instance.exports.getWitness(i); + const arr = new Uint32Array(this.n32); + for (let j = 0; j < this.n32; j++) { + arr[this.n32 - 1 - j] = this.instance.exports.readSharedRWMemory(j); + } + w.push(fromArray32(arr)); + } + + return w; + } + + async calculateBinWitness(input, sanityCheck) { + const buff32 = new Uint32Array(this.witnessSize * this.n32); + const buff = new Uint8Array(buff32.buffer); + await this._doCalculateWitness(input, sanityCheck); + + for (let i = 0; i < this.witnessSize; i++) { + this.instance.exports.getWitness(i); + const pos = i * this.n32; + for (let j = 0; j < this.n32; j++) { + buff32[pos + j] = this.instance.exports.readSharedRWMemory(j); + } + } + + return buff; + } + + async calculateWTNSBin(input, sanityCheck) { + const buff32 = new Uint32Array(this.witnessSize * this.n32 + this.n32 + 11); + const buff = new Uint8Array(buff32.buffer); + await this._doCalculateWitness(input, sanityCheck); + + //"wtns" + buff[0] = "w".charCodeAt(0); + buff[1] = "t".charCodeAt(0); + buff[2] = "n".charCodeAt(0); + buff[3] = "s".charCodeAt(0); + + //version 2 + buff32[1] = 2; + + //number of sections: 2 + buff32[2] = 2; + + //id section 1 + buff32[3] = 1; + + const n8 = this.n32 * 4; + //id section 1 length in 64bytes + const idSection1length = 8 + n8; + const idSection1lengthHex = idSection1length.toString(16); + buff32[4] = parseInt(idSection1lengthHex.slice(0, 8), 16); + buff32[5] = parseInt(idSection1lengthHex.slice(8, 16), 16); + + //this.n32 + buff32[6] = n8; + + //prime number + this.instance.exports.getRawPrime(); + + var pos = 7; + for (let j = 0; j < this.n32; j++) { + buff32[pos + j] = this.instance.exports.readSharedRWMemory(j); + } + pos += this.n32; + + // witness size + buff32[pos] = this.witnessSize; + pos++; + + //id section 2 + buff32[pos] = 2; + pos++; + + // section 2 length + const idSection2length = n8 * this.witnessSize; + const idSection2lengthHex = idSection2length.toString(16); + buff32[pos] = parseInt(idSection2lengthHex.slice(0, 8), 16); + buff32[pos + 1] = parseInt(idSection2lengthHex.slice(8, 16), 16); + + pos += 2; + for (let i = 0; i < this.witnessSize; i++) { + this.instance.exports.getWitness(i); + for (let j = 0; j < this.n32; j++) { + buff32[pos + j] = this.instance.exports.readSharedRWMemory(j); + } + pos += this.n32; + } + + return buff; + } +} + +function toArray32(rem, size) { + const res = []; //new Uint32Array(size); //has no unshift + const radix = BigInt(0x100000000); + while (rem) { + res.unshift(Number(rem % radix)); + rem = rem / radix; + } + if (size) { + var i = size - res.length; + while (i > 0) { + res.unshift(0); + i--; + } + } + return res; +} + +function fromArray32(arr) { + //returns a BigInt + var res = BigInt(0); + const radix = BigInt(0x100000000); + for (let i = 0; i < arr.length; i++) { + res = res * radix + BigInt(arr[i]); + } + return res; +} + +function flatArray(a) { + var res = []; + fillArray(res, a); + return res; + + function fillArray(res, a) { + if (Array.isArray(a)) { + for (let i = 0; i < a.length; i++) { + fillArray(res, a[i]); + } + } else { + res.push(a); + } + } +} + +function fnvHash(str) { + const uint64_max = BigInt(2) ** BigInt(64); + let hash = BigInt("0xCBF29CE484222325"); + for (var i = 0; i < str.length; i++) { + hash ^= BigInt(str[i].charCodeAt()); + hash *= BigInt(0x100000001b3); + hash %= uint64_max; + } + let shash = hash.toString(16); + let n = 16 - shash.length; + shash = "0".repeat(n).concat(shash); + return shash; +} diff --git a/packages/rln/src/rln.ts b/packages/rln/src/rln.ts new file mode 100644 index 0000000000..dd6b0be47e --- /dev/null +++ b/packages/rln/src/rln.ts @@ -0,0 +1,298 @@ +import { createDecoder, createEncoder } from "@waku/core"; +import type { + ContentTopic, + IDecodedMessage, + EncoderOptions as WakuEncoderOptions +} from "@waku/interfaces"; +import { Logger } from "@waku/utils"; +import init from "@waku/zerokit-rln-wasm"; +import * as zerokitRLN from "@waku/zerokit-rln-wasm"; +import { ethers } from "ethers"; + +import { + createRLNDecoder, + createRLNEncoder, + type RLNDecoder, + type RLNEncoder +} from "./codec.js"; +import { RLNContract, SEPOLIA_CONTRACT } from "./contract/index.js"; +import { IdentityCredential } from "./identity.js"; +import { Keystore } from "./keystore/index.js"; +import type { + DecryptedCredentials, + EncryptedCredentials +} from "./keystore/index.js"; +import { KeystoreEntity, Password } from "./keystore/types.js"; +import verificationKey from "./resources/verification_key"; +import * as wc from "./resources/witness_calculator"; +import { extractMetaMaskSigner } from "./utils/index.js"; +import { Zerokit } from "./zerokit.js"; + +const log = new Logger("waku:rln"); + +async function loadWitnessCalculator(): Promise { + const res = await fetch("/base/rln.wasm"); + if (!res.ok) { + throw new Error(`Failed to fetch rln.wasm: ${res.statusText}`); + } + const witnessBuffer = await res.arrayBuffer(); + return wc.builder(new Uint8Array(witnessBuffer), false); +} + +async function loadZkey(): Promise { + const res = await fetch("/base/rln_final.zkey"); + if (!res.ok) { + throw new Error(`Failed to fetch rln_final.zkey: ${res.statusText}`); + } + return new Uint8Array(await res.arrayBuffer()); +} + +/** + * Create an instance of RLN + * @returns RLNInstance + */ +export async function create(): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (init as any)?.(); + zerokitRLN.init_panic_hook(); + + const witnessCalculator = await loadWitnessCalculator(); + const zkey = await loadZkey(); + + const stringEncoder = new TextEncoder(); + const vkey = stringEncoder.encode(JSON.stringify(verificationKey)); + + const DEPTH = 20; + const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey); + const zerokit = new Zerokit(zkRLN, witnessCalculator); + + return new RLNInstance(zerokit); + } catch (error) { + log.error("Failed to initialize RLN:", error); + throw error; + } +} + +type StartRLNOptions = { + /** + * If not set - will extract MetaMask account and get signer from it. + */ + signer?: ethers.Signer; + /** + * If not set - will use default SEPOLIA_CONTRACT address. + */ + registryAddress?: string; + /** + * Credentials to use for generating proofs and connecting to the contract and network. + * If provided used for validating the network chainId and connecting to registry contract. + */ + credentials?: EncryptedCredentials | DecryptedCredentials; +}; + +type RegisterMembershipOptions = + | { signature: string } + | { identity: IdentityCredential }; + +type WakuRLNEncoderOptions = WakuEncoderOptions & { + credentials: EncryptedCredentials | DecryptedCredentials; +}; + +export class RLNInstance { + private started = false; + private starting = false; + + private _contract: undefined | RLNContract; + private _signer: undefined | ethers.Signer; + + private keystore = Keystore.create(); + private _credentials: undefined | DecryptedCredentials; + + public constructor(public zerokit: Zerokit) {} + + public get contract(): undefined | RLNContract { + return this._contract; + } + + public get signer(): undefined | ethers.Signer { + return this._signer; + } + + public async start(options: StartRLNOptions = {}): Promise { + if (this.started || this.starting) { + return; + } + + this.starting = true; + + try { + const { credentials, keystore } = + await RLNInstance.decryptCredentialsIfNeeded(options.credentials); + const { signer, registryAddress } = await this.determineStartOptions( + options, + credentials + ); + + if (keystore) { + this.keystore = keystore; + } + + this._credentials = credentials; + this._signer = signer!; + this._contract = await RLNContract.init(this, { + registryAddress: registryAddress!, + signer: signer! + }); + this.started = true; + } finally { + this.starting = false; + } + } + + private async determineStartOptions( + options: StartRLNOptions, + credentials: KeystoreEntity | undefined + ): Promise { + let chainId = credentials?.membership.chainId; + const registryAddress = + credentials?.membership.address || + options.registryAddress || + SEPOLIA_CONTRACT.address; + + if (registryAddress === SEPOLIA_CONTRACT.address) { + chainId = SEPOLIA_CONTRACT.chainId; + } + + const signer = options.signer || (await extractMetaMaskSigner()); + const currentChainId = await signer.getChainId(); + + if (chainId && chainId !== currentChainId) { + throw Error( + `Failed to start RLN contract, chain ID of contract is different from current one: contract-${chainId}, current network-${currentChainId}` + ); + } + + return { + signer, + registryAddress + }; + } + + private static async decryptCredentialsIfNeeded( + credentials?: EncryptedCredentials | DecryptedCredentials + ): Promise<{ credentials?: DecryptedCredentials; keystore?: Keystore }> { + if (!credentials) { + return {}; + } + + if ("identity" in credentials) { + return { credentials }; + } + + const keystore = Keystore.fromString(credentials.keystore); + + if (!keystore) { + return {}; + } + + const decryptedCredentials = await keystore.readCredential( + credentials.id, + credentials.password + ); + + return { + keystore, + credentials: decryptedCredentials + }; + } + + public async registerMembership( + options: RegisterMembershipOptions + ): Promise { + if (!this.contract) { + throw Error("RLN Contract is not initialized."); + } + + let identity = "identity" in options && options.identity; + + if ("signature" in options) { + identity = this.zerokit.generateSeededIdentityCredential( + options.signature + ); + } + + if (!identity) { + throw Error("Missing signature or identity to register membership."); + } + + return this.contract.registerWithIdentity(identity); + } + + /** + * Changes credentials in use by relying on provided Keystore earlier in rln.start + * @param id: string, hash of credentials to select from Keystore + * @param password: string or bytes to use to decrypt credentials from Keystore + */ + public async useCredentials(id: string, password: Password): Promise { + this._credentials = await this.keystore?.readCredential(id, password); + } + + public async createEncoder( + options: WakuRLNEncoderOptions + ): Promise { + const { credentials: decryptedCredentials } = + await RLNInstance.decryptCredentialsIfNeeded(options.credentials); + const credentials = decryptedCredentials || this._credentials; + + if (!credentials) { + throw Error( + "Failed to create Encoder: missing RLN credentials. Use createRLNEncoder directly." + ); + } + + await this.verifyCredentialsAgainstContract(credentials); + + return createRLNEncoder({ + encoder: createEncoder(options), + rlnInstance: this, + index: credentials.membership.treeIndex, + credential: credentials.identity + }); + } + + private async verifyCredentialsAgainstContract( + credentials: KeystoreEntity + ): Promise { + if (!this._contract) { + throw Error( + "Failed to verify chain coordinates: no contract initialized." + ); + } + + const registryAddress = credentials.membership.address; + const currentRegistryAddress = this._contract.registry.address; + if (registryAddress !== currentRegistryAddress) { + throw Error( + `Failed to verify chain coordinates: credentials contract address=${registryAddress} is not equal to registryContract address=${currentRegistryAddress}` + ); + } + + const chainId = credentials.membership.chainId; + const network = await this._contract.registry.provider.getNetwork(); + const currentChainId = network.chainId; + if (chainId !== currentChainId) { + throw Error( + `Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}` + ); + } + } + + public createDecoder( + contentTopic: ContentTopic + ): RLNDecoder { + return createRLNDecoder({ + rlnInstance: this, + decoder: createDecoder(contentTopic) + }); + } +} diff --git a/packages/rln/src/root_tracker.spec.ts b/packages/rln/src/root_tracker.spec.ts new file mode 100644 index 0000000000..8e3c03efe0 --- /dev/null +++ b/packages/rln/src/root_tracker.spec.ts @@ -0,0 +1,56 @@ +import { assert, expect } from "chai"; + +import { MerkleRootTracker } from "./root_tracker.js"; + +describe("js-rln", () => { + it("should track merkle roots and backfill from block number", async function () { + const acceptableRootWindow = 3; + + const tracker = new MerkleRootTracker( + acceptableRootWindow, + new Uint8Array([0, 0, 0, 0]) + ); + expect(tracker.roots()).to.have.length(1); + expect(tracker.buffer()).to.have.length(0); + expect(tracker.roots()[0]).to.deep.equal(new Uint8Array([0, 0, 0, 0])); + + for (let i = 1; i <= 30; i++) { + tracker.pushRoot(i, new Uint8Array([0, 0, 0, i])); + } + + expect(tracker.roots()).to.have.length(acceptableRootWindow); + expect(tracker.buffer()).to.have.length(20); + assert.sameDeepMembers(tracker.roots(), [ + new Uint8Array([0, 0, 0, 30]), + new Uint8Array([0, 0, 0, 29]), + new Uint8Array([0, 0, 0, 28]) + ]); + + // Buffer should keep track of 20 blocks previous to the current valid merkle root window + expect(tracker.buffer()[0]).to.be.eql(new Uint8Array([0, 0, 0, 8])); + expect(tracker.buffer()[19]).to.be.eql(new Uint8Array([0, 0, 0, 27])); + + // Remove roots 29 and 30 + tracker.backFill(29); + assert.sameDeepMembers(tracker.roots(), [ + new Uint8Array([0, 0, 0, 28]), + new Uint8Array([0, 0, 0, 27]), + new Uint8Array([0, 0, 0, 26]) + ]); + + expect(tracker.buffer()).to.have.length(18); + expect(tracker.buffer()[0]).to.be.eql(new Uint8Array([0, 0, 0, 8])); + expect(tracker.buffer()[17]).to.be.eql(new Uint8Array([0, 0, 0, 25])); + + // Remove roots from block 15 onwards. These blocks exists within the buffer + tracker.backFill(15); + assert.sameDeepMembers(tracker.roots(), [ + new Uint8Array([0, 0, 0, 14]), + new Uint8Array([0, 0, 0, 13]), + new Uint8Array([0, 0, 0, 12]) + ]); + expect(tracker.buffer()).to.have.length(4); + expect(tracker.buffer()[0]).to.be.eql(new Uint8Array([0, 0, 0, 8])); + expect(tracker.buffer()[3]).to.be.eql(new Uint8Array([0, 0, 0, 11])); + }); +}); diff --git a/packages/rln/src/root_tracker.ts b/packages/rln/src/root_tracker.ts new file mode 100644 index 0000000000..79b71845c1 --- /dev/null +++ b/packages/rln/src/root_tracker.ts @@ -0,0 +1,92 @@ +class RootPerBlock { + public constructor( + public root: Uint8Array, + public blockNumber: number + ) {} +} + +const maxBufferSize = 20; + +export class MerkleRootTracker { + private validMerkleRoots: Array = new Array(); + private merkleRootBuffer: Array = new Array(); + + public constructor( + private acceptableRootWindowSize: number, + initialRoot: Uint8Array + ) { + this.pushRoot(0, initialRoot); + } + + public backFill(fromBlockNumber: number): void { + if (this.validMerkleRoots.length == 0) return; + + let numBlocks = 0; + for (let i = this.validMerkleRoots.length - 1; i >= 0; i--) { + if (this.validMerkleRoots[i].blockNumber >= fromBlockNumber) { + numBlocks++; + } + } + + if (numBlocks == 0) return; + + const olderBlock = fromBlockNumber < this.validMerkleRoots[0].blockNumber; + + // Remove last roots + let rootsToPop = numBlocks; + if (this.validMerkleRoots.length < rootsToPop) { + rootsToPop = this.validMerkleRoots.length; + } + + this.validMerkleRoots = this.validMerkleRoots.slice( + 0, + this.validMerkleRoots.length - rootsToPop + ); + + if (this.merkleRootBuffer.length == 0) return; + + if (olderBlock) { + const idx = this.merkleRootBuffer.findIndex( + (x) => x.blockNumber == fromBlockNumber + ); + if (idx > -1) { + this.merkleRootBuffer = this.merkleRootBuffer.slice(0, idx); + } + } + + // Backfill the tree's acceptable roots + let rootsToRestore = + this.acceptableRootWindowSize - this.validMerkleRoots.length; + if (this.merkleRootBuffer.length < rootsToRestore) { + rootsToRestore = this.merkleRootBuffer.length; + } + + for (let i = 0; i < rootsToRestore; i++) { + const x = this.merkleRootBuffer.pop(); + if (x) this.validMerkleRoots.unshift(x); + } + } + + public pushRoot(blockNumber: number, root: Uint8Array): void { + this.validMerkleRoots.push(new RootPerBlock(root, blockNumber)); + + // Maintain valid merkle root window + if (this.validMerkleRoots.length > this.acceptableRootWindowSize) { + const x = this.validMerkleRoots.shift(); + if (x) this.merkleRootBuffer.push(x); + } + + // Maintain merkle root buffer + if (this.merkleRootBuffer.length > maxBufferSize) { + this.merkleRootBuffer.shift(); + } + } + + public roots(): Array { + return this.validMerkleRoots.map((x) => x.root); + } + + public buffer(): Array { + return this.merkleRootBuffer.map((x) => x.root); + } +} diff --git a/packages/rln/src/utils/bytes.ts b/packages/rln/src/utils/bytes.ts new file mode 100644 index 0000000000..279a2b3822 --- /dev/null +++ b/packages/rln/src/utils/bytes.ts @@ -0,0 +1,84 @@ +/** + * Concatenate Uint8Arrays + * @param input + * @returns concatenation of all Uint8Array received as input + */ +export function concatenate(...input: Uint8Array[]): Uint8Array { + let totalLength = 0; + for (const arr of input) { + totalLength += arr.length; + } + const result = new Uint8Array(totalLength); + let offset = 0; + for (const arr of input) { + result.set(arr, offset); + offset += arr.length; + } + return result; +} + +// Adapted from https://github.com/feross/buffer +function checkInt( + buf: Uint8Array, + value: number, + offset: number, + ext: number, + max: number, + min: number +): void { + if (value > max || value < min) + throw new RangeError('"value" argument is out of bounds'); + if (offset + ext > buf.length) throw new RangeError("Index out of range"); +} + +export function writeUIntLE( + buf: Uint8Array, + value: number, + offset: number, + byteLength: number, + noAssert?: boolean +): Uint8Array { + value = +value; + offset = offset >>> 0; + byteLength = byteLength >>> 0; + if (!noAssert) { + const maxBytes = Math.pow(2, 8 * byteLength) - 1; + checkInt(buf, value, offset, byteLength, maxBytes, 0); + } + + let mul = 1; + let i = 0; + buf[offset] = value & 0xff; + while (++i < byteLength && (mul *= 0x100)) { + buf[offset + i] = (value / mul) & 0xff; + } + + return buf; +} + +/** + * Transforms Uint8Array into BigInt + * @param array: Uint8Array + * @returns BigInt + */ +export function buildBigIntFromUint8Array( + array: Uint8Array, + byteOffset: number = 0 +): bigint { + const dataView = new DataView(array.buffer); + return dataView.getBigUint64(byteOffset, true); +} + +/** + * Fills with zeros to set length + * @param array little endian Uint8Array + * @param length amount to pad + * @returns little endian Uint8Array padded with zeros to set length + */ +export function zeroPadLE(array: Uint8Array, length: number): Uint8Array { + const result = new Uint8Array(length); + for (let i = 0; i < length; i++) { + result[i] = array[i] || 0; + } + return result; +} diff --git a/packages/rln/src/utils/epoch.spec.ts b/packages/rln/src/utils/epoch.spec.ts new file mode 100644 index 0000000000..ac06245813 --- /dev/null +++ b/packages/rln/src/utils/epoch.spec.ts @@ -0,0 +1,17 @@ +import { expect } from "chai"; +import fc from "fast-check"; + +import { epochBytesToInt, epochIntToBytes } from "./epoch.js"; + +describe("epoch serialization", () => { + it("Round trip", async function () { + await fc.assert( + fc.asyncProperty(fc.integer({ min: 0 }), async (date) => { + const bytes = epochIntToBytes(date); + const _date = epochBytesToInt(bytes); + + expect(_date.valueOf()).to.eq(date.valueOf()); + }) + ); + }); +}); diff --git a/packages/rln/src/utils/epoch.ts b/packages/rln/src/utils/epoch.ts new file mode 100644 index 0000000000..937e6641cd --- /dev/null +++ b/packages/rln/src/utils/epoch.ts @@ -0,0 +1,30 @@ +import { Logger } from "@waku/utils"; + +const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds + +const log = new Logger("waku:rln:epoch"); + +export function dateToEpoch( + timestamp: Date, + epochUnitSeconds: number = DefaultEpochUnitSeconds +): number { + const time = timestamp.getTime(); + const epoch = Math.floor(time / 1000 / epochUnitSeconds); + log.info("generated epoch", epoch); + return epoch; +} + +export function epochIntToBytes(epoch: number): Uint8Array { + const bytes = new Uint8Array(32); + const db = new DataView(bytes.buffer); + db.setUint32(0, epoch, true); + log.info("encoded epoch", epoch, bytes); + return bytes; +} + +export function epochBytesToInt(bytes: Uint8Array): number { + const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const epoch = dv.getUint32(0, true); + log.info("decoded epoch", epoch, bytes); + return epoch; +} diff --git a/packages/rln/src/utils/hash.ts b/packages/rln/src/utils/hash.ts new file mode 100644 index 0000000000..78422e21e0 --- /dev/null +++ b/packages/rln/src/utils/hash.ts @@ -0,0 +1,15 @@ +import * as zerokitRLN from "@waku/zerokit-rln-wasm"; + +import { concatenate, writeUIntLE } from "./bytes.js"; + +export function poseidonHash(...input: Array): Uint8Array { + const inputLen = writeUIntLE(new Uint8Array(8), input.length, 0, 8); + const lenPrefixedData = concatenate(inputLen, ...input); + return zerokitRLN.poseidonHash(lenPrefixedData); +} + +export function sha256(input: Uint8Array): Uint8Array { + const inputLen = writeUIntLE(new Uint8Array(8), input.length, 0, 8); + const lenPrefixedData = concatenate(inputLen, input); + return zerokitRLN.hash(lenPrefixedData); +} diff --git a/packages/rln/src/utils/index.ts b/packages/rln/src/utils/index.ts new file mode 100644 index 0000000000..74b5bb6016 --- /dev/null +++ b/packages/rln/src/utils/index.ts @@ -0,0 +1,9 @@ +export { extractMetaMaskSigner } from "./metamask.js"; +export { + concatenate, + writeUIntLE, + buildBigIntFromUint8Array, + zeroPadLE +} from "./bytes.js"; +export { sha256, poseidonHash } from "./hash.js"; +export { dateToEpoch, epochIntToBytes, epochBytesToInt } from "./epoch.js"; diff --git a/packages/rln/src/utils/metamask.ts b/packages/rln/src/utils/metamask.ts new file mode 100644 index 0000000000..83c7066106 --- /dev/null +++ b/packages/rln/src/utils/metamask.ts @@ -0,0 +1,17 @@ +import { ethers } from "ethers"; + +export const extractMetaMaskSigner = async (): Promise => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ethereum = (window as any).ethereum; + + if (!ethereum) { + throw Error( + "Missing or invalid Ethereum provider. Please install a Web3 wallet such as MetaMask." + ); + } + + await ethereum.request({ method: "eth_requestAccounts" }); + const provider = new ethers.providers.Web3Provider(ethereum, "any"); + + return provider.getSigner(); +}; diff --git a/packages/rln/src/zerokit.ts b/packages/rln/src/zerokit.ts new file mode 100644 index 0000000000..098ce3ad3a --- /dev/null +++ b/packages/rln/src/zerokit.ts @@ -0,0 +1,184 @@ +import type { IRateLimitProof } from "@waku/interfaces"; +import * as zerokitRLN from "@waku/zerokit-rln-wasm"; + +import { IdentityCredential } from "./identity.js"; +import { Proof, proofToBytes } from "./proof.js"; +import { WitnessCalculator } from "./resources/witness_calculator"; +import { + concatenate, + dateToEpoch, + epochIntToBytes, + writeUIntLE +} from "./utils/index.js"; + +export class Zerokit { + public constructor( + private readonly zkRLN: number, + private readonly witnessCalculator: WitnessCalculator + ) {} + + public generateIdentityCredentials(): IdentityCredential { + const memKeys = zerokitRLN.generateExtendedMembershipKey(this.zkRLN); // TODO: rename this function in zerokit rln-wasm + return IdentityCredential.fromBytes(memKeys); + } + + public generateSeededIdentityCredential(seed: string): IdentityCredential { + const stringEncoder = new TextEncoder(); + const seedBytes = stringEncoder.encode(seed); + // TODO: rename this function in zerokit rln-wasm + const memKeys = zerokitRLN.generateSeededExtendedMembershipKey( + this.zkRLN, + seedBytes + ); + return IdentityCredential.fromBytes(memKeys); + } + + public insertMember(idCommitment: Uint8Array): void { + zerokitRLN.insertMember(this.zkRLN, idCommitment); + } + + public insertMembers( + index: number, + ...idCommitments: Array + ): void { + // serializes a seq of IDCommitments to a byte seq + // the order of serialization is |id_commitment_len<8>|id_commitment| + const idCommitmentLen = writeUIntLE( + new Uint8Array(8), + idCommitments.length, + 0, + 8 + ); + const idCommitmentBytes = concatenate(idCommitmentLen, ...idCommitments); + zerokitRLN.setLeavesFrom(this.zkRLN, index, idCommitmentBytes); + } + + public deleteMember(index: number): void { + zerokitRLN.deleteLeaf(this.zkRLN, index); + } + + public getMerkleRoot(): Uint8Array { + return zerokitRLN.getRoot(this.zkRLN); + } + + public serializeMessage( + uint8Msg: Uint8Array, + memIndex: number, + epoch: Uint8Array, + idKey: Uint8Array + ): Uint8Array { + // calculate message length + const msgLen = writeUIntLE(new Uint8Array(8), uint8Msg.length, 0, 8); + + // Converting index to LE bytes + const memIndexBytes = writeUIntLE(new Uint8Array(8), memIndex, 0, 8); + + // [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal ] + return concatenate(idKey, memIndexBytes, epoch, msgLen, uint8Msg); + } + + public async generateRLNProof( + msg: Uint8Array, + index: number, + epoch: Uint8Array | Date | undefined, + idSecretHash: Uint8Array + ): Promise { + if (epoch === undefined) { + epoch = epochIntToBytes(dateToEpoch(new Date())); + } else if (epoch instanceof Date) { + epoch = epochIntToBytes(dateToEpoch(epoch)); + } + + if (epoch.length !== 32) throw new Error("invalid epoch"); + if (idSecretHash.length !== 32) throw new Error("invalid id secret hash"); + if (index < 0) throw new Error("index must be >= 0"); + + const serialized_msg = this.serializeMessage( + msg, + index, + epoch, + idSecretHash + ); + const rlnWitness = zerokitRLN.getSerializedRLNWitness( + this.zkRLN, + serialized_msg + ); + const inputs = zerokitRLN.RLNWitnessToJson(this.zkRLN, rlnWitness); + const calculatedWitness = await this.witnessCalculator.calculateWitness( + inputs, + false + ); // no sanity check being used in zerokit + + const proofBytes = zerokitRLN.generate_rln_proof_with_witness( + this.zkRLN, + calculatedWitness, + rlnWitness + ); + + return new Proof(proofBytes); + } + + public verifyRLNProof( + proof: IRateLimitProof | Uint8Array, + msg: Uint8Array + ): boolean { + let pBytes: Uint8Array; + if (proof instanceof Uint8Array) { + pBytes = proof; + } else { + pBytes = proofToBytes(proof); + } + + // calculate message length + const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8); + + return zerokitRLN.verifyRLNProof( + this.zkRLN, + concatenate(pBytes, msgLen, msg) + ); + } + + public verifyWithRoots( + proof: IRateLimitProof | Uint8Array, + msg: Uint8Array, + ...roots: Array + ): boolean { + let pBytes: Uint8Array; + if (proof instanceof Uint8Array) { + pBytes = proof; + } else { + pBytes = proofToBytes(proof); + } + // calculate message length + const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8); + + const rootsBytes = concatenate(...roots); + + return zerokitRLN.verifyWithRoots( + this.zkRLN, + concatenate(pBytes, msgLen, msg), + rootsBytes + ); + } + + public verifyWithNoRoot( + proof: IRateLimitProof | Uint8Array, + msg: Uint8Array + ): boolean { + let pBytes: Uint8Array; + if (proof instanceof Uint8Array) { + pBytes = proof; + } else { + pBytes = proofToBytes(proof); + } + + // calculate message length + const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8); + + return zerokitRLN.verifyWithRoots( + this.zkRLN, + concatenate(pBytes, msgLen, msg), + new Uint8Array() + ); + } +} diff --git a/packages/rln/tsconfig.dev.json b/packages/rln/tsconfig.dev.json new file mode 100644 index 0000000000..96c99dc461 --- /dev/null +++ b/packages/rln/tsconfig.dev.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.dev" +} \ No newline at end of file diff --git a/packages/rln/tsconfig.json b/packages/rln/tsconfig.json new file mode 100644 index 0000000000..6f8756934b --- /dev/null +++ b/packages/rln/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "dist/", + "rootDir": "src", + "tsBuildInfoFile": "dist/.tsbuildinfo" + }, + "include": ["src"], + "exclude": ["src/**/*.spec.ts", "src/test_utils"] +} \ No newline at end of file diff --git a/packages/tests/src/lib/runNodes.ts b/packages/tests/src/lib/runNodes.ts index b7d02d0c13..1a196208c4 100644 --- a/packages/tests/src/lib/runNodes.ts +++ b/packages/tests/src/lib/runNodes.ts @@ -51,7 +51,7 @@ export async function runNodes( log.info("Starting js waku node with :", JSON.stringify(waku_options)); let waku: WakuNode | undefined; try { - waku = (await createNode(waku_options)) as WakuNode; + waku = (await createNode(waku_options)) as unknown as WakuNode; await waku.start(); } catch (error) { log.error("jswaku node failed to start:", error); diff --git a/tsconfig.json b/tsconfig.json index 5a268831bf..d79e055766 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "incremental": true, "target": "ES2022", "moduleResolution": "Bundler", - "module": "ES2022", + "module": "esnext", "declaration": true, "sourceMap": true, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,