Compare commits

...

37 Commits

Author SHA1 Message Date
Sasha
f6d5deb8cb
chore: bump version (#108) 2024-04-08 09:55:19 +03:00
Arseniy Klempner
126bce32f7
fix: use correct offset when converting commitment to bigint (#103) 2024-03-22 13:11:46 +01:00
Sasha
3dc59084c9
fix: improve mapping over blocks (#100) 2024-03-05 23:41:38 +01:00
Sasha
6454446c1c
feat: align ts and tests to js-waku (#99)
* adjust config

* fix all lint issues

* fix ts build

* fix test issue

* fix ci

* add browser

* imporve wasm test

* fix wasm karma server

* try

* try

* add output folder

* try production config

* fix test runner
2024-03-01 01:07:33 +01:00
Sasha
65b7d8bcbc
feat!: restructure package (#98)
* decouple utils, remove global variables

* decouple hash utils

* decouple proof related stuff

* move to utils, move to resources

* decouple zerokit

* fix spelling

* fix mock

* remove auto prettier, typo

* retur prettier

* comma
2024-02-17 00:22:47 +01:00
Sasha
e1679b6bd9
bug: fix access to the network (#97) 2024-02-14 00:13:49 +01:00
Sasha
86d4f56818
chore: bump to 0.1.2 (#96) 2024-02-13 22:27:19 +01:00
Sasha
ac12f6800a
feat: improve cred managment (#95) 2024-02-13 22:19:35 +01:00
Sasha
77ba0a6d5d
feat!: add new operations and improve start (#93)
* featimprove start, types, import/export, add checks for netwrok, shortcuts for decoder encoder

* fix type issue

* update tests

* provide ability to use Keystore as a seed for credentials

* fix types

* up test

* initialize keystore by default

* add keys operation to Keystore
2024-01-30 22:28:12 +01:00
Sasha
bafbe01e52
feat: simplify API of bootstrapping, connection to MetaMask (#92)
* add MetaMask abstraction, add default start to RLN Instance, expose RLN Contract

* remove console timer

* improve text

* rename to createRLN

* update README, fix tests

* use Provider type

* use provider as it is
2024-01-24 21:23:52 +01:00
Vaclav Pavlin
18ce994d5e
fix: failing README instructions for example (#88) 2024-01-03 00:52:22 +01:00
Sasha
9b1e8187da
fix: zeroPad util (#87)
* fix: idCommitmnet little endianess

* stop double conversion

* fix problem with zeroPad

* fix test
2023-12-01 14:04:07 +01:00
Sasha
0c98a266e2
fix: idCommitmnet little endianess (#86)
* fix: idCommitmnet little endianess

* stop double conversion
2023-12-01 13:20:48 +01:00
Sasha
60a50709c7
chore: keep 5 roots by default (#84) 2023-11-27 12:09:31 +01:00
Sasha
0fbf6beec8
bug: fix insert / remove spam (#83) 2023-11-22 23:13:59 +01:00
Sasha
530034bf13
chore: update zerokit to 0.0.13(#81)
* chore: update zerokit

* update to 0.0.12

* use 0.0.13

* up waku version
2023-11-08 21:43:06 +01:00
Sasha
7e8cb899be
chore: update @waku/* packages (#77)
* update @waku/* packages

* fix typing

* remove type
2023-10-28 14:19:56 +02:00
Sasha
fa49e29856
feat: use Proxy contract address, add fixes to RLN Contract (#74)
* use Proxy contract address, add fixes to RLN Contract

* whitelist word

* update tesst

* up mock

* up

* fixes to contract, keystore and utils

* whitelist word

* update tests

* move to hash map

* up mock

* up
2023-10-19 03:23:05 +02:00
Sasha
89283768c3
chore: test fixes (#73) 2023-10-17 11:36:37 +02:00
Sasha
5b9414aede
feat: update contract (#72)
* feat: update contract

* update ABI

* update contract and fix name of ABI

* update exports

* ignore constants

* fix tests

* update mock

* up mock

* add logs

* add mock
2023-10-17 11:26:17 +02:00
Sasha
891ee3474a
feat: keystore implementation (#70)
* add deps

* complete keystore, move code

* remove esling

* rename command

* rename export

* fix bug

* rollback example

* rollback example[2]

* improve compatibility with nwaku

* add packages

* update example

* add keystore.spec.ts

* up node, add whitelist words

* add types

* try break test

* add test

* add more tests

* add helper functions, update example, add tests

* fix types

* fix test

* add logs

* fix build of the object

* use different approach to catch errors

* run one test

* anotehr one

* fix validation error

* apply last fixes

* update example

* up

* ignroe generated files

* up

* up

* fix test

* test

* fix

* up

* add prettier ignore

* up

* fix lint

* up

* last
2023-10-14 02:21:35 +02:00
richΛrd
5d7f77f300
fix: add moduleResolution to package.json (#65)
Co-authored-by: Sasha <118575614+weboko@users.noreply.github.com>
2023-07-03 10:30:03 -04:00
fryorcraken.eth
b429b055db
Merge pull request #61 from waku-org/feat/meta 2023-05-24 11:13:37 +10:00
fryorcraken.eth
5fb26dfd16
feat: Expose meta field and ensure encoder sets it 2023-05-24 10:48:11 +10:00
fryorcraken.eth
71aa967a86
chore: bump @waku packages 2023-05-24 10:48:11 +10:00
RichΛrd
fa1d3f8222
fix: merkle root import (#64) 2023-05-23 07:08:59 -04:00
RichΛrd
7e93896538
feat: track roots in rln contract and use sepolia instead of goerli (#62) 2023-05-14 12:18:21 -04:00
fryorcraken.eth
b7cb3f9bbf
chore: bump @waku/core to 0.0.17 (#60)
* chore: bump mocha

9.x has a vulnerability

* chore: bump @waku/core to 0.0.17

And other Waku packages.

* chore: npm audit fix

* chore: bump @waku/interfaces to 0.0.12
2023-05-10 12:19:46 +02:00
RichΛrd
e52107dcf8
feat: use identity credentials, and expose hash, bulk insert and delete members functions (#58)
* feat: use identity credentials, and expose hash, bulk insert and delete members functions
* feat: merkle root tracker
2023-05-08 18:10:26 -04:00
Sasha
7e0966aef7
feat: add custom queryFilter (#57)
* add custom fetcher

* improve fetch in chunks

* remove comment

* fetch in portions of 5

* allow to configure fetch params

* add error log
2023-04-28 01:20:29 +02:00
Sasha
88a28a11e3
feat!: add registerMemberFromMembershipKey (#56)
* add registerMemberFromMembershipKey
* make better naming
2023-04-19 23:28:04 +02:00
Sasha
360dc829f5
chore!: bump to new @waku (#55)
* move to newer version

* fix import

* add content topic on the message

* update types

* address comments

* setup getter
2023-04-19 20:02:13 +02:00
fryorcraken.eth
fae4bea48d
Merge pull request #50 from waku-org/doc/39-update-readme 2023-02-27 09:34:18 +11:00
fryorcraken.eth
bdf388f8d5
chore: update cspell dictionary 2023-02-24 15:36:27 +11:00
fryorcraken.eth
45d1b28286
doc: note about semaphore compatibility 2023-02-24 15:34:49 +11:00
fryorcraken.eth
ed22a3f66b
doc: update repo's rationale
Fixes #39
2023-02-24 15:32:16 +11:00
Sasha
d77370fbec
Add RLN Contract abstraction (#43)
* implement rln contract abstraction, add basic tests, add usefull constants

* remove test command

* resolve simple comments

* move to getter for members, add init method

* fix naming

* remove default signature message

* use direct path to js file

* try different karma config

* try generic import

* update test

* address comments: rename const file, return prev regexp

* remove test

* bring back test file

* fix mock approach

* use any for type casting

* use another approach for typecasting

* update mocks

* update mocked event

* use correct value for mock

* fix spy definition

* add BigInt to MembershipKey

* fix joining

* use slice

* remove accidentally commited junk

* fix typo, use DataView for conversion, use BigInt directly

Co-authored-by: weboko <anon@mail.com>
2023-01-26 18:58:18 +01:00
54 changed files with 15304 additions and 26116 deletions

View File

@ -3,14 +3,26 @@
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
"language": "en",
"words": [
"Waku",
"arrayify",
"circom",
"keypair",
"merkle",
"nwaku",
"rlnjs",
"sepolia",
"vkey",
"zkey",
"circom",
"Waku",
"zerokit",
"nwaku"
"zkey",
"Keccak",
"keccak",
"chainsafe",
"kdfparams",
"ciphertext",
"cipherparams",
"codegen",
"hexlify",
"Arraylike"
],
"flagWords": [],
"ignorePaths": [
@ -23,7 +35,8 @@
"gen",
"proto",
"*.spec.ts",
"src/resources.ts"
"src/resources/*",
"src/contract/constants.ts"
],
"patterns": [
{
@ -32,4 +45,4 @@
}
],
"ignoreRegExpList": ["import"]
}
}

View File

@ -2,11 +2,9 @@
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.dev.json"
},
"env": {
"es6": true
"project": ["./tsconfig.json"]
},
"env": { "es6": true },
"ignorePatterns": ["node_modules", "build", "coverage", "proto"],
"plugins": ["import", "eslint-comments", "functional"],
"extends": [
@ -16,18 +14,15 @@
"plugin:import/typescript",
"plugin:prettier/recommended"
],
"globals": {
"BigInt": true,
"console": true,
"WebAssembly": true
},
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
"rules": {
"@typescript-eslint/explicit-function-return-type": [
"prettier/prettier": [
"error",
{
"allowExpressions": true
"trailingComma": "none"
}
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"eslint-comments/disable-enable-pair": [
"error",
@ -45,26 +40,63 @@
}
}
],
"no-constant-condition": [
"no-constant-condition": ["error", { "checkLoops": false }],
"import/no-extraneous-dependencies": [
"error",
{
"checkLoops": false
"devDependencies": [
"**/*.test.ts",
"**/*.spec.ts",
"**/tests/**",
"**/rollup.config.js",
"**/playwright.config.ts",
"**/.eslintrc.cjs",
"**/karma.conf.cjs"
]
}
],
"sort-imports": [
"error",
{
"ignoreDeclarationSort": true,
"ignoreCase": true
}
]
{ "ignoreDeclarationSort": true, "ignoreCase": true }
],
"no-console": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-explicit-any": "warn",
"id-match": ["error", "^(?!.*[pP]ubSub)"]
},
"overrides": [
{
"files": ["*.spec.ts", "**/test_utils/*.ts"],
"files": ["*.spec.ts", "**/test_utils/*.ts", "*.js", "*.cjs"],
"rules": {
"@typescript-eslint/no-non-null-assertion": "off"
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"no-console": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true
}
]
}
},
{
"files": ["*.ts", "*.mts", "*.cts", "*.tsx"],
"rules": {
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": true
}
]
}
},
{
"files": ["**/ci/*.js"],
"rules": {
"no-undef": "off"
}
}
]
}
}

View File

@ -21,7 +21,6 @@ jobs:
node-version: ${{ env.NODE_JS }}
- uses: bahmutov/npm-install@v1
- run: npm run test:lint
- run: npm run test:prettier
- run: npm run test:spelling
- run: npm run test:tsc
@ -43,6 +42,7 @@ jobs:
with:
node-version: ${{ env.NODE_JS }}
- uses: bahmutov/npm-install@v1
- run: npx playwright install --with-deps
- run: npm run test:browser
release_next:

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
*/**/*_generated.ts

111
README.md
View File

@ -1,9 +1,34 @@
# js-rln
# `@waku/rln`
Browser library providing the cryptographic functions for Waku RLN Relay
https://rfc.vac.dev/spec/17/
This browser library enables the usage of RLN with Waku, as specified in the [Waku v2 RLN Relay RFC](https://rfc.vac.dev/spec/17/).
### Install
## Purpose
### RLN Cryptography
The RLN cryptographic function are provided by [zerokit](https://github.com/vacp2p/zerokit/).
This is imported via the `@waku/zerokit-rln-wasm` dependencies which contains a WASM extract of zerokit's RLN functions.
Note that RLN Credentials generated with `zerokit`, and hence `@waku/rln`, are compatible with semaphore credentials.
Note that the WASM blob uses browser APIs, **NodeJS is not supported**.
### Waku Interfaces
This library implements the [`IEncoder`](https://github.com/waku-org/js-waku/blob/604ba1a889f1994bd27f5749107c3a5b2ef490d5/packages/interfaces/src/message.ts#L43)
and [`IDecoder`](https://github.com/waku-org/js-waku/blob/604ba1a889f1994bd27f5749107c3a5b2ef490d5/packages/interfaces/src/message.ts#L58)
interfaces of [js-waku](https://github.com/waku-org/js-waku).
This enables a seamless usage with js-waku applications, as demonstrated in the [rln-js example](https://github.com/waku-org/js-waku-examples/tree/master/examples/rln-js).
### Comparison to Existing Work
[Rate-Limiting-Nullifier/rlnjs](https://github.com/Rate-Limiting-Nullifier/rlnjs)
is an existing JavaScript / TypeScript library that already provides RLN cryptographic functionalities for the browser.
The core difference is that `@waku/rln` uses [zerokit](https://github.com/vacp2p/zerokit/) for cryptography and provide opinionated interfaces to use RLN specifically in the context of Waku.
## Install
```
npm install @waku/rln
@ -13,50 +38,83 @@ npm install @waku/rln
yarn add @waku/rln
```
### Running example app
Or to use ESM import directly from a `<script>` tag:
```html
<script type="module">
import * as rln from "https://unpkg.com/@waku/rln@0.0.13/bundle/index.js";
</script>
```
## Running example app
```
git clone https://github.com/waku-org/js-rln
cd js-rln/example
cd js-rln/
npm install
cd example/
npm install # or yarn
npm start
```
Browse http://localhost:8080 and open the dev tools console to see the proof being generated and its verification
### Usage
## Usage
#### Initializing the library
### Initializing the Library
```js
import * as rln from "@waku/rln";
const rlnInstance = wait rln.create();
const rlnInstance = await rln.createRLN();
```
#### Generating RLN membership keypair
### Starting RLN to listen to a contract
```js
let memKeys = rlnInstance.generateMembershipKey();
import * as rln from "@waku/rln";
const rlnInstance = await rln.createRLN();
await rlnInstance.start(); // will use default Sepolia contract
```
#### Generating RLN membership keypair using a seed
#### Generating RLN Membership Credentials
```js
let memKeys = rlnInstance.generateSeededMembershipKey(seed);
let credentials = rlnInstance.generateIdentityCredentials();
```
#### Adding membership keys into merkle tree
### Generating RLN Membership Keypair Using a Seed
This can be used to generate credentials from a signature hash (e.g. signed by an Ethereum account).
```js
rlnInstance.insertMember(memKeys.IDCommitment);
let credentials = rlnInstance.generateSeededIdentityCredentials(seed);
```
#### Generating a proof
### Adding Membership Keys Into Merkle Tree
```js
rlnInstance.insertMember(credentials.IDCommitment);
```
### Registering Membership on a contract
```js
import * as rln from "@waku/rln";
const rlnInstance = await rln.createRLN();
await rlnInstance.start(); // will use default Sepolia contract
const membershipInfo = await rlnInstance.contract.registerWithKey(credentials);
```
### Generating a Proof
```js
// prepare the message
@ -72,26 +130,27 @@ const proof = await rlnInstance.generateProof(
uint8Msg,
index,
epoch,
memKeys.IDKey
credentials.IDSecretHash
);
```
#### Verifying a proof
### Verifying a Proof
```js
try {
// verify the proof
const verificationResult = rlnInstance.verifyProof(proof);
const verificationResult = rlnInstance.verifyProof(proof, uint8Msg);
console.log("Is proof verified?", verificationResult ? "yes" : "no");
} catch (err) {
console.log("Invalid proof");
}
```
### Updating circuit, verification key and zkey
### Updating Circuit, Verification Key and Zkey
The RLN specs defines the defaults. These values are fixed and should not
change. Currently, these [resources](https://github.com/vacp2p/zerokit/tree/master/rln/resources/tree_height_20) are being used.
The RLN specs defines the defaults.
These values are fixed and should not change.
Currently, these [resources](https://github.com/vacp2p/zerokit/tree/master/rln/resources/tree_height_20) are being used.
If they change, this file needs to be updated in `resources.ts` which
contains these values encoded in base64 in this format:
@ -104,9 +163,9 @@ export {verification_key, circuit, zkey};
A tool like GNU's `base64` could be used to encode this data.
### Updating zerokit
### Updating Zerokit
1. Make sure you have nodejs installed and a C compiler
1. Make sure you have NodeJS installed and a C compiler
2. Install wasm-pack
```
@ -137,6 +196,6 @@ Licensed and distributed under either of
or
- Apache License, Version 2.0, ([LICENSE-APACHEv2](LICENSE-APACHEv2) or http://www.apache.org/licenses/LICENSE-2.0)
- Apache License, Version 2.0, ([LICENSE-APACHE-v2](LICENSE-APACHE-v2) or http://www.apache.org/licenses/LICENSE-2.0)
at your option. These files may not be copied, modified, or distributed except according to those terms.

View File

@ -6,6 +6,8 @@
</head>
<body>
<p>Open the developer tools to see the generated proof and its validation</p>
<script src="https://cdn.ethers.io/lib/ethers-5.6.umd.min.js" type="text/javascript">
</script>
<script src="./index.js"></script>
</body>
</html>

View File

@ -1,41 +1,160 @@
import * as rln from "@waku/rln";
rln.create().then(async rlnInstance => {
let memKeys = rlnInstance.generateMembershipKey();
rln.create().then(async (rlnInstance) => {
const credentials = rlnInstance.generateIdentityCredentials();
//peer's index in the Merkle Tree
const index = 5
//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.insertMember(memKeys.IDCommitment);
} else {
// create a new key pair
let memKeys = rlnInstance.generateMembershipKey(); // TODO: handle error
rlnInstance.insertMember(memKeys.IDCommitment);
// Create a Merkle tree with random members
for (let i = 0; i < 10; i++) {
if (i == index) {
// insert the current peer's pk
rlnInstance.insertMember(credentials.IDCommitment);
} else {
// create a new key pair
const credentials = rlnInstance.generateIdentityCredentials(); // TODO: handle error
rlnInstance.insertMember(credentials.IDCommitment);
}
}
// prepare the message
const uint8Msg = Uint8Array.from(
"Hello World".split("").map((x) => x.charCodeAt())
);
// setting up the epoch
const epoch = new Date();
console.log("Generating proof...");
console.time("proof_gen_timer");
let proof = await rlnInstance.generateRLNProof(
uint8Msg,
index,
epoch,
credentials.IDSecretHash
);
console.timeEnd("proof_gen_timer");
console.log("Proof", proof);
try {
// verify the proof
let verifResult = rlnInstance.verifyRLNProof(proof, uint8Msg);
console.log("Is proof verified?", verifResult ? "yes" : "no");
} catch (err) {
console.log("Invalid proof");
}
const provider = new ethers.providers.Web3Provider(window.ethereum, "any");
const DEFAULT_SIGNATURE_MESSAGE =
"The signature of this message will be used to generate your RLN credentials. Anyone accessing it may send messages on your behalf, please only share with the RLN dApp";
const signer = provider.getSigner();
const signature = await signer.signMessage(DEFAULT_SIGNATURE_MESSAGE);
console.log(`Got signature: ${signature}`);
const contract = await rln.RLNContract.init(rlnInstance, {
address: rln.SEPOLIA_CONTRACT.address,
provider: signer,
});
const event = await contract.registerMember(rlnInstance, signature);
console.log(`Registered as member with ${event}`);
});
const run = async () => {
const data = {
"application": "waku-rln-relay",
"appIdentifier": "01234567890abcdef",
"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"
}
}
}
},
"version": "0.2"
};
// prepare the message
const uint8Msg = Uint8Array.from("Hello World".split("").map(x => x.charCodeAt()));
const keystore = await rln.Keystore.fromObject(data);
// setting up the epoch
const epoch = new Date();
const identity = {
"idTrapdoor":[248,73,210,129,12,83,146,208,57,117,35,145,20,66,203,61,238,130,60,23,94,249,123,92,114,190,24,0,4,242,10,24],
"idNullifier":[80,233,27,57,133,192,162,150,251,120,215,194,103,198,249,177,225,15,68,137,221,204,174,12,107,102,123,129,111,191,121,22],
"idSecretHash":[120,204,2,248,110,149,95,135,222,176,27,137,217,255,86,205,74,130,21,250,220,111,203,11,199,229,25,76,146,3,249,13],
"idCommitment":[71,207,30,230,39,50,198,40,128,131,79,200,166,168,187,137,235,132,131,164,92,233,195,170,21,205,94,85,246,144,203,9]
};
const membership = {
"chainId":"0xAA36A7","address":"0x0A988fd9CA5BAebDf098b8A73621b2AaDa6492E8","treeIndex":66851
};
console.log("Generating proof...");
console.time("proof_gen_timer");
let proof = await rlnInstance.generateRLNProof(uint8Msg, index, epoch, memKeys.IDKey)
console.timeEnd("proof_gen_timer");
console.log("Proof", proof)
await keystore.addCredential({ identity, membership }, "sup3rsecure");
try {
// verify the proof
let verifResult = rlnInstance.verifyRLNProof(proof, uint8Msg);
console.log("Is proof verified?", verifResult ? "yes" : "no");
} catch (err) {
console.log("Invalid proof")
}
});
await keystore.readCredential("8479C6B9125D43E7B7739F1BAB41779F2F5A4D27FF0E2B6F6CA353032010A22C", "sup3rsecure").then(console.log);
};
run();

20915
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,12 +9,13 @@
"start": "webpack-dev-server"
},
"dependencies": {
"@waku/rln": "file:../"
"@waku/rln": "file:../",
"@waku/utils": "^0.0.11"
},
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1",
"copy-webpack-plugin": "^11.0.0"
"webpack-dev-server": "^4.11.1"
}
}

View File

@ -1,13 +1,11 @@
process.env.CHROME_BIN = require("puppeteer").executablePath();
const os = require("os");
const path = require("path");
const ResolveTypeScriptPlugin = require("resolve-typescript-plugin");
const webpack = require("webpack");
const playwright = require('playwright');
process.env.CHROME_BIN = playwright.chromium.executablePath();
const output = {
path:
path.join(os.tmpdir(), "_karma_webpack_") +
Math.floor(Math.random() * 1000000),
path: path.join(__dirname, "dist"),
};
module.exports = function (config) {
@ -16,7 +14,6 @@ module.exports = function (config) {
preprocessors: {
"**/*.ts": ["webpack"],
},
files: [
"src/**/*.spec.ts",
"src/**/*.ts",
@ -30,19 +27,16 @@ module.exports = function (config) {
envPreprocessor: ["CI"],
reporters: ["progress"],
browsers: ["ChromeHeadless"],
pingTimeout: 60000,
singleRun: true,
client: {
mocha: {
timeout: 6000, // Default is 2s
timeout: 60000, // Default is 2s
},
},
webpack: {
output,
mode: "production",
resolve: {
// Add `.ts` and `.tsx` as a resolvable extension.
extensions: [".ts", ".tsx", ".js"],
plugins: [new ResolveTypeScriptPlugin()],
},
module: {
rules: [
{
@ -61,7 +55,25 @@ module.exports = function (config) {
},
],
},
output,
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", ".tsx", ".js"],
extensionAlias: {
".js": [".js", ".ts"],
".cjs": [".cjs", ".cts"],
".mjs": [".mjs", ".mts"]
}
},
stats: { warnings: false },
devtool: "inline-source-map"
},
});
};

16757
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@waku/rln",
"version": "0.0.13",
"version": "0.1.3",
"description": "Rate Limit Nullifier for js-waku",
"types": "./dist/index.d.ts",
"module": "./dist/index.js",
@ -24,15 +24,14 @@
"scripts": {
"prepare": "husky install",
"build": "run-s build:**",
"build:codegen": "node ./scripts/schema_validator_codegen.js",
"build:tsc": "tsc",
"build:bundle": "rollup --config rollup.config.js",
"size": "npm run build && size-limit",
"fix": "run-s fix:*",
"fix:prettier": "prettier \"src/**/*.ts\" \"./*.json\" \"*.*js\" \".github/**/*.yml\" --write",
"fix:lint": "eslint src --ext .ts --ext .cjs --fix",
"test": "run-s test:*",
"test:lint": "eslint src --ext .ts",
"test:prettier": "prettier \"src/**/*.ts\" \"./*.json\" \"*.*js\" \".github/**/*.yml\" --list-different",
"test:spelling": "cspell \"{*.md,.github/*.md,src/**/*.ts}\"",
"test:tsc": "tsc -p tsconfig.dev.json",
"test:browser": "karma start karma.conf.cjs",
@ -45,7 +44,7 @@
"crypto": false
},
"engines": {
"node": ">=16"
"node": ">=18"
},
"publishConfig": {
"access": "public",
@ -59,51 +58,63 @@
"@size-limit/preset-big-lib": "^8.0.0",
"@types/app-root-path": "^1.2.4",
"@types/chai": "^4.2.15",
"@types/chai-as-promised": "^7.1.6",
"@types/chai-spies": "^1.0.3",
"@types/chai-subset": "^1.3.3",
"@types/debug": "^4.1.7",
"@types/deep-equal-in-any-order": "^1.0.1",
"@types/lodash": "^4.14.199",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.6",
"@types/tail": "^2.0.0",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.21.0",
"@waku/interfaces": "^0.0.20",
"@waku/message-encryption": "^0.0.23",
"@web/rollup-plugin-import-meta-assets": "^1.0.7",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"app-root-path": "^3.0.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chai-spies": "^1.0.0",
"chai-subset": "^1.6.0",
"cspell": "^5.14.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
"deep-equal-in-any-order": "^2.0.6",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^4.0.2",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-functional": "^6.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^5.0.1",
"fast-check": "^2.25.0",
"gh-pages": "^3.2.3",
"husky": "^7.0.4",
"ignore-loader": "^0.1.2",
"isomorphic-fetch": "^3.0.0",
"js-waku": "^0.29.0-29436ea",
"jsdom": "^19.0.0",
"jsdom-global": "^3.0.2",
"karma": "^6.3.12",
"karma-chrome-launcher": "^3.1.0",
"karma": "^6.4.2",
"karma-chrome-launcher": "^3.2.0",
"karma-firefox-launcher": "^2.1.2",
"karma-mocha": "^2.0.1",
"karma-webpack": "^5.0.0",
"karma-webkit-launcher": "^2.4.0",
"karma-webpack": "github:codymikol/karma-webpack#2337a82beb078c0d8e25ae8333a06249b8e72828",
"lint-staged": "^13.0.3",
"mocha": "^9.1.3",
"mocha": "10.1.0",
"npm-run-all": "^4.1.5",
"p-timeout": "^4.1.0",
"prettier": "^2.1.1",
"playwright": "^1.40.1",
"process": "^0.11.10",
"puppeteer": "^13.0.1",
"resolve-typescript-plugin": "^1.2.0",
"rollup": "^2.75.0",
"rollup-plugin-copy": "^3.4.0",
"size-limit": "^8.0.0",
"tail": "^2.2.0",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"typedoc": "^0.23.10",
"typescript": "^4.5.5"
"typedoc": "^0.25.7",
"typescript": "^5.3.2"
},
"files": [
"dist",
@ -119,12 +130,17 @@
"lint-staged": {
"*.ts": [
"eslint --fix"
],
"*.{ts,md,json,conf*.*js}": [
"prettier --write"
]
},
"dependencies": {
"@waku/zerokit-rln-wasm": "^0.0.5"
"@chainsafe/bls-keystore": "^3.0.0",
"@waku/core": "^0.0.25",
"@waku/utils": "^0.0.13",
"@waku/zerokit-rln-wasm": "^0.0.13",
"debug": "^4.3.4",
"ethereum-cryptography": "^2.1.2",
"ethers": "^5.7.2",
"lodash": "^4.17.21",
"uuid": "^9.0.1"
}
}
}

0
runtime.js Normal file
View File

View File

@ -0,0 +1,23 @@
{
"$ref": "#/definitions/Keystore",
"definitions": {
"Keystore": {
"type": "object",
"properties": {
"credentials": {
"type": "object"
},
"appIdentifier": {
"type": "string"
},
"version": {
"type": "string"
},
"application": {
"type": "string"
}
},
"required": ["application", "appIdentifier", "credentials", "version"]
}
}
}

View File

@ -0,0 +1,42 @@
{
"$ref": "#/definitions/Credential",
"definitions": {
"Credential": {
"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"]
}
}
}

View File

@ -0,0 +1,77 @@
import { writeFileSync } from "fs";
import { join } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
import Ajv from "ajv";
import addFormats from "ajv-formats";
import standaloneCode from "ajv/dist/standalone/index.js";
import keystoreSchema from "./nwaku_keystore.json" assert { type: "json" };
import credentialSchema from "./nwaku_keystore_credential.json" assert { type: "json" };
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Generate a function that validates the Keystore schema.
// Write the function to a file in the source.
const OUTPUT_DIR = join(__dirname, "../src/keystore");
const OUTPUT_KEYSTORE_FILE = join(
OUTPUT_DIR,
"keystore_validation_generated.ts"
);
const OUTPUT_CREDENTIALS_FILE = join(
OUTPUT_DIR,
"credential_validation_generated.ts"
);
// Initialize ajv instance
let ajv = new Ajv({
schemas: [keystoreSchema],
code: {
source: true,
esm: true,
},
});
addFormats(ajv);
// The output code is minified vanilla javascript and uses the cute pattern of attaching errors to the exported function as a property.
// The easiest way to treat this is to just ts-ignore the whole output and wrap the function with a handcrafted type.
const keystoreModuleCode =
`
/* 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
` +
standaloneCode(ajv, {
Keystore: "#/definitions/Keystore",
});
ajv = new Ajv({
schemas: [credentialSchema],
code: {
source: true,
esm: true,
},
});
addFormats(ajv);
const credentialModuleCode =
`
/* 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
` +
standaloneCode(ajv, {
Credential: "#/definitions/Credential",
});
writeFileSync(OUTPUT_KEYSTORE_FILE, keystoreModuleCode);
writeFileSync(OUTPUT_CREDENTIALS_FILE, credentialModuleCode);

View File

@ -1,39 +0,0 @@
// 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;
}

View File

@ -1,58 +1,78 @@
import { expect } from "chai";
import {
createDecoder,
createEncoder,
DecodedMessage
} from "@waku/core/lib/message/version_0";
import type { IProtoMessage } from "@waku/interfaces";
import {
generatePrivateKey,
generateSymmetricKey,
getPublicKey,
} from "js-waku";
getPublicKey
} from "@waku/message-encryption";
import {
DecoderV0,
EncoderV0,
MessageV0,
} from "js-waku/lib/waku_message/version_0";
createDecoder as createAsymDecoder,
createEncoder as createAsymEncoder
} from "@waku/message-encryption/ecies";
import {
AsymDecoder,
AsymEncoder,
SymDecoder,
SymEncoder,
} from "js-waku/lib/waku_message/version_1";
createDecoder as createSymDecoder,
createEncoder as createSymEncoder
} from "@waku/message-encryption/symmetric";
import { expect } from "chai";
import { RLNDecoder, RLNEncoder } from "./codec.js";
import { epochBytesToInt } from "./epoch.js";
import {
createRLNDecoder,
createRLNEncoder,
RLNDecoder,
RLNEncoder
} from "./codec.js";
import { createRLN } from "./create.js";
import { RlnMessage } from "./message.js";
import * as rln from "./index.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 rln.create();
const memKeys = rlnInstance.generateMembershipKey();
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.insertMember(memKeys.IDCommitment);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const rlnEncoder = new RLNEncoder(
new EncoderV0(TestContentTopic),
const rlnEncoder = createRLNEncoder({
encoder: createEncoder({ contentTopic: TestContentTopic }),
rlnInstance,
index,
memKeys
);
const rlnDecoder = new RLNDecoder(
credential
});
const rlnDecoder = createRLNDecoder({
rlnInstance,
new DecoderV0(TestContentTopic)
);
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(protoResult!))!;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
protoResult!
))!;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify()).to.be.true;
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);
@ -64,35 +84,36 @@ describe("RLN codec with version 0", () => {
});
it("toProtoObj", async function () {
const rlnInstance = await rln.create();
const memKeys = rlnInstance.generateMembershipKey();
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.insertMember(memKeys.IDCommitment);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const rlnEncoder = new RLNEncoder(
new EncoderV0(TestContentTopic),
createEncoder({ contentTopic: TestContentTopic }),
rlnInstance,
index,
memKeys
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
new DecoderV0(TestContentTopic)
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<MessageV0>;
)) as RlnMessage<DecodedMessage>;
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify()).to.be.true;
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);
@ -106,24 +127,27 @@ describe("RLN codec with version 0", () => {
describe("RLN codec with version 1", () => {
it("Symmetric, toWire", async function () {
const rlnInstance = await rln.create();
const memKeys = rlnInstance.generateMembershipKey();
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.insertMember(memKeys.IDCommitment);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const symKey = generateSymmetricKey();
const rlnEncoder = new RLNEncoder(
new SymEncoder(TestContentTopic, symKey),
createSymEncoder({
contentTopic: TestContentTopic,
symKey
}),
rlnInstance,
index,
memKeys
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
new SymDecoder(TestContentTopic, symKey)
createSymDecoder(TestContentTopic, symKey)
);
const bytes = await rlnEncoder.toWire({ payload });
@ -132,10 +156,13 @@ describe("RLN codec with version 1", () => {
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(protoResult!))!;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
protoResult!
))!;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify()).to.be.true;
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);
@ -147,37 +174,41 @@ describe("RLN codec with version 1", () => {
});
it("Symmetric, toProtoObj", async function () {
const rlnInstance = await rln.create();
const memKeys = rlnInstance.generateMembershipKey();
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.insertMember(memKeys.IDCommitment);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const symKey = generateSymmetricKey();
const rlnEncoder = new RLNEncoder(
new SymEncoder(TestContentTopic, symKey),
createSymEncoder({
contentTopic: TestContentTopic,
symKey
}),
rlnInstance,
index,
memKeys
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
new SymDecoder(TestContentTopic, symKey)
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<MessageV0>;
)) as RlnMessage<DecodedMessage>;
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify()).to.be.true;
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);
@ -189,25 +220,28 @@ describe("RLN codec with version 1", () => {
});
it("Asymmetric, toWire", async function () {
const rlnInstance = await rln.create();
const memKeys = rlnInstance.generateMembershipKey();
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.insertMember(memKeys.IDCommitment);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const privateKey = generatePrivateKey();
const publicKey = getPublicKey(privateKey);
const rlnEncoder = new RLNEncoder(
new AsymEncoder(TestContentTopic, publicKey),
createAsymEncoder({
contentTopic: TestContentTopic,
publicKey
}),
rlnInstance,
index,
memKeys
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
new AsymDecoder(TestContentTopic, privateKey)
createAsymDecoder(TestContentTopic, privateKey)
);
const bytes = await rlnEncoder.toWire({ payload });
@ -216,10 +250,13 @@ describe("RLN codec with version 1", () => {
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(protoResult!))!;
const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC,
protoResult!
))!;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify()).to.be.true;
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);
@ -231,38 +268,42 @@ describe("RLN codec with version 1", () => {
});
it("Asymmetric, toProtoObj", async function () {
const rlnInstance = await rln.create();
const memKeys = rlnInstance.generateMembershipKey();
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.insertMember(memKeys.IDCommitment);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const privateKey = generatePrivateKey();
const publicKey = getPublicKey(privateKey);
const rlnEncoder = new RLNEncoder(
new AsymEncoder(TestContentTopic, publicKey),
createAsymEncoder({
contentTopic: TestContentTopic,
publicKey
}),
rlnInstance,
index,
memKeys
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
new AsymDecoder(TestContentTopic, privateKey)
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<MessageV0>;
)) as RlnMessage<DecodedMessage>;
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify()).to.be.true;
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);
@ -276,30 +317,31 @@ describe("RLN codec with version 1", () => {
describe("RLN Codec - epoch", () => {
it("toProtoObj", async function () {
const rlnInstance = await rln.create();
const memKeys = rlnInstance.generateMembershipKey();
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.insertMember(memKeys.IDCommitment);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const rlnEncoder = new RLNEncoder(
new EncoderV0(TestContentTopic),
createEncoder({ contentTopic: TestContentTopic }),
rlnInstance,
index,
memKeys
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
new DecoderV0(TestContentTopic)
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<MessageV0>;
)) as RlnMessage<DecodedMessage>;
const epochBytes = proto!.rateLimitProof!.epoch;
const epoch = epochBytesToInt(epochBytes);
@ -307,7 +349,7 @@ describe("RLN Codec - epoch", () => {
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify()).to.be.true;
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);
@ -319,3 +361,110 @@ describe("RLN Codec - epoch", () => {
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<DecodedMessage>;
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;
});
});

View File

@ -1,84 +1,130 @@
import type {
IDecodedMessage,
IDecoder,
IEncoder,
IMessage,
IProtoMessage,
IRateLimitProof
} from "@waku/interfaces";
import debug from "debug";
import {
Decoder,
Encoder,
Message,
ProtoMessage,
RateLimitProof,
} from "js-waku/lib/interfaces";
import type { IdentityCredential } from "./identity.js";
import { RlnMessage, toRLNSignal } from "./message.js";
import { MembershipKey, RLNInstance } from "./rln.js";
import { RLNInstance } from "./rln.js";
const log = debug("waku:rln:encoder");
export class RLNEncoder implements Encoder {
public contentTopic: string;
private readonly idKey: Uint8Array;
export class RLNEncoder implements IEncoder {
private readonly idSecretHash: Uint8Array;
constructor(
private encoder: Encoder,
private encoder: IEncoder,
private rlnInstance: RLNInstance,
private index: number,
membershipKey: MembershipKey
identityCredential: IdentityCredential
) {
if (index < 0) throw "invalid membership index";
this.idKey = membershipKey.IDKey;
this.contentTopic = encoder.contentTopic;
this.idSecretHash = identityCredential.IDSecretHash;
}
async toWire(message: Partial<Message>): Promise<Uint8Array | undefined> {
message.contentTopic = this.contentTopic;
async toWire(message: IMessage): Promise<Uint8Array | undefined> {
message.rateLimitProof = await this.generateProof(message);
log("Proof generated", message.rateLimitProof);
return this.encoder.toWire(message);
}
async toProtoObj(
message: Partial<Message>
): Promise<ProtoMessage | undefined> {
message.contentTopic = this.contentTopic;
async toProtoObj(message: IMessage): Promise<IProtoMessage | undefined> {
const protoMessage = await this.encoder.toProtoObj(message);
if (!protoMessage) return;
protoMessage.contentTopic = this.contentTopic;
protoMessage.rateLimitProof = await this.generateProof(message);
log("Proof generated", protoMessage.rateLimitProof);
return protoMessage;
}
private async generateProof(
message: Partial<Message>
): Promise<RateLimitProof> {
const signal = toRLNSignal(message);
console.time("proof_gen_timer");
const proof = await this.rlnInstance.generateRLNProof(
private async generateProof(message: IMessage): Promise<IRateLimitProof> {
const signal = toRLNSignal(this.contentTopic, message);
const proof = await this.rlnInstance.zerokit.generateRLNProof(
signal,
this.index,
message.timestamp,
this.idKey
this.idSecretHash
);
console.timeEnd("proof_gen_timer");
return proof;
}
get pubsubTopic(): string {
return this.encoder.pubsubTopic;
}
get contentTopic(): string {
return this.encoder.contentTopic;
}
get ephemeral(): boolean {
return this.encoder.ephemeral;
}
}
export class RLNDecoder<T extends Message> implements Decoder<RlnMessage<T>> {
constructor(private rlnInstance: RLNInstance, private decoder: Decoder<T>) {}
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<T extends IDecodedMessage>
implements IDecoder<RlnMessage<T>>
{
constructor(
private rlnInstance: RLNInstance,
private decoder: IDecoder<T>
) {}
get pubsubTopic(): string {
return this.decoder.pubsubTopic;
}
get contentTopic(): string {
return this.decoder.contentTopic;
}
fromWireToProtoObj(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
fromWireToProtoObj(bytes: Uint8Array): Promise<IProtoMessage | undefined> {
const protoMessage = this.decoder.fromWireToProtoObj(bytes);
log("Message decoded", protoMessage);
return Promise.resolve(protoMessage);
}
async fromProtoObj(proto: ProtoMessage): Promise<RlnMessage<T> | undefined> {
const msg: T | undefined = await this.decoder.fromProtoObj(proto);
async fromProtoObj(
pubsubTopic: string,
proto: IProtoMessage
): Promise<RlnMessage<T> | undefined> {
const msg: T | undefined = await this.decoder.fromProtoObj(
pubsubTopic,
proto
);
if (!msg) return;
return new RlnMessage(this.rlnInstance, msg, proto.rateLimitProof);
}
}
type RLNDecoderOptions<T extends IDecodedMessage> = {
decoder: IDecoder<T>;
rlnInstance: RLNInstance;
};
export const createRLNDecoder = <T extends IDecodedMessage>(
options: RLNDecoderOptions<T>
): RLNDecoder<T> => {
return new RLNDecoder(options.rlnInstance, options.decoder);
};

68
src/contract/constants.ts Normal file
View File

@ -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
};

2
src/contract/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { RLNContract } from "./rln_contract.js";
export * from "./constants.js";

View File

@ -0,0 +1,78 @@
import chai from "chai";
import spies from "chai-spies";
import * as ethers from "ethers";
import { createRLN } from "../create.js";
import { SEPOLIA_CONTRACT } from "./constants.js";
import { RLNContract } from "./rln_contract.js";
chai.use(spies);
describe("RLN Contract abstraction", () => {
it("should be able to fetch members from events and store to rln instance", async () => {
const rlnInstance = await createRLN();
rlnInstance.zerokit.insertMember = () => undefined;
const insertMemberSpy = chai.spy.on(rlnInstance.zerokit, "insertMember");
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);
chai.expect(insertMemberSpy).to.have.been.called();
});
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
});
rlnContract["storageIndex"] = 1;
rlnContract["_membersFilter"] = {
address: "",
topics: []
} as unknown as ethers.EventFilter;
rlnContract["registryContract"] = {
"register(uint16,uint256)": () =>
Promise.resolve({ wait: () => Promise.resolve(undefined) })
} as unknown as ethers.Contract;
const contractSpy = chai.spy.on(
rlnContract["registryContract"],
"register(uint16,uint256)"
);
const identity =
rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature);
await rlnContract.registerWithIdentity(identity);
chai.expect(contractSpy).to.have.been.called();
});
});
function mockEvent(): ethers.Event {
return {
args: {
idCommitment: { _hex: "0xb3df1c4e5600ef2b" },
index: ethers.BigNumber.from(1)
}
} as unknown as ethers.Event;
}

View File

@ -0,0 +1,353 @@
import { hexToBytes } from "@waku/utils/bytes";
import debug from "debug";
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 = debug("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<number, Member> = new Map();
public static async init(
rlnInstance: RLNInstance,
options: RLNContractInitOptions
): Promise<RLNContract> {
const rlnContract = new RLNContract(rlnInstance, options);
await rlnContract.initStorageContract(options.signer);
await rlnContract.fetchMembers(rlnInstance);
rlnContract.subscribeToMembers(rlnInstance);
return rlnContract;
}
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<void> {
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<void> {
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<number, number[]>();
const toInsertTable = new Map<number, ethers.Event[]>();
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<number, ethers.Event[]>
): 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<number, number[]>
): 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<DecryptedCredentials | undefined> {
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<ethers.Event[]> {
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<T>(array: T[], size: number): Iterable<T[]> {
let start = 0;
while (start < array.length) {
const portion = array.slice(start, start + size);
yield portion;
start += size;
}
}
function ignoreErrors<T>(promise: Promise<T>, defaultValue: T): Promise<T> {
return promise.catch((err) => {
log(`Ignoring an error during query: ${err?.message}`);
return defaultValue;
});
}

View File

@ -1,12 +1,12 @@
import { assert, expect } from "chai";
import * as rln from "./index.js";
import { createRLN } from "./create.js";
describe("js-rln", () => {
it("should verify a proof", async function () {
const rlnInstance = await rln.create();
const rlnInstance = await createRLN();
const memKeys = rlnInstance.generateMembershipKey();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
//peer's index in the Merkle Tree
const index = 5;
@ -15,11 +15,11 @@ describe("js-rln", () => {
for (let i = 0; i < 10; i++) {
if (i == index) {
// insert the current peer's pk
rlnInstance.insertMember(memKeys.IDCommitment);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
} else {
// create a new key pair
rlnInstance.insertMember(
rlnInstance.generateMembershipKey().IDCommitment
rlnInstance.zerokit.insertMember(
rlnInstance.zerokit.generateIdentityCredentials().IDCommitment
);
}
}
@ -33,16 +33,16 @@ describe("js-rln", () => {
const epoch = new Date();
// generating proof
const proof = await rlnInstance.generateRLNProof(
const proof = await rlnInstance.zerokit.generateRLNProof(
uint8Msg,
index,
epoch,
memKeys.IDKey
credential.IDSecretHash
);
try {
// verify the proof
const verifResult = rlnInstance.verifyRLNProof(proof, uint8Msg);
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
expect(verifResult).to.be.true;
} catch (err) {
assert.fail(0, 1, "should not have failed proof verification");
@ -52,16 +52,17 @@ describe("js-rln", () => {
// Modifying the signal so it's invalid
uint8Msg[4] = 4;
// verify the proof
const verifResult = rlnInstance.verifyRLNProof(proof, uint8Msg);
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 rln.create();
const rlnInstance = await createRLN();
const seed = "This is a test seed";
const memKeys = rlnInstance.generateSeededMembershipKey(seed);
const credential =
rlnInstance.zerokit.generateSeededIdentityCredential(seed);
//peer's index in the Merkle Tree
const index = 5;
@ -70,11 +71,11 @@ describe("js-rln", () => {
for (let i = 0; i < 10; i++) {
if (i == index) {
// insert the current peer's pk
rlnInstance.insertMember(memKeys.IDCommitment);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
} else {
// create a new key pair
rlnInstance.insertMember(
rlnInstance.generateMembershipKey().IDCommitment
rlnInstance.zerokit.insertMember(
rlnInstance.zerokit.generateIdentityCredentials().IDCommitment
);
}
}
@ -88,16 +89,16 @@ describe("js-rln", () => {
const epoch = new Date();
// generating proof
const proof = await rlnInstance.generateRLNProof(
const proof = await rlnInstance.zerokit.generateRLNProof(
uint8Msg,
index,
epoch,
memKeys.IDKey
credential.IDSecretHash
);
try {
// verify the proof
const verifResult = rlnInstance.verifyRLNProof(proof, uint8Msg);
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
expect(verifResult).to.be.true;
} catch (err) {
assert.fail(0, 1, "should not have failed proof verification");
@ -107,23 +108,30 @@ describe("js-rln", () => {
// Modifying the signal so it's invalid
uint8Msg[4] = 4;
// verify the proof
const verifResult = rlnInstance.verifyRLNProof(proof, uint8Msg);
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 rln.create();
const rlnInstance = await createRLN();
const seed = "This is a test seed";
const memKeys1 = rlnInstance.generateSeededMembershipKey(seed);
const memKeys2 = rlnInstance.generateSeededMembershipKey(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.IDKey.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDKey[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]);
});
});
});

9
src/create.ts Normal file
View File

@ -0,0 +1,9 @@
import type { RLNInstance } from "./rln.js";
export async function createRLN(): Promise<RLNInstance> {
// 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();
}

27
src/identity.ts Normal file
View File

@ -0,0 +1,27 @@
import { buildBigIntFromUint8Array } from "./utils/index.js";
export class IdentityCredential {
constructor(
public readonly IDTrapdoor: Uint8Array,
public readonly IDNullifier: Uint8Array,
public readonly IDSecretHash: Uint8Array,
public readonly IDCommitment: Uint8Array,
public readonly IDCommitmentBigInt: bigint
) {}
static fromBytes(memKeys: Uint8Array): IdentityCredential {
const idTrapdoor = memKeys.subarray(0, 32);
const idNullifier = memKeys.subarray(32, 64);
const idSecretHash = memKeys.subarray(64, 96);
const idCommitment = memKeys.subarray(96);
const idCommitmentBigInt = buildBigIntFromUint8Array(idCommitment, 96);
return new IdentityCredential(
idTrapdoor,
idNullifier,
idSecretHash,
idCommitment,
idCommitmentBigInt
);
}
}

View File

@ -1,14 +1,30 @@
import { RLNDecoder, RLNEncoder } from "./codec.js";
import type { Proof, RLNInstance } from "./rln.js";
import { MembershipKey } from "./rln.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";
// reexport the create function, dynamically imported from rln.ts
export async function create(): Promise<RLNInstance> {
// 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 await rlnModule.create();
}
export { RLNInstance, MembershipKey, Proof, RLNEncoder, RLNDecoder };
export {
createRLN,
Keystore,
RLNInstance,
IdentityCredential,
Proof,
RLNEncoder,
RLNDecoder,
MerkleRootTracker,
RLNContract,
RLN_STORAGE_ABI,
RLN_REGISTRY_ABI,
SEPOLIA_CONTRACT,
extractMetaMaskSigner
};

54
src/keystore/cipher.ts Normal file
View File

@ -0,0 +1,54 @@
import type { IKeystore as IEipKeystore } from "@chainsafe/bls-keystore";
import { cipherDecrypt } from "@chainsafe/bls-keystore/lib/cipher.js";
import { kdf } from "@chainsafe/bls-keystore/lib/kdf.js";
import { normalizePassword } from "@chainsafe/bls-keystore/lib/password.js";
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<boolean> => {
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<Uint8Array> => {
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<Keccak256Hash> => {
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);
};

View File

@ -0,0 +1,8 @@
/* 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;}

5
src/keystore/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { Keystore } from "./keystore.js";
import type { DecryptedCredentials, EncryptedCredentials } from "./types.js";
export { Keystore };
export type { EncryptedCredentials, DecryptedCredentials };

View File

@ -0,0 +1,311 @@
import chai, { expect } from "chai";
import chaiAsPromised from "chai-as-promised";
import chaiSubset from "chai-subset";
import deepEqualInAnyOrder from "deep-equal-in-any-order";
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);
});
});

330
src/keystore/keystore.ts Normal file
View File

@ -0,0 +1,330 @@
import type {
ICipherModule,
IKeystore as IEipKeystore,
IPbkdf2KdfModule
} from "@chainsafe/bls-keystore";
import { create as createEipKeystore } from "@chainsafe/bls-keystore";
import debug from "debug";
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 = debug("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("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<MembershipHash> {
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<undefined | KeystoreEntity> {
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("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
}
})
);
}
}

View File

@ -0,0 +1,8 @@
/* 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;}

View File

@ -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);
}

36
src/keystore/types.ts Normal file
View File

@ -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;
};

View File

@ -1,41 +1,51 @@
import { utils } from "js-waku";
import { Message, RateLimitProof } from "js-waku/lib/interfaces";
import type {
IDecodedMessage,
IMessage,
IRateLimitProof
} from "@waku/interfaces";
import * as utils from "@waku/utils/bytes";
import { epochBytesToInt } from "./epoch.js";
import { RLNInstance } from "./rln.js";
import { epochBytesToInt } from "./utils/index.js";
export function toRLNSignal(msg: Partial<Message>): Uint8Array {
const contentTopicBytes = utils.utf8ToBytes(msg.contentTopic ?? "");
export function toRLNSignal(contentTopic: string, msg: IMessage): Uint8Array {
const contentTopicBytes = utils.utf8ToBytes(contentTopic ?? "");
return new Uint8Array([...(msg.payload ?? []), ...contentTopicBytes]);
}
export class RlnMessage<T extends Message> implements Message {
export class RlnMessage<T extends IDecodedMessage> implements IDecodedMessage {
public pubsubTopic = "";
constructor(
public rlnInstance: RLNInstance,
public msg: T,
public rateLimitProof: RateLimitProof | undefined
public rateLimitProof: IRateLimitProof | undefined
) {}
public verify(): boolean | undefined {
public verify(roots: Uint8Array[]): boolean | undefined {
return this.rateLimitProof
? this.rlnInstance.verifyWithRoots(this.rateLimitProof, toRLNSignal(this)) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed
? 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.verifyWithNoRoot(
? this.rlnInstance.zerokit.verifyWithNoRoot(
this.rateLimitProof,
toRLNSignal(this)
toRLNSignal(this.msg.contentTopic, this.msg)
) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed
: undefined;
}
get payload(): Uint8Array | undefined {
get payload(): Uint8Array {
return this.msg.payload;
}
get contentTopic(): string | undefined {
get contentTopic(): string {
return this.msg.contentTopic;
}
@ -43,6 +53,14 @@ export class RlnMessage<T extends Message> implements Message {
return this.msg.timestamp;
}
get ephemeral(): boolean | undefined {
return this.msg.ephemeral;
}
get meta(): Uint8Array | undefined {
return this.msg.meta;
}
get epoch(): number | undefined {
const bytes = this.msg.rateLimitProof?.epoch;
if (!bytes) return;

67
src/proof.ts Normal file
View File

@ -0,0 +1,67 @@
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 {
constructor(
public readonly nullifier: Uint8Array,
public readonly shareX: Uint8Array,
public readonly shareY: Uint8Array,
public readonly externalNullifier: Uint8Array
) {}
}
export class Proof implements IRateLimitProof {
readonly proof: Uint8Array;
readonly merkleRoot: Uint8Array;
readonly epoch: Uint8Array;
readonly shareX: Uint8Array;
readonly shareY: Uint8Array;
readonly nullifier: Uint8Array;
readonly rlnIdentifier: Uint8Array;
constructor(proofBytes: Uint8Array) {
if (proofBytes.length < rlnIdentifierOffset) throw "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
);
}
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
);
}

Binary file not shown.

Binary file not shown.

View File

@ -1,120 +1,112 @@
const verificationKey = {
"protocol": "groth16",
"curve": "bn128",
"nPublic": 6,
"vk_alpha_1": [
"1805378556360488226980822394597799963030511477964155500103132920745199284516",
"11990395240534218699464972016456017378439762088320057798320175886595281336136",
"1"
protocol: "groth16",
curve: "bn128",
nPublic: 6,
vk_alpha_1: [
"20124996762962216725442980738609010303800849578410091356605067053491763969391",
"9118593021526896828671519912099489027245924097793322973632351264852174143923",
"1",
],
"vk_beta_2": [
vk_beta_2: [
[
"11031529986141021025408838211017932346992429731488270384177563837022796743627",
"16042159910707312759082561183373181639420894978640710177581040523252926273854"
"4693952934005375501364248788849686435240706020501681709396105298107971354382",
"14346958885444710485362620645446987998958218205939139994511461437152241966681",
],
[
"20112698439519222240302944148895052359035104222313380895334495118294612255131",
"19441583024670359810872018179190533814486480928824742448673677460151702019379"
"16851772916911573982706166384196538392731905827088356034885868448550849804972",
"823612331030938060799959717749043047845343400798220427319188951998582076532",
],
[
"1",
"0"
]
["1", "0"],
],
"vk_gamma_2": [
vk_gamma_2: [
[
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
"11559732032986387107991004021392285783925812861821192530917403151452391805634",
],
[
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
"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",
"0"
]
],
"vk_delta_2": [
[
"1948496782571164085469528023647105317580208688174386157591917599801657832035",
"20445814069256658101339037520922621162739470138213615104905368409238414511981"
],
[
"10024680869920840984813249386422727863826862577760330492647062850849851925340",
"10512156247842686783409460795717734694774542185222602679117887145206209285142"
],
[
"17018665030246167677911144513385572506766200776123272044534328594850561667818",
"18601114175490465275436712413925513066546725461375425769709566180981674884464",
"1",
"0"
]
],
[
"18799470100699658367834559797874857804183288553462108031963980039244731716542",
"13064227487174191981628537974951887429496059857753101852163607049188825592007",
"1",
],
[
"17432501889058124609368103715904104425610382063762621017593209214189134571156",
"13406815149699834788256141097399354592751313348962590382887503595131085938635",
"1",
],
[
"10320964835612716439094703312987075811498239445882526576970512041988148264481",
"9024164961646353611176283204118089412001502110138072989569118393359029324867",
"1",
],
[
"718355081067365548229685160476620267257521491773976402837645005858953849298",
"14635482993933988261008156660773180150752190597753512086153001683711587601974",
"1",
],
[
"11777720285956632126519898515392071627539405001940313098390150593689568177535",
"8483603647274280691250972408211651407952870456587066148445913156086740744515",
"1",
],
],
"vk_alphabeta_12": [
[
[
"5151991366823434428398919091000210787450832786814248297320989361921939794156",
"15735191313289001022885148627913534790382722933676436876510746491415970766821"
],
[
"3387907257437913904447588318761906430938415556102110876587455322225272831272",
"1998779853452712881084781956683721603875246565720647583735935725110674288056"
],
[
"14280074182991498185075387990446437410077692353432005297922275464876153151820",
"17092408446352310039633488224969232803092763095456307462247653153107223117633"
]
],
[
[
"4359046709531668109201634396816565829237358165496082832279660960675584351266",
"4511888308846208349307186938266411423935335853916317436093178288331845821336"
],
[
"11429499807090785857812316277335883295048773373068683863667725283965356423273",
"16232274853200678548795010078253506586114563833318973594428907292096178657392"
],
[
"18068999605870933925311275504102553573815570223888590384919752303726860800970",
"17309569111965782732372130116757295842160193489132771344011460471298173784984"
]
]
],
"IC": [
[
"18693301901828818437917730940595978397160482710354161265484535387752523310572",
"17985273354976640088538673802000794244421192643855111089693820179790551470769",
"1"
],
[
"21164641723988537620541455173278629777250883365474191521194244273980931825942",
"998385854410718613441067082771678946155853656328717326195057262123686425518",
"1"
],
[
"21666968581672145768705229094968410656430989593283335488162701230986314747515",
"17996457608540683483506630273632100555125353447506062045735279661096094677264",
"1"
],
[
"20137761979695192602424300886442379728165712610493092740175904438282083668117",
"19184814924890679891263780109959113289320127263583260218200636509492157834679",
"1"
],
[
"10943171273393803842589314082509655332154393332394322726077270895078286354146",
"10872472035685319847811233167729172672344935625121511932198535224727331126439",
"1"
],
[
"13049169779481227658517545034348883391527506091990880778783387628208561946597",
"10083689369261379027228809473568899816311684698866922944902456565434209079955",
"1"
],
[
"19633516378466409167014413361365552102431118630694133723053441455184566611083",
"8059525100726933978719058611146131904598011633549012007359165766216730722269",
"1"
]
]
}
export default verificationKey
};
export default verificationKey;

View File

@ -4,5 +4,5 @@ export async function builder(
): Promise<WitnessCalculator>;
export class WitnessCalculator {
calculateWitness(input, sanityCheck): Array<bigint>;
calculateWitness(input, sanityCheck): Promise<Array<bigint>>;
}

View File

@ -1,34 +1,32 @@
import init, * as zerokitRLN from "@waku/zerokit-rln-wasm";
import { RateLimitProof } from "js-waku/lib/interfaces";
import { createDecoder, createEncoder } from "@waku/core";
import type {
ContentTopic,
IDecodedMessage,
EncoderOptions as WakuEncoderOptions
} from "@waku/interfaces";
import init from "@waku/zerokit-rln-wasm";
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
import { ethers } from "ethers";
import { writeUIntLE } from "./byte_utils.js";
import { dateToEpoch, epochIntToBytes } from "./epoch.js";
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.js";
import * as wc from "./witness_calculator.js";
import { WitnessCalculator } from "./witness_calculator.js";
/**
* Concatenate Uint8Arrays
* @param input
* @returns concatenation of all Uint8Array received as input
*/
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;
}
const stringEncoder = new TextEncoder();
const DEPTH = 20;
import * as wc from "./resources/witness_calculator.js";
import { WitnessCalculator } from "./resources/witness_calculator.js";
import { extractMetaMaskSigner } from "./utils/index.js";
import { Zerokit } from "./zerokit.js";
async function loadWitnessCalculator(): Promise<WitnessCalculator> {
const url = new URL("./resources/rln.wasm", import.meta.url);
@ -47,212 +45,242 @@ async function loadZkey(): Promise<Uint8Array> {
* @returns RLNInstance
*/
export async function create(): Promise<RLNInstance> {
await init();
// 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);
return new RLNInstance(zkRLN, witnessCalculator);
const zerokit = new Zerokit(zkRLN, witnessCalculator);
return new RLNInstance(zerokit);
}
export class MembershipKey {
constructor(
public readonly IDKey: Uint8Array,
public readonly IDCommitment: Uint8Array
) {}
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;
};
static fromBytes(memKeys: Uint8Array): MembershipKey {
const idKey = memKeys.subarray(0, 32);
const idCommitment = memKeys.subarray(32);
return new MembershipKey(idKey, idCommitment);
}
}
type RegisterMembershipOptions =
| { signature: string }
| { identity: IdentityCredential };
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;
export class Proof implements RateLimitProof {
readonly proof: Uint8Array;
readonly merkleRoot: Uint8Array;
readonly epoch: Uint8Array;
readonly shareX: Uint8Array;
readonly shareY: Uint8Array;
readonly nullifier: Uint8Array;
readonly rlnIdentifier: Uint8Array;
constructor(proofBytes: Uint8Array) {
if (proofBytes.length < rlnIdentifierOffset) throw "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
);
}
}
export function proofToBytes(p: RateLimitProof): Uint8Array {
return concatenate(
p.proof,
p.merkleRoot,
p.epoch,
p.shareX,
p.shareY,
p.nullifier,
p.rlnIdentifier
);
}
type WakuRLNEncoderOptions = WakuEncoderOptions & {
credentials: EncryptedCredentials | DecryptedCredentials;
};
export class RLNInstance {
constructor(
private zkRLN: number,
private witnessCalculator: WitnessCalculator
) {}
private started = false;
private starting = false;
generateMembershipKey(): MembershipKey {
const memKeys = zerokitRLN.generateMembershipKey(this.zkRLN);
return MembershipKey.fromBytes(memKeys);
private _contract: undefined | RLNContract;
private _signer: undefined | ethers.Signer;
private keystore = Keystore.create();
private _credentials: undefined | DecryptedCredentials;
constructor(public zerokit: Zerokit) {}
public get contract(): undefined | RLNContract {
return this._contract;
}
generateSeededMembershipKey(seed: string): MembershipKey {
const seedBytes = stringEncoder.encode(seed);
const memKeys = zerokitRLN.generateSeededMembershipKey(
this.zkRLN,
seedBytes
);
return MembershipKey.fromBytes(memKeys);
public get signer(): undefined | ethers.Signer {
return this._signer;
}
insertMember(idCommitment: Uint8Array): void {
zerokitRLN.insertMember(this.zkRLN, idCommitment);
}
getMerkleRoot(): Uint8Array {
return zerokitRLN.getRoot(this.zkRLN);
}
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<var> ]
return concatenate(idKey, memIndexBytes, epoch, msgLen, uint8Msg);
}
async generateRLNProof(
msg: Uint8Array,
index: number,
epoch: Uint8Array | Date | undefined,
idKey: Uint8Array
): Promise<RateLimitProof> {
if (epoch == undefined) {
epoch = epochIntToBytes(dateToEpoch(new Date()));
} else if (epoch instanceof Date) {
epoch = epochIntToBytes(dateToEpoch(epoch));
public async start(options: StartRLNOptions = {}): Promise<void> {
if (this.started || this.starting) {
return;
}
if (epoch.length != 32) throw "invalid epoch";
if (idKey.length != 32) throw "invalid id key";
if (index < 0) throw "index must be >= 0";
this.starting = true;
const serialized_msg = this.serializeMessage(msg, index, epoch, idKey);
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
try {
const { credentials, keystore } =
await RLNInstance.decryptCredentialsIfNeeded(options.credentials);
const { signer, registryAddress } = await this.determineStartOptions(
options,
credentials
);
const proofBytes = zerokitRLN.generate_rln_proof_with_witness(
this.zkRLN,
calculatedWitness,
rlnWitness
);
if (keystore) {
this.keystore = keystore;
}
return new Proof(proofBytes);
this._credentials = credentials;
this._signer = signer!;
this._contract = await RLNContract.init(this, {
registryAddress: registryAddress!,
signer: signer!
});
this.started = true;
} finally {
this.starting = false;
}
}
verifyRLNProof(proof: RateLimitProof | Uint8Array, msg: Uint8Array): boolean {
let pBytes: Uint8Array;
if (proof instanceof Uint8Array) {
pBytes = proof;
} else {
pBytes = proofToBytes(proof);
private async determineStartOptions(
options: StartRLNOptions,
credentials: KeystoreEntity | undefined
): Promise<StartRLNOptions> {
let chainId = credentials?.membership.chainId;
const registryAddress =
credentials?.membership.address ||
options.registryAddress ||
SEPOLIA_CONTRACT.address;
if (registryAddress === SEPOLIA_CONTRACT.address) {
chainId = SEPOLIA_CONTRACT.chainId;
}
// calculate message length
const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
const signer = options.signer || (await extractMetaMaskSigner());
const currentChainId = await signer.getChainId();
return zerokitRLN.verifyRLNProof(
this.zkRLN,
concatenate(pBytes, msgLen, msg)
);
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
};
}
verifyWithRoots(
proof: RateLimitProof | Uint8Array,
msg: Uint8Array
): boolean {
let pBytes: Uint8Array;
if (proof instanceof Uint8Array) {
pBytes = proof;
} else {
pBytes = proofToBytes(proof);
private static async decryptCredentialsIfNeeded(
credentials?: EncryptedCredentials | DecryptedCredentials
): Promise<{ credentials?: DecryptedCredentials; keystore?: Keystore }> {
if (!credentials) {
return {};
}
// calculate message length
const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
if ("identity" in credentials) {
return { credentials };
}
// obtain root
const root = zerokitRLN.getRoot(this.zkRLN);
const keystore = Keystore.fromString(credentials.keystore);
return zerokitRLN.verifyWithRoots(
this.zkRLN,
concatenate(pBytes, msgLen, msg),
root
if (!keystore) {
return {};
}
const decryptedCredentials = await keystore.readCredential(
credentials.id,
credentials.password
);
return {
keystore,
credentials: decryptedCredentials
};
}
verifyWithNoRoot(
proof: RateLimitProof | Uint8Array,
msg: Uint8Array
): boolean {
let pBytes: Uint8Array;
if (proof instanceof Uint8Array) {
pBytes = proof;
} else {
pBytes = proofToBytes(proof);
public async registerMembership(
options: RegisterMembershipOptions
): Promise<undefined | DecryptedCredentials> {
if (!this.contract) {
throw Error("RLN Contract is not initialized.");
}
// calculate message length
const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
let identity = "identity" in options && options.identity;
return zerokitRLN.verifyWithRoots(
this.zkRLN,
concatenate(pBytes, msgLen, msg),
new Uint8Array()
);
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<void> {
this._credentials = await this.keystore?.readCredential(id, password);
}
public async createEncoder(
options: WakuRLNEncoderOptions
): Promise<RLNEncoder> {
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<void> {
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<IDecodedMessage> {
return createRLNDecoder({
rlnInstance: this,
decoder: createDecoder(contentTopic)
});
}
}

56
src/root_tracker.spec.ts Normal file
View File

@ -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]));
});
});

91
src/root_tracker.ts Normal file
View File

@ -0,0 +1,91 @@
class RootPerBlock {
constructor(
public root: Uint8Array,
public blockNumber: number
) {}
}
const maxBufferSize = 20;
export class MerkleRootTracker {
private validMerkleRoots: Array<RootPerBlock> = new Array<RootPerBlock>();
private merkleRootBuffer: Array<RootPerBlock> = new Array<RootPerBlock>();
constructor(
private acceptableRootWindowSize: number,
initialRoot: Uint8Array
) {
this.pushRoot(0, initialRoot);
}
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);
}
}
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();
}
}
roots(): Array<Uint8Array> {
return this.validMerkleRoots.map((x) => x.root);
}
buffer(): Array<Uint8Array> {
return this.merkleRootBuffer.map((x) => x.root);
}
}

84
src/utils/bytes.ts Normal file
View File

@ -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;
}

15
src/utils/hash.ts Normal file
View File

@ -0,0 +1,15 @@
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
import { concatenate, writeUIntLE } from "./bytes.js";
export function poseidonHash(...input: Array<Uint8Array>): 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);
}

9
src/utils/index.ts Normal file
View File

@ -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";

17
src/utils/metamask.ts Normal file
View File

@ -0,0 +1,17 @@
import { ethers } from "ethers";
export const extractMetaMaskSigner = async (): Promise<ethers.Signer> => {
// 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();
};

181
src/zerokit.ts Normal file
View File

@ -0,0 +1,181 @@
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.js";
import {
concatenate,
dateToEpoch,
epochIntToBytes,
writeUIntLE
} from "./utils/index.js";
export class Zerokit {
constructor(
private zkRLN: number,
private witnessCalculator: WitnessCalculator
) {}
generateIdentityCredentials(): IdentityCredential {
const memKeys = zerokitRLN.generateExtendedMembershipKey(this.zkRLN); // TODO: rename this function in zerokit rln-wasm
return IdentityCredential.fromBytes(memKeys);
}
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);
}
insertMember(idCommitment: Uint8Array): void {
zerokitRLN.insertMember(this.zkRLN, idCommitment);
}
insertMembers(index: number, ...idCommitments: Array<Uint8Array>): void {
// serializes a seq of IDCommitments to a byte seq
// the order of serialization is |id_commitment_len<8>|id_commitment<var>|
const idCommitmentLen = writeUIntLE(
new Uint8Array(8),
idCommitments.length,
0,
8
);
const idCommitmentBytes = concatenate(idCommitmentLen, ...idCommitments);
zerokitRLN.setLeavesFrom(this.zkRLN, index, idCommitmentBytes);
}
deleteMember(index: number): void {
zerokitRLN.deleteLeaf(this.zkRLN, index);
}
getMerkleRoot(): Uint8Array {
return zerokitRLN.getRoot(this.zkRLN);
}
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<var> ]
return concatenate(idKey, memIndexBytes, epoch, msgLen, uint8Msg);
}
async generateRLNProof(
msg: Uint8Array,
index: number,
epoch: Uint8Array | Date | undefined,
idSecretHash: Uint8Array
): Promise<IRateLimitProof> {
if (epoch == undefined) {
epoch = epochIntToBytes(dateToEpoch(new Date()));
} else if (epoch instanceof Date) {
epoch = epochIntToBytes(dateToEpoch(epoch));
}
if (epoch.length != 32) throw "invalid epoch";
if (idSecretHash.length != 32) throw "invalid id secret hash";
if (index < 0) throw "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);
}
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)
);
}
verifyWithRoots(
proof: IRateLimitProof | Uint8Array,
msg: Uint8Array,
...roots: Array<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);
const rootsBytes = concatenate(...roots);
return zerokitRLN.verifyWithRoots(
this.zkRLN,
concatenate(pBytes, msgLen, msg),
rootsBytes
);
}
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()
);
}
}

