mirror of
https://github.com/logos-messaging/js-rln.git
synced 2026-01-11 01:53:13 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6d5deb8cb | ||
|
|
126bce32f7 | ||
|
|
3dc59084c9 | ||
|
|
6454446c1c | ||
|
|
65b7d8bcbc | ||
|
|
e1679b6bd9 | ||
|
|
86d4f56818 | ||
|
|
ac12f6800a | ||
|
|
77ba0a6d5d | ||
|
|
bafbe01e52 | ||
|
|
18ce994d5e | ||
|
|
9b1e8187da | ||
|
|
0c98a266e2 | ||
|
|
60a50709c7 | ||
|
|
0fbf6beec8 | ||
|
|
530034bf13 | ||
|
|
7e8cb899be | ||
|
|
fa49e29856 | ||
|
|
89283768c3 | ||
|
|
5b9414aede | ||
|
|
891ee3474a | ||
|
|
5d7f77f300 | ||
|
|
b429b055db | ||
|
|
5fb26dfd16 | ||
|
|
71aa967a86 | ||
|
|
fa1d3f8222 | ||
|
|
7e93896538 | ||
|
|
b7cb3f9bbf | ||
|
|
e52107dcf8 | ||
|
|
7e0966aef7 | ||
|
|
88a28a11e3 | ||
|
|
360dc829f5 | ||
|
|
fae4bea48d | ||
|
|
bdf388f8d5 | ||
|
|
45d1b28286 | ||
|
|
ed22a3f66b | ||
|
|
d77370fbec |
25
.cspell.json
25
.cspell.json
@ -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"]
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -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
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
||||
*/**/*_generated.ts
|
||||
111
README.md
111
README.md
@ -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.
|
||||
|
||||
@ -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>
|
||||
|
||||
181
example/index.js
181
example/index.js
@ -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
20915
example/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
16757
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
68
package.json
68
package.json
@ -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
0
runtime.js
Normal file
23
scripts/nwaku_keystore.json
Normal file
23
scripts/nwaku_keystore.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
42
scripts/nwaku_keystore_credential.json
Normal file
42
scripts/nwaku_keystore_credential.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
77
scripts/schema_validator_codegen.js
Normal file
77
scripts/schema_validator_codegen.js
Normal 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);
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
116
src/codec.ts
116
src/codec.ts
@ -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
68
src/contract/constants.ts
Normal 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
2
src/contract/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { RLNContract } from "./rln_contract.js";
|
||||
export * from "./constants.js";
|
||||
78
src/contract/rln_contract.spec.ts
Normal file
78
src/contract/rln_contract.spec.ts
Normal 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;
|
||||
}
|
||||
353
src/contract/rln_contract.ts
Normal file
353
src/contract/rln_contract.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
@ -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
9
src/create.ts
Normal 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
27
src/identity.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
40
src/index.ts
40
src/index.ts
@ -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
54
src/keystore/cipher.ts
Normal 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);
|
||||
};
|
||||
8
src/keystore/credential_validation_generated.ts
Normal file
8
src/keystore/credential_validation_generated.ts
Normal 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
5
src/keystore/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Keystore } from "./keystore.js";
|
||||
import type { DecryptedCredentials, EncryptedCredentials } from "./types.js";
|
||||
|
||||
export { Keystore };
|
||||
export type { EncryptedCredentials, DecryptedCredentials };
|
||||
311
src/keystore/keystore.spec.ts
Normal file
311
src/keystore/keystore.spec.ts
Normal 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
330
src/keystore/keystore.ts
Normal 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
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
8
src/keystore/keystore_validation_generated.ts
Normal file
8
src/keystore/keystore_validation_generated.ts
Normal 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;}
|
||||
34
src/keystore/schema_validator.ts
Normal file
34
src/keystore/schema_validator.ts
Normal 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
36
src/keystore/types.ts
Normal 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;
|
||||
};
|
||||
@ -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
67
src/proof.ts
Normal 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.
@ -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;
|
||||
|
||||
@ -4,5 +4,5 @@ export async function builder(
|
||||
): Promise<WitnessCalculator>;
|
||||
|
||||
export class WitnessCalculator {
|
||||
calculateWitness(input, sanityCheck): Array<bigint>;
|
||||
calculateWitness(input, sanityCheck): Promise<Array<bigint>>;
|
||||
}
|
||||
424
src/rln.ts
424
src/rln.ts
@ -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
56
src/root_tracker.spec.ts
Normal 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
91
src/root_tracker.ts
Normal 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
84
src/utils/bytes.ts
Normal 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
15
src/utils/hash.ts
Normal 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
9
src/utils/index.ts
Normal 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
17
src/utils/metamask.ts
Normal 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
181
src/zerokit.ts
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user