View File

@ -1,8 +1,8 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"module": "esnext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true
},
"exclude": []
}
}
}

View File

@ -1,18 +1,15 @@
{
"compilerOptions": {
"incremental": true,
"target": "es2020",
"outDir": "dist/",
"rootDir": "src",
"moduleResolution": "node",
"module": "es2020",
"target": "ES2022",
"moduleResolution": "Bundler",
"module": "ES2022",
"declaration": true,
"allowJs": true,
"sourceMap": true,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"resolveJsonModule": true /* Include modules imported with .json extension. */,
"tsBuildInfoFile": "dist/.tsbuildinfo",
"strict": true /* Enable all strict type-checking options. */,
/* Strict Type-Checking Options */
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
@ -20,30 +17,37 @@
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"forceConsistentCasingInFileNames": true,
/* Debugging Options */
"traceResolution": false /* Report module resolution log messages. */,
"listEmittedFiles": false /* Print names of generated files part of the compilation. */,
"listFiles": false /* Print names of files part of the compilation. */,
"pretty": true /* Stylize errors and messages using color and context. */,
// Due to broken types in indirect dependencies
"skipLibCheck": true,
"allowJs": true,
/* Experimental Options */
// "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
// "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
"lib": ["es2020", "dom"],
"lib": ["es2022", "dom"],
"types": ["node", "mocha"],
"typeRoots": ["node_modules/@types", "src/types"]
"typeRoots": ["node_modules/@types"],
"outDir": "dist/",
"rootDir": "src",
},
"include": ["src"],
"exclude": ["src/**/*.spec.ts", "src/test_utils"],
"compileOnSave": false,
"ts-node": {
"files": true
}
}
}