mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-02 13:53:12 +00:00
feat: @waku/rln (#2244)
* chore: setup rln as a new package * chore: migrate src * fix: wasm loading, tests, config * chore: fix Karma CI * fix: bundler * chore: copy dist resources * chore(rln): enable all tests * chore: increase karma timeouts
This commit is contained in:
parent
becb46f3a5
commit
0a0a92bccb
10
.cspell.json
10
.cspell.json
@ -7,6 +7,8 @@
|
|||||||
"Addrs",
|
"Addrs",
|
||||||
"ahadns",
|
"ahadns",
|
||||||
"Alives",
|
"Alives",
|
||||||
|
"alphabeta",
|
||||||
|
"Arraylike",
|
||||||
"asym",
|
"asym",
|
||||||
"autoshard",
|
"autoshard",
|
||||||
"autosharding",
|
"autosharding",
|
||||||
@ -17,9 +19,11 @@
|
|||||||
"bufbuild",
|
"bufbuild",
|
||||||
"chainsafe",
|
"chainsafe",
|
||||||
"cimg",
|
"cimg",
|
||||||
|
"cipherparams",
|
||||||
"ciphertext",
|
"ciphertext",
|
||||||
"circleci",
|
"circleci",
|
||||||
"codecov",
|
"codecov",
|
||||||
|
"codegen",
|
||||||
"commitlint",
|
"commitlint",
|
||||||
"dependabot",
|
"dependabot",
|
||||||
"dialable",
|
"dialable",
|
||||||
@ -57,6 +61,7 @@
|
|||||||
"iwant",
|
"iwant",
|
||||||
"jdev",
|
"jdev",
|
||||||
"jswaku",
|
"jswaku",
|
||||||
|
"kdfparams",
|
||||||
"keccak",
|
"keccak",
|
||||||
"keypair",
|
"keypair",
|
||||||
"lastpub",
|
"lastpub",
|
||||||
@ -87,6 +92,7 @@
|
|||||||
"proto",
|
"proto",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"protoc",
|
"protoc",
|
||||||
|
"proxiable",
|
||||||
"reactjs",
|
"reactjs",
|
||||||
"recid",
|
"recid",
|
||||||
"rlnrelay",
|
"rlnrelay",
|
||||||
@ -116,16 +122,18 @@
|
|||||||
"upgrader",
|
"upgrader",
|
||||||
"vacp",
|
"vacp",
|
||||||
"varint",
|
"varint",
|
||||||
"weboko",
|
"vkey",
|
||||||
"waku",
|
"waku",
|
||||||
"wakuconnect",
|
"wakuconnect",
|
||||||
"wakunode",
|
"wakunode",
|
||||||
"wakuorg",
|
"wakuorg",
|
||||||
"wakuv",
|
"wakuv",
|
||||||
"webfonts",
|
"webfonts",
|
||||||
|
"weboko",
|
||||||
"websockets",
|
"websockets",
|
||||||
"wifi",
|
"wifi",
|
||||||
"xsalsa20",
|
"xsalsa20",
|
||||||
|
"zerokit",
|
||||||
"Привет",
|
"Привет",
|
||||||
"مرحبا"
|
"مرحبا"
|
||||||
],
|
],
|
||||||
|
|||||||
1289
package-lock.json
generated
1289
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,8 @@
|
|||||||
"packages/browser-tests",
|
"packages/browser-tests",
|
||||||
"packages/build-utils",
|
"packages/build-utils",
|
||||||
"packages/react-native-polyfills",
|
"packages/react-native-polyfills",
|
||||||
"packages/sds"
|
"packages/sds",
|
||||||
|
"packages/rln"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
@ -70,5 +71,8 @@
|
|||||||
"*.{ts,js}": [
|
"*.{ts,js}": [
|
||||||
"eslint --fix"
|
"eslint --fix"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@waku/utils": "^0.0.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
packages/rln/.eslintrc.cjs
Normal file
7
packages/rln/.eslintrc.cjs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: "./tsconfig.dev.json"
|
||||||
|
},
|
||||||
|
ignorePatterns: ["src/resources/**/*"]
|
||||||
|
};
|
||||||
27
packages/rln/.mocharc.cjs
Normal file
27
packages/rln/.mocharc.cjs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const config = {
|
||||||
|
extension: ['ts'],
|
||||||
|
spec: 'src/**/*.spec.ts',
|
||||||
|
require: ['ts-node/register', 'isomorphic-fetch'],
|
||||||
|
loader: 'ts-node/esm',
|
||||||
|
'node-option': [
|
||||||
|
'experimental-specifier-resolution=node',
|
||||||
|
'loader=ts-node/esm'
|
||||||
|
],
|
||||||
|
exit: true,
|
||||||
|
retries: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.CI) {
|
||||||
|
console.log("Running tests in parallel");
|
||||||
|
config.parallel = true;
|
||||||
|
config.jobs = 6;
|
||||||
|
console.log("Activating allure reporting");
|
||||||
|
config.reporter = 'mocha-multi-reporters';
|
||||||
|
config.reporterOptions = {
|
||||||
|
configFile: '.mocha.reporters.json'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log("Running tests serially. To enable parallel execution update mocha config");
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
25
packages/rln/README.md
Normal file
25
packages/rln/README.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# @waku/rln
|
||||||
|
|
||||||
|
Rate Limiting Nullifier (RLN) implementation for Waku.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This package provides RLN functionality for the Waku protocol, enabling rate-limiting capabilities while preserving privacy.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @waku/rln
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { RLN } from '@waku/rln';
|
||||||
|
|
||||||
|
// Usage examples coming soon
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT OR Apache-2.0
|
||||||
167
packages/rln/karma.conf.cjs
Normal file
167
packages/rln/karma.conf.cjs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const webpack = require("webpack");
|
||||||
|
|
||||||
|
const rootConfig = require("../../karma.conf.cjs");
|
||||||
|
|
||||||
|
module.exports = function (config) {
|
||||||
|
rootConfig(config);
|
||||||
|
|
||||||
|
const configuration = {
|
||||||
|
frameworks: ["mocha", "webpack"],
|
||||||
|
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
pattern: "src/**/*.spec.ts",
|
||||||
|
type: "js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "src/resources/**/*.wasm",
|
||||||
|
included: false,
|
||||||
|
served: true,
|
||||||
|
watched: false,
|
||||||
|
type: "wasm",
|
||||||
|
nocache: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "src/resources/**/*.zkey",
|
||||||
|
included: false,
|
||||||
|
served: true,
|
||||||
|
watched: false,
|
||||||
|
nocache: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "../../node_modules/@waku/zerokit-rln-wasm/*.wasm",
|
||||||
|
included: false,
|
||||||
|
served: true,
|
||||||
|
watched: false,
|
||||||
|
type: "wasm",
|
||||||
|
nocache: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
preprocessors: {
|
||||||
|
"src/**/*.spec.ts": ["webpack"]
|
||||||
|
},
|
||||||
|
|
||||||
|
client: {
|
||||||
|
mocha: {
|
||||||
|
timeout: 180000 // 3 minutes
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
browserDisconnectTimeout: 180000, // 3 minutes
|
||||||
|
browserDisconnectTolerance: 3, // Number of tries before failing
|
||||||
|
browserNoActivityTimeout: 180000, // 3 minutes
|
||||||
|
captureTimeout: 300000, // 5 minutes
|
||||||
|
|
||||||
|
mime: {
|
||||||
|
"application/wasm": ["wasm"],
|
||||||
|
"application/octet-stream": ["zkey"]
|
||||||
|
},
|
||||||
|
|
||||||
|
customHeaders: [
|
||||||
|
{
|
||||||
|
match: ".*\\.wasm$",
|
||||||
|
name: "Content-Type",
|
||||||
|
value: "application/wasm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: ".*\\.zkey$",
|
||||||
|
name: "Content-Type",
|
||||||
|
value: "application/octet-stream"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
proxies: {
|
||||||
|
"/base/rln_wasm_bg.wasm":
|
||||||
|
"/absolute" +
|
||||||
|
path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../node_modules/@waku/zerokit-rln-wasm/rln_wasm_bg.wasm"
|
||||||
|
),
|
||||||
|
"/base/rln.wasm":
|
||||||
|
"/absolute" + path.resolve(__dirname, "src/resources/rln.wasm"),
|
||||||
|
"/base/rln_final.zkey":
|
||||||
|
"/absolute" + path.resolve(__dirname, "src/resources/rln_final.zkey")
|
||||||
|
},
|
||||||
|
|
||||||
|
webpack: {
|
||||||
|
mode: "development",
|
||||||
|
experiments: {
|
||||||
|
asyncWebAssembly: true,
|
||||||
|
syncWebAssembly: true,
|
||||||
|
topLevelAwait: true
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
wasmLoading: "fetch",
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
publicPath: "/base/",
|
||||||
|
clean: true
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
use: "ts-loader",
|
||||||
|
exclude: /node_modules/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.wasm$/,
|
||||||
|
type: "asset/resource",
|
||||||
|
generator: {
|
||||||
|
filename: "[name][ext]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.zkey$/,
|
||||||
|
type: "asset/resource",
|
||||||
|
generator: {
|
||||||
|
filename: "[name][ext]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
"process.env.CI": process.env.CI || false,
|
||||||
|
"process.env.DISPLAY": "Browser"
|
||||||
|
}),
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
process: "process/browser.js"
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".js", ".wasm"],
|
||||||
|
modules: ["node_modules", "../../node_modules"],
|
||||||
|
alias: {
|
||||||
|
"@waku/zerokit-rln-wasm": path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../node_modules/@waku/zerokit-rln-wasm/rln_wasm.js"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
crypto: false,
|
||||||
|
fs: false,
|
||||||
|
path: false,
|
||||||
|
stream: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stats: { warnings: false },
|
||||||
|
devtool: "inline-source-map"
|
||||||
|
},
|
||||||
|
|
||||||
|
reporters: ["progress"],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: process.env.CI ? ["ChromeHeadlessCI"] : ["ChromeHeadless"],
|
||||||
|
singleRun: true,
|
||||||
|
concurrency: 1, // Reduce concurrency to avoid memory pressure
|
||||||
|
browserSocketTimeout: 180000 // 3 minutes
|
||||||
|
};
|
||||||
|
|
||||||
|
config.set(configuration);
|
||||||
|
};
|
||||||
89
packages/rln/package.json
Normal file
89
packages/rln/package.json
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"name": "@waku/rln",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "RLN (Rate Limiting Nullifier) implementation for Waku",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"homepage": "https://github.com/waku-org/js-waku/tree/master/packages/rln#readme",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/waku-org/js-waku.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/waku-org/js-waku/issues"
|
||||||
|
},
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"keywords": [
|
||||||
|
"waku",
|
||||||
|
"rln",
|
||||||
|
"rate-limiting",
|
||||||
|
"privacy",
|
||||||
|
"web3"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "run-s build:**",
|
||||||
|
"build:copy": "mkdir -p dist/resources && cp -r src/resources/* dist/resources/",
|
||||||
|
"build:esm": "tsc",
|
||||||
|
"build:bundle": "rollup --config rollup.config.js",
|
||||||
|
"fix": "run-s fix:*",
|
||||||
|
"fix:lint": "eslint src *.js --fix",
|
||||||
|
"check": "run-s check:*",
|
||||||
|
"check:tsc": "tsc -p tsconfig.dev.json",
|
||||||
|
"check:lint": "eslint \"src/!(resources)/**/*.{ts,js}\" *.js",
|
||||||
|
"check:spelling": "cspell \"{README.md,src/**/*.ts}\"",
|
||||||
|
"test": "NODE_ENV=test run-s test:*",
|
||||||
|
"test:browser": "karma start karma.conf.cjs",
|
||||||
|
"watch:build": "tsc -p tsconfig.json -w",
|
||||||
|
"watch:test": "mocha --watch",
|
||||||
|
"prepublish": "npm run build",
|
||||||
|
"reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^25.0.7",
|
||||||
|
"@rollup/plugin-json": "^6.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
|
"@types/chai": "^5.0.1",
|
||||||
|
"@types/chai-spies": "^1.0.6",
|
||||||
|
"@types/deep-equal-in-any-order": "^1.0.4",
|
||||||
|
"@types/lodash": "^4.17.15",
|
||||||
|
"@waku/build-utils": "^1.0.0",
|
||||||
|
"@waku/message-encryption": "^0.0.31",
|
||||||
|
"chai": "^5.1.2",
|
||||||
|
"chai-as-promised": "^8.0.1",
|
||||||
|
"chai-spies": "^1.1.0",
|
||||||
|
"chai-subset": "^1.6.0",
|
||||||
|
"deep-equal-in-any-order": "^2.0.6",
|
||||||
|
"fast-check": "^3.23.2",
|
||||||
|
"rollup-plugin-copy": "^3.5.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"bundle",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"!**/*.spec.*",
|
||||||
|
"!**/*.json",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@chainsafe/bls-keystore": "3.0.0",
|
||||||
|
"@waku/core": "^0.0.33",
|
||||||
|
"@waku/utils": "^0.0.21",
|
||||||
|
"@waku/zerokit-rln-wasm": "^0.0.13",
|
||||||
|
"ethereum-cryptography": "^3.1.0",
|
||||||
|
"ethers": "^5.7.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"uuid": "^11.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
packages/rln/rollup.config.js
Normal file
35
packages/rln/rollup.config.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import json from "@rollup/plugin-json";
|
||||||
|
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||||
|
import { extractExports } from "@waku/build-utils";
|
||||||
|
import copy from "rollup-plugin-copy";
|
||||||
|
|
||||||
|
import * as packageJson from "./package.json" assert { type: "json" };
|
||||||
|
|
||||||
|
const input = extractExports(packageJson);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input,
|
||||||
|
output: {
|
||||||
|
dir: "bundle",
|
||||||
|
format: "esm",
|
||||||
|
preserveModules: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
copy({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
src: ["src/resources/*"],
|
||||||
|
dest: "bundle/resources"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
copyOnce: true
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
json(),
|
||||||
|
nodeResolve({
|
||||||
|
browser: true,
|
||||||
|
preferBuiltins: false
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
470
packages/rln/src/codec.spec.ts
Normal file
470
packages/rln/src/codec.spec.ts
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
import {
|
||||||
|
createDecoder,
|
||||||
|
createEncoder,
|
||||||
|
DecodedMessage
|
||||||
|
} from "@waku/core/lib/message/version_0";
|
||||||
|
import type { IProtoMessage } from "@waku/interfaces";
|
||||||
|
import {
|
||||||
|
generatePrivateKey,
|
||||||
|
generateSymmetricKey,
|
||||||
|
getPublicKey
|
||||||
|
} from "@waku/message-encryption";
|
||||||
|
import {
|
||||||
|
createDecoder as createAsymDecoder,
|
||||||
|
createEncoder as createAsymEncoder
|
||||||
|
} from "@waku/message-encryption/ecies";
|
||||||
|
import {
|
||||||
|
createDecoder as createSymDecoder,
|
||||||
|
createEncoder as createSymEncoder
|
||||||
|
} from "@waku/message-encryption/symmetric";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createRLNDecoder,
|
||||||
|
createRLNEncoder,
|
||||||
|
RLNDecoder,
|
||||||
|
RLNEncoder
|
||||||
|
} from "./codec.js";
|
||||||
|
import { createRLN } from "./create.js";
|
||||||
|
import { RlnMessage } from "./message.js";
|
||||||
|
import { epochBytesToInt } from "./utils/index.js";
|
||||||
|
|
||||||
|
const TestContentTopic = "/test/1/waku-message/utf8";
|
||||||
|
const EMPTY_PUBSUB_TOPIC = "";
|
||||||
|
|
||||||
|
const EMPTY_PROTO_MESSAGE = {
|
||||||
|
timestamp: undefined,
|
||||||
|
contentTopic: "",
|
||||||
|
ephemeral: undefined,
|
||||||
|
meta: undefined,
|
||||||
|
rateLimitProof: undefined,
|
||||||
|
version: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("RLN codec with version 0", () => {
|
||||||
|
it("toWire", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
||||||
|
const index = 0;
|
||||||
|
const payload = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
|
||||||
|
const rlnEncoder = createRLNEncoder({
|
||||||
|
encoder: createEncoder({ contentTopic: TestContentTopic }),
|
||||||
|
rlnInstance,
|
||||||
|
index,
|
||||||
|
credential
|
||||||
|
});
|
||||||
|
const rlnDecoder = createRLNDecoder({
|
||||||
|
rlnInstance,
|
||||||
|
decoder: createDecoder(TestContentTopic)
|
||||||
|
});
|
||||||
|
|
||||||
|
const bytes = await rlnEncoder.toWire({ payload });
|
||||||
|
|
||||||
|
expect(bytes).to.not.be.undefined;
|
||||||
|
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
|
||||||
|
expect(protoResult).to.not.be.undefined;
|
||||||
|
const msg = (await rlnDecoder.fromProtoObj(
|
||||||
|
EMPTY_PUBSUB_TOPIC,
|
||||||
|
protoResult!
|
||||||
|
))!;
|
||||||
|
|
||||||
|
expect(msg.rateLimitProof).to.not.be.undefined;
|
||||||
|
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
|
||||||
|
expect(msg.verifyNoRoot()).to.be.true;
|
||||||
|
expect(msg.epoch).to.not.be.undefined;
|
||||||
|
expect(msg.epoch).to.be.gt(0);
|
||||||
|
|
||||||
|
expect(msg.contentTopic).to.eq(TestContentTopic);
|
||||||
|
expect(msg.msg.version).to.eq(0);
|
||||||
|
expect(msg.payload).to.deep.eq(payload);
|
||||||
|
expect(msg.timestamp).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toProtoObj", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
||||||
|
const index = 0;
|
||||||
|
const payload = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
|
||||||
|
const rlnEncoder = new RLNEncoder(
|
||||||
|
createEncoder({ contentTopic: TestContentTopic }),
|
||||||
|
rlnInstance,
|
||||||
|
index,
|
||||||
|
credential
|
||||||
|
);
|
||||||
|
const rlnDecoder = new RLNDecoder(
|
||||||
|
rlnInstance,
|
||||||
|
createDecoder(TestContentTopic)
|
||||||
|
);
|
||||||
|
|
||||||
|
const proto = await rlnEncoder.toProtoObj({ payload });
|
||||||
|
|
||||||
|
expect(proto).to.not.be.undefined;
|
||||||
|
const msg = (await rlnDecoder.fromProtoObj(
|
||||||
|
EMPTY_PUBSUB_TOPIC,
|
||||||
|
proto!
|
||||||
|
)) as RlnMessage<DecodedMessage>;
|
||||||
|
|
||||||
|
expect(msg).to.not.be.undefined;
|
||||||
|
expect(msg.rateLimitProof).to.not.be.undefined;
|
||||||
|
|
||||||
|
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
|
||||||
|
expect(msg.verifyNoRoot()).to.be.true;
|
||||||
|
expect(msg.epoch).to.not.be.undefined;
|
||||||
|
expect(msg.epoch).to.be.gt(0);
|
||||||
|
|
||||||
|
expect(msg.contentTopic).to.eq(TestContentTopic);
|
||||||
|
expect(msg.msg.version).to.eq(0);
|
||||||
|
expect(msg.payload).to.deep.eq(payload);
|
||||||
|
expect(msg.timestamp).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("RLN codec with version 1", () => {
|
||||||
|
it("Symmetric, toWire", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
||||||
|
const index = 0;
|
||||||
|
const payload = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
|
||||||
|
const symKey = generateSymmetricKey();
|
||||||
|
|
||||||
|
const rlnEncoder = new RLNEncoder(
|
||||||
|
createSymEncoder({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
symKey
|
||||||
|
}),
|
||||||
|
rlnInstance,
|
||||||
|
index,
|
||||||
|
credential
|
||||||
|
);
|
||||||
|
const rlnDecoder = new RLNDecoder(
|
||||||
|
rlnInstance,
|
||||||
|
createSymDecoder(TestContentTopic, symKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
const bytes = await rlnEncoder.toWire({ payload });
|
||||||
|
|
||||||
|
expect(bytes).to.not.be.undefined;
|
||||||
|
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
|
||||||
|
|
||||||
|
expect(protoResult).to.not.be.undefined;
|
||||||
|
const msg = (await rlnDecoder.fromProtoObj(
|
||||||
|
EMPTY_PUBSUB_TOPIC,
|
||||||
|
protoResult!
|
||||||
|
))!;
|
||||||
|
|
||||||
|
expect(msg.rateLimitProof).to.not.be.undefined;
|
||||||
|
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
|
||||||
|
expect(msg.verifyNoRoot()).to.be.true;
|
||||||
|
expect(msg.epoch).to.not.be.undefined;
|
||||||
|
expect(msg.epoch).to.be.gt(0);
|
||||||
|
|
||||||
|
expect(msg.contentTopic).to.eq(TestContentTopic);
|
||||||
|
expect(msg.msg.version).to.eq(1);
|
||||||
|
expect(msg.payload).to.deep.eq(payload);
|
||||||
|
expect(msg.timestamp).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Symmetric, toProtoObj", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
||||||
|
const index = 0;
|
||||||
|
const payload = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
|
||||||
|
const symKey = generateSymmetricKey();
|
||||||
|
|
||||||
|
const rlnEncoder = new RLNEncoder(
|
||||||
|
createSymEncoder({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
symKey
|
||||||
|
}),
|
||||||
|
rlnInstance,
|
||||||
|
index,
|
||||||
|
credential
|
||||||
|
);
|
||||||
|
const rlnDecoder = new RLNDecoder(
|
||||||
|
rlnInstance,
|
||||||
|
createSymDecoder(TestContentTopic, symKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
const proto = await rlnEncoder.toProtoObj({ payload });
|
||||||
|
|
||||||
|
expect(proto).to.not.be.undefined;
|
||||||
|
const msg = (await rlnDecoder.fromProtoObj(
|
||||||
|
EMPTY_PUBSUB_TOPIC,
|
||||||
|
proto!
|
||||||
|
)) as RlnMessage<DecodedMessage>;
|
||||||
|
|
||||||
|
expect(msg).to.not.be.undefined;
|
||||||
|
expect(msg.rateLimitProof).to.not.be.undefined;
|
||||||
|
|
||||||
|
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
|
||||||
|
expect(msg.verifyNoRoot()).to.be.true;
|
||||||
|
expect(msg.epoch).to.not.be.undefined;
|
||||||
|
expect(msg.epoch).to.be.gt(0);
|
||||||
|
|
||||||
|
expect(msg.contentTopic).to.eq(TestContentTopic);
|
||||||
|
expect(msg.msg.version).to.eq(1);
|
||||||
|
expect(msg.payload).to.deep.eq(payload);
|
||||||
|
expect(msg.timestamp).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Asymmetric, toWire", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
||||||
|
const index = 0;
|
||||||
|
const payload = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
|
||||||
|
const privateKey = generatePrivateKey();
|
||||||
|
const publicKey = getPublicKey(privateKey);
|
||||||
|
|
||||||
|
const rlnEncoder = new RLNEncoder(
|
||||||
|
createAsymEncoder({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
publicKey
|
||||||
|
}),
|
||||||
|
rlnInstance,
|
||||||
|
index,
|
||||||
|
credential
|
||||||
|
);
|
||||||
|
const rlnDecoder = new RLNDecoder(
|
||||||
|
rlnInstance,
|
||||||
|
createAsymDecoder(TestContentTopic, privateKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
const bytes = await rlnEncoder.toWire({ payload });
|
||||||
|
|
||||||
|
expect(bytes).to.not.be.undefined;
|
||||||
|
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
|
||||||
|
|
||||||
|
expect(protoResult).to.not.be.undefined;
|
||||||
|
const msg = (await rlnDecoder.fromProtoObj(
|
||||||
|
EMPTY_PUBSUB_TOPIC,
|
||||||
|
protoResult!
|
||||||
|
))!;
|
||||||
|
|
||||||
|
expect(msg.rateLimitProof).to.not.be.undefined;
|
||||||
|
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
|
||||||
|
expect(msg.verifyNoRoot()).to.be.true;
|
||||||
|
expect(msg.epoch).to.not.be.undefined;
|
||||||
|
expect(msg.epoch).to.be.gt(0);
|
||||||
|
|
||||||
|
expect(msg.contentTopic).to.eq(TestContentTopic);
|
||||||
|
expect(msg.msg.version).to.eq(1);
|
||||||
|
expect(msg.payload).to.deep.eq(payload);
|
||||||
|
expect(msg.timestamp).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Asymmetric, toProtoObj", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
||||||
|
const index = 0;
|
||||||
|
const payload = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
|
||||||
|
const privateKey = generatePrivateKey();
|
||||||
|
const publicKey = getPublicKey(privateKey);
|
||||||
|
|
||||||
|
const rlnEncoder = new RLNEncoder(
|
||||||
|
createAsymEncoder({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
publicKey
|
||||||
|
}),
|
||||||
|
rlnInstance,
|
||||||
|
index,
|
||||||
|
credential
|
||||||
|
);
|
||||||
|
const rlnDecoder = new RLNDecoder(
|
||||||
|
rlnInstance,
|
||||||
|
createAsymDecoder(TestContentTopic, privateKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
const proto = await rlnEncoder.toProtoObj({ payload });
|
||||||
|
|
||||||
|
expect(proto).to.not.be.undefined;
|
||||||
|
const msg = (await rlnDecoder.fromProtoObj(
|
||||||
|
EMPTY_PUBSUB_TOPIC,
|
||||||
|
proto!
|
||||||
|
)) as RlnMessage<DecodedMessage>;
|
||||||
|
|
||||||
|
expect(msg).to.not.be.undefined;
|
||||||
|
expect(msg.rateLimitProof).to.not.be.undefined;
|
||||||
|
|
||||||
|
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
|
||||||
|
expect(msg.verifyNoRoot()).to.be.true;
|
||||||
|
expect(msg.epoch).to.not.be.undefined;
|
||||||
|
expect(msg.epoch).to.be.gt(0);
|
||||||
|
|
||||||
|
expect(msg.contentTopic).to.eq(TestContentTopic);
|
||||||
|
expect(msg.msg.version).to.eq(1);
|
||||||
|
expect(msg.payload).to.deep.eq(payload);
|
||||||
|
expect(msg.timestamp).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("RLN Codec - epoch", () => {
|
||||||
|
it("toProtoObj", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
||||||
|
const index = 0;
|
||||||
|
const payload = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
|
||||||
|
const rlnEncoder = new RLNEncoder(
|
||||||
|
createEncoder({ contentTopic: TestContentTopic }),
|
||||||
|
rlnInstance,
|
||||||
|
index,
|
||||||
|
credential
|
||||||
|
);
|
||||||
|
const rlnDecoder = new RLNDecoder(
|
||||||
|
rlnInstance,
|
||||||
|
createDecoder(TestContentTopic)
|
||||||
|
);
|
||||||
|
|
||||||
|
const proto = await rlnEncoder.toProtoObj({ payload });
|
||||||
|
|
||||||
|
expect(proto).to.not.be.undefined;
|
||||||
|
const msg = (await rlnDecoder.fromProtoObj(
|
||||||
|
EMPTY_PUBSUB_TOPIC,
|
||||||
|
proto!
|
||||||
|
)) as RlnMessage<DecodedMessage>;
|
||||||
|
|
||||||
|
const epochBytes = proto!.rateLimitProof!.epoch;
|
||||||
|
const epoch = epochBytesToInt(epochBytes);
|
||||||
|
|
||||||
|
expect(msg).to.not.be.undefined;
|
||||||
|
expect(msg.rateLimitProof).to.not.be.undefined;
|
||||||
|
|
||||||
|
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
|
||||||
|
expect(msg.verifyNoRoot()).to.be.true;
|
||||||
|
expect(msg.epoch).to.not.be.undefined;
|
||||||
|
expect(msg.epoch!.toString(10).length).to.eq(9);
|
||||||
|
expect(msg.epoch).to.eq(epoch);
|
||||||
|
|
||||||
|
expect(msg.contentTopic).to.eq(TestContentTopic);
|
||||||
|
expect(msg.msg.version).to.eq(0);
|
||||||
|
expect(msg.payload).to.deep.eq(payload);
|
||||||
|
expect(msg.timestamp).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("RLN codec with version 0 and meta setter", () => {
|
||||||
|
// Encode the length of the payload
|
||||||
|
// Not a relevant real life example
|
||||||
|
const metaSetter = (msg: IProtoMessage & { meta: undefined }): Uint8Array => {
|
||||||
|
const buffer = new ArrayBuffer(4);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
view.setUint32(0, msg.payload.length, false);
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("toWire", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
||||||
|
const index = 0;
|
||||||
|
const payload = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
|
||||||
|
const rlnEncoder = createRLNEncoder({
|
||||||
|
encoder: createEncoder({ contentTopic: TestContentTopic, metaSetter }),
|
||||||
|
rlnInstance,
|
||||||
|
index,
|
||||||
|
credential
|
||||||
|
});
|
||||||
|
const rlnDecoder = createRLNDecoder({
|
||||||
|
rlnInstance,
|
||||||
|
decoder: createDecoder(TestContentTopic)
|
||||||
|
});
|
||||||
|
|
||||||
|
const bytes = await rlnEncoder.toWire({ payload });
|
||||||
|
|
||||||
|
expect(bytes).to.not.be.undefined;
|
||||||
|
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
|
||||||
|
expect(protoResult).to.not.be.undefined;
|
||||||
|
const msg = (await rlnDecoder.fromProtoObj(
|
||||||
|
EMPTY_PUBSUB_TOPIC,
|
||||||
|
protoResult!
|
||||||
|
))!;
|
||||||
|
|
||||||
|
const expectedMeta = metaSetter({
|
||||||
|
...EMPTY_PROTO_MESSAGE,
|
||||||
|
payload: protoResult!.payload
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(msg!.meta).to.deep.eq(expectedMeta);
|
||||||
|
|
||||||
|
expect(msg.rateLimitProof).to.not.be.undefined;
|
||||||
|
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
|
||||||
|
expect(msg.verifyNoRoot()).to.be.true;
|
||||||
|
expect(msg.epoch).to.not.be.undefined;
|
||||||
|
expect(msg.epoch).to.be.gt(0);
|
||||||
|
|
||||||
|
expect(msg.contentTopic).to.eq(TestContentTopic);
|
||||||
|
expect(msg.msg.version).to.eq(0);
|
||||||
|
expect(msg.payload).to.deep.eq(payload);
|
||||||
|
expect(msg.timestamp).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toProtoObj", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
||||||
|
const index = 0;
|
||||||
|
const payload = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
|
||||||
|
const rlnEncoder = new RLNEncoder(
|
||||||
|
createEncoder({ contentTopic: TestContentTopic, metaSetter }),
|
||||||
|
rlnInstance,
|
||||||
|
index,
|
||||||
|
credential
|
||||||
|
);
|
||||||
|
const rlnDecoder = new RLNDecoder(
|
||||||
|
rlnInstance,
|
||||||
|
createDecoder(TestContentTopic)
|
||||||
|
);
|
||||||
|
|
||||||
|
const proto = await rlnEncoder.toProtoObj({ payload });
|
||||||
|
|
||||||
|
expect(proto).to.not.be.undefined;
|
||||||
|
const msg = (await rlnDecoder.fromProtoObj(
|
||||||
|
EMPTY_PUBSUB_TOPIC,
|
||||||
|
proto!
|
||||||
|
)) as RlnMessage<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;
|
||||||
|
});
|
||||||
|
});
|
||||||
134
packages/rln/src/codec.ts
Normal file
134
packages/rln/src/codec.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import type {
|
||||||
|
IDecodedMessage,
|
||||||
|
IDecoder,
|
||||||
|
IEncoder,
|
||||||
|
IMessage,
|
||||||
|
IProtoMessage,
|
||||||
|
IRateLimitProof
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { Logger } from "@waku/utils";
|
||||||
|
|
||||||
|
import type { IdentityCredential } from "./identity.js";
|
||||||
|
import { RlnMessage, toRLNSignal } from "./message.js";
|
||||||
|
import { RLNInstance } from "./rln.js";
|
||||||
|
|
||||||
|
const log = new Logger("waku:rln:encoder");
|
||||||
|
|
||||||
|
export class RLNEncoder implements IEncoder {
|
||||||
|
private readonly idSecretHash: Uint8Array;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly encoder: IEncoder,
|
||||||
|
private readonly rlnInstance: RLNInstance,
|
||||||
|
private readonly index: number,
|
||||||
|
identityCredential: IdentityCredential
|
||||||
|
) {
|
||||||
|
if (index < 0) throw new Error("Invalid membership index");
|
||||||
|
this.idSecretHash = identityCredential.IDSecretHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async toWire(message: IMessage): Promise<Uint8Array | undefined> {
|
||||||
|
message.rateLimitProof = await this.generateProof(message);
|
||||||
|
log.info("Proof generated", message.rateLimitProof);
|
||||||
|
return this.encoder.toWire(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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.info("Proof generated", protoMessage.rateLimitProof);
|
||||||
|
return protoMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.idSecretHash
|
||||||
|
);
|
||||||
|
return proof;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get pubsubTopic(): string {
|
||||||
|
return this.encoder.pubsubTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get contentTopic(): string {
|
||||||
|
return this.encoder.contentTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get ephemeral(): boolean {
|
||||||
|
return this.encoder.ephemeral;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RLNEncoderOptions = {
|
||||||
|
encoder: IEncoder;
|
||||||
|
rlnInstance: RLNInstance;
|
||||||
|
index: number;
|
||||||
|
credential: IdentityCredential;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => {
|
||||||
|
return new RLNEncoder(
|
||||||
|
options.encoder,
|
||||||
|
options.rlnInstance,
|
||||||
|
options.index,
|
||||||
|
options.credential
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RLNDecoder<T extends IDecodedMessage>
|
||||||
|
implements IDecoder<RlnMessage<T>>
|
||||||
|
{
|
||||||
|
public constructor(
|
||||||
|
private readonly rlnInstance: RLNInstance,
|
||||||
|
private readonly decoder: IDecoder<T>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public get pubsubTopic(): string {
|
||||||
|
return this.decoder.pubsubTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get contentTopic(): string {
|
||||||
|
return this.decoder.contentTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public fromWireToProtoObj(
|
||||||
|
bytes: Uint8Array
|
||||||
|
): Promise<IProtoMessage | undefined> {
|
||||||
|
const protoMessage = this.decoder.fromWireToProtoObj(bytes);
|
||||||
|
log.info("Message decoded", protoMessage);
|
||||||
|
return Promise.resolve(protoMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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
packages/rln/src/contract/constants.ts
Normal file
68
packages/rln/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
packages/rln/src/contract/index.ts
Normal file
2
packages/rln/src/contract/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { RLNContract } from "./rln_contract.js";
|
||||||
|
export * from "./constants.js";
|
||||||
82
packages/rln/src/contract/rln_contract.spec.ts
Normal file
82
packages/rln/src/contract/rln_contract.spec.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { expect } from "chai";
|
||||||
|
import * as ethers from "ethers";
|
||||||
|
|
||||||
|
import { createRLN } from "../create.js";
|
||||||
|
|
||||||
|
import { SEPOLIA_CONTRACT } from "./constants.js";
|
||||||
|
import { RLNContract } from "./rln_contract.js";
|
||||||
|
|
||||||
|
describe("RLN Contract abstraction", () => {
|
||||||
|
it("should be able to fetch members from events and store to rln instance", async () => {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
let insertMemberCalled = false;
|
||||||
|
|
||||||
|
// Track if insertMember was called
|
||||||
|
const originalInsertMember = rlnInstance.zerokit.insertMember;
|
||||||
|
rlnInstance.zerokit.insertMember = function (
|
||||||
|
this: any,
|
||||||
|
...args: Parameters<typeof originalInsertMember>
|
||||||
|
) {
|
||||||
|
insertMemberCalled = true;
|
||||||
|
return originalInsertMember.apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
const voidSigner = new ethers.VoidSigner(SEPOLIA_CONTRACT.address);
|
||||||
|
const rlnContract = new RLNContract(rlnInstance, {
|
||||||
|
registryAddress: SEPOLIA_CONTRACT.address,
|
||||||
|
signer: voidSigner
|
||||||
|
});
|
||||||
|
|
||||||
|
rlnContract["storageContract"] = {
|
||||||
|
queryFilter: () => Promise.resolve([mockEvent()])
|
||||||
|
} as unknown as ethers.Contract;
|
||||||
|
rlnContract["_membersFilter"] = {
|
||||||
|
address: "",
|
||||||
|
topics: []
|
||||||
|
} as unknown as ethers.EventFilter;
|
||||||
|
|
||||||
|
await rlnContract.fetchMembers(rlnInstance);
|
||||||
|
|
||||||
|
expect(insertMemberCalled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should register a member", async () => {
|
||||||
|
const mockSignature =
|
||||||
|
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c";
|
||||||
|
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const voidSigner = new ethers.VoidSigner(SEPOLIA_CONTRACT.address);
|
||||||
|
const rlnContract = new RLNContract(rlnInstance, {
|
||||||
|
registryAddress: SEPOLIA_CONTRACT.address,
|
||||||
|
signer: voidSigner
|
||||||
|
});
|
||||||
|
|
||||||
|
let registerCalled = false;
|
||||||
|
rlnContract["storageIndex"] = 1;
|
||||||
|
rlnContract["_membersFilter"] = {
|
||||||
|
address: "",
|
||||||
|
topics: []
|
||||||
|
} as unknown as ethers.EventFilter;
|
||||||
|
rlnContract["registryContract"] = {
|
||||||
|
"register(uint16,uint256)": () => {
|
||||||
|
registerCalled = true;
|
||||||
|
return Promise.resolve({ wait: () => Promise.resolve(undefined) });
|
||||||
|
}
|
||||||
|
} as unknown as ethers.Contract;
|
||||||
|
|
||||||
|
const identity =
|
||||||
|
rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature);
|
||||||
|
await rlnContract.registerWithIdentity(identity);
|
||||||
|
|
||||||
|
expect(registerCalled).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockEvent(): ethers.Event {
|
||||||
|
return {
|
||||||
|
args: {
|
||||||
|
idCommitment: { _hex: "0xb3df1c4e5600ef2b" },
|
||||||
|
index: ethers.BigNumber.from(1)
|
||||||
|
}
|
||||||
|
} as unknown as ethers.Event;
|
||||||
|
}
|
||||||
353
packages/rln/src/contract/rln_contract.ts
Normal file
353
packages/rln/src/contract/rln_contract.ts
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
import { Logger } from "@waku/utils";
|
||||||
|
import { hexToBytes } from "@waku/utils/bytes";
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
|
||||||
|
import type { IdentityCredential } from "../identity.js";
|
||||||
|
import type { DecryptedCredentials } from "../keystore/index.js";
|
||||||
|
import type { RLNInstance } from "../rln.js";
|
||||||
|
import { MerkleRootTracker } from "../root_tracker.js";
|
||||||
|
import { zeroPadLE } from "../utils/index.js";
|
||||||
|
|
||||||
|
import { RLN_REGISTRY_ABI, RLN_STORAGE_ABI } from "./constants.js";
|
||||||
|
|
||||||
|
const log = new Logger("waku:rln:contract");
|
||||||
|
|
||||||
|
type Member = {
|
||||||
|
idCommitment: string;
|
||||||
|
index: ethers.BigNumber;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Signer = ethers.Signer;
|
||||||
|
|
||||||
|
type RLNContractOptions = {
|
||||||
|
signer: Signer;
|
||||||
|
registryAddress: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RLNStorageOptions = {
|
||||||
|
storageIndex?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RLNContractInitOptions = RLNContractOptions & RLNStorageOptions;
|
||||||
|
|
||||||
|
type FetchMembersOptions = {
|
||||||
|
fromBlock?: number;
|
||||||
|
fetchRange?: number;
|
||||||
|
fetchChunks?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RLNContract {
|
||||||
|
private registryContract: ethers.Contract;
|
||||||
|
private merkleRootTracker: MerkleRootTracker;
|
||||||
|
|
||||||
|
private deployBlock: undefined | number;
|
||||||
|
private storageIndex: undefined | number;
|
||||||
|
private storageContract: undefined | ethers.Contract;
|
||||||
|
private _membersFilter: undefined | ethers.EventFilter;
|
||||||
|
|
||||||
|
private _members: Map<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
rlnInstance: RLNInstance,
|
||||||
|
{ registryAddress, signer }: RLNContractOptions
|
||||||
|
) {
|
||||||
|
const initialRoot = rlnInstance.zerokit.getMerkleRoot();
|
||||||
|
|
||||||
|
this.registryContract = new ethers.Contract(
|
||||||
|
registryAddress,
|
||||||
|
RLN_REGISTRY_ABI,
|
||||||
|
signer
|
||||||
|
);
|
||||||
|
this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initStorageContract(
|
||||||
|
signer: Signer,
|
||||||
|
options: RLNStorageOptions = {}
|
||||||
|
): Promise<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.info(`Ignoring an error during query: ${err?.message}`);
|
||||||
|
return defaultValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
137
packages/rln/src/create.spec.ts
Normal file
137
packages/rln/src/create.spec.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { assert, expect } from "chai";
|
||||||
|
|
||||||
|
import { createRLN } from "./create.js";
|
||||||
|
|
||||||
|
describe("js-rln", () => {
|
||||||
|
it("should verify a proof", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
|
||||||
|
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
||||||
|
|
||||||
|
//peer's index in the Merkle Tree
|
||||||
|
const index = 5;
|
||||||
|
|
||||||
|
// Create a Merkle tree with random members
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
if (i == index) {
|
||||||
|
// insert the current peer's pk
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
} else {
|
||||||
|
// create a new key pair
|
||||||
|
rlnInstance.zerokit.insertMember(
|
||||||
|
rlnInstance.zerokit.generateIdentityCredentials().IDCommitment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare the message
|
||||||
|
const uint8Msg = Uint8Array.from(
|
||||||
|
"Hello World".split("").map((x) => x.charCodeAt(0))
|
||||||
|
);
|
||||||
|
|
||||||
|
// setting up the epoch
|
||||||
|
const epoch = new Date();
|
||||||
|
|
||||||
|
// generating proof
|
||||||
|
const proof = await rlnInstance.zerokit.generateRLNProof(
|
||||||
|
uint8Msg,
|
||||||
|
index,
|
||||||
|
epoch,
|
||||||
|
credential.IDSecretHash
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// verify the proof
|
||||||
|
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
|
||||||
|
expect(verifResult).to.be.true;
|
||||||
|
} catch (err) {
|
||||||
|
assert.fail(0, 1, "should not have failed proof verification");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Modifying the signal so it's invalid
|
||||||
|
uint8Msg[4] = 4;
|
||||||
|
// verify the proof
|
||||||
|
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
|
||||||
|
expect(verifResult).to.be.false;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("should verify a proof with a seeded membership key generation", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const seed = "This is a test seed";
|
||||||
|
const credential =
|
||||||
|
rlnInstance.zerokit.generateSeededIdentityCredential(seed);
|
||||||
|
|
||||||
|
//peer's index in the Merkle Tree
|
||||||
|
const index = 5;
|
||||||
|
|
||||||
|
// Create a Merkle tree with random members
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
if (i == index) {
|
||||||
|
// insert the current peer's pk
|
||||||
|
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
||||||
|
} else {
|
||||||
|
// create a new key pair
|
||||||
|
rlnInstance.zerokit.insertMember(
|
||||||
|
rlnInstance.zerokit.generateIdentityCredentials().IDCommitment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare the message
|
||||||
|
const uint8Msg = Uint8Array.from(
|
||||||
|
"Hello World".split("").map((x) => x.charCodeAt(0))
|
||||||
|
);
|
||||||
|
|
||||||
|
// setting up the epoch
|
||||||
|
const epoch = new Date();
|
||||||
|
|
||||||
|
// generating proof
|
||||||
|
const proof = await rlnInstance.zerokit.generateRLNProof(
|
||||||
|
uint8Msg,
|
||||||
|
index,
|
||||||
|
epoch,
|
||||||
|
credential.IDSecretHash
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// verify the proof
|
||||||
|
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
|
||||||
|
expect(verifResult).to.be.true;
|
||||||
|
} catch (err) {
|
||||||
|
assert.fail(0, 1, "should not have failed proof verification");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Modifying the signal so it's invalid
|
||||||
|
uint8Msg[4] = 4;
|
||||||
|
// verify the proof
|
||||||
|
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
|
||||||
|
expect(verifResult).to.be.false;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate the same membership key if the same seed is provided", async function () {
|
||||||
|
const rlnInstance = await createRLN();
|
||||||
|
const seed = "This is a test seed";
|
||||||
|
const memKeys1 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
|
||||||
|
const memKeys2 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
|
||||||
|
|
||||||
|
memKeys1.IDCommitment.forEach((element, index) => {
|
||||||
|
expect(element).to.equal(memKeys2.IDCommitment[index]);
|
||||||
|
});
|
||||||
|
memKeys1.IDNullifier.forEach((element, index) => {
|
||||||
|
expect(element).to.equal(memKeys2.IDNullifier[index]);
|
||||||
|
});
|
||||||
|
memKeys1.IDSecretHash.forEach((element, index) => {
|
||||||
|
expect(element).to.equal(memKeys2.IDSecretHash[index]);
|
||||||
|
});
|
||||||
|
memKeys1.IDTrapdoor.forEach((element, index) => {
|
||||||
|
expect(element).to.equal(memKeys2.IDTrapdoor[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
9
packages/rln/src/create.ts
Normal file
9
packages/rln/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();
|
||||||
|
}
|
||||||
31
packages/rln/src/identity.ts
Normal file
31
packages/rln/src/identity.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { buildBigIntFromUint8Array } from "./utils/index.js";
|
||||||
|
|
||||||
|
export class IdentityCredential {
|
||||||
|
public constructor(
|
||||||
|
public readonly IDTrapdoor: Uint8Array,
|
||||||
|
public readonly IDNullifier: Uint8Array,
|
||||||
|
public readonly IDSecretHash: Uint8Array,
|
||||||
|
public readonly IDCommitment: Uint8Array,
|
||||||
|
public readonly IDCommitmentBigInt: bigint
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static fromBytes(memKeys: Uint8Array): IdentityCredential {
|
||||||
|
if (memKeys.length < 128) {
|
||||||
|
throw new Error("Invalid memKeys length - must be at least 128 bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
const idTrapdoor = memKeys.subarray(0, 32);
|
||||||
|
const idNullifier = memKeys.subarray(32, 64);
|
||||||
|
const idSecretHash = memKeys.subarray(64, 96);
|
||||||
|
const idCommitment = memKeys.subarray(96, 128);
|
||||||
|
const idCommitmentBigInt = buildBigIntFromUint8Array(idCommitment, 32);
|
||||||
|
|
||||||
|
return new IdentityCredential(
|
||||||
|
idTrapdoor,
|
||||||
|
idNullifier,
|
||||||
|
idSecretHash,
|
||||||
|
idCommitment,
|
||||||
|
idCommitmentBigInt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
packages/rln/src/index.ts
Normal file
30
packages/rln/src/index.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { RLNDecoder, RLNEncoder } from "./codec.js";
|
||||||
|
import {
|
||||||
|
RLN_REGISTRY_ABI,
|
||||||
|
RLN_STORAGE_ABI,
|
||||||
|
SEPOLIA_CONTRACT
|
||||||
|
} from "./contract/index.js";
|
||||||
|
import { RLNContract } from "./contract/index.js";
|
||||||
|
import { createRLN } from "./create.js";
|
||||||
|
import { IdentityCredential } from "./identity.js";
|
||||||
|
import { Keystore } from "./keystore/index.js";
|
||||||
|
import { Proof } from "./proof.js";
|
||||||
|
import { RLNInstance } from "./rln.js";
|
||||||
|
import { MerkleRootTracker } from "./root_tracker.js";
|
||||||
|
import { extractMetaMaskSigner } from "./utils/index.js";
|
||||||
|
|
||||||
|
export {
|
||||||
|
createRLN,
|
||||||
|
Keystore,
|
||||||
|
RLNInstance,
|
||||||
|
IdentityCredential,
|
||||||
|
Proof,
|
||||||
|
RLNEncoder,
|
||||||
|
RLNDecoder,
|
||||||
|
MerkleRootTracker,
|
||||||
|
RLNContract,
|
||||||
|
RLN_STORAGE_ABI,
|
||||||
|
RLN_REGISTRY_ABI,
|
||||||
|
SEPOLIA_CONTRACT,
|
||||||
|
extractMetaMaskSigner
|
||||||
|
};
|
||||||
54
packages/rln/src/keystore/cipher.ts
Normal file
54
packages/rln/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";
|
||||||
|
import { kdf } from "@chainsafe/bls-keystore/lib/kdf";
|
||||||
|
import { normalizePassword } from "@chainsafe/bls-keystore/lib/password";
|
||||||
|
import { keccak256 } from "ethereum-cryptography/keccak";
|
||||||
|
import {
|
||||||
|
bytesToHex,
|
||||||
|
concatBytes,
|
||||||
|
hexToBytes
|
||||||
|
} from "ethereum-cryptography/utils";
|
||||||
|
|
||||||
|
import type { Keccak256Hash, Password } from "./types.js";
|
||||||
|
|
||||||
|
// eipKeystore supports only sha256 checksum so we just make an assumption it is keccak256
|
||||||
|
const validateChecksum = async (
|
||||||
|
password: Password,
|
||||||
|
eipKeystore: IEipKeystore
|
||||||
|
): Promise<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);
|
||||||
|
};
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
/* eslint eslint-comments/no-unlimited-disable: "off" */
|
||||||
|
// This file was generated by /scripts/schema-validation-codegen.ts
|
||||||
|
// Do not modify this file by hand.
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
"use strict";export const Credential = validate11;const schema12 = {"type":"object","properties":{"crypto":{"type":"object","properties":{"cipher":{"type":"string"},"cipherparams":{"type":"object"},"ciphertext":{"type":"string"},"kdf":{"type":"string"},"kdfparams":{"type":"object"},"mac":{"type":"string"}},"required":["cipher","cipherparams","ciphertext","kdf","kdfparams","mac"]}},"required":["crypto"]};function validate11(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((data.crypto === undefined) && (missing0 = "crypto")){validate11.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {if(data.crypto !== undefined){let data0 = data.crypto;const _errs1 = errors;if(errors === _errs1){if(data0 && typeof data0 == "object" && !Array.isArray(data0)){let missing1;if(((((((data0.cipher === undefined) && (missing1 = "cipher")) || ((data0.cipherparams === undefined) && (missing1 = "cipherparams"))) || ((data0.ciphertext === undefined) && (missing1 = "ciphertext"))) || ((data0.kdf === undefined) && (missing1 = "kdf"))) || ((data0.kdfparams === undefined) && (missing1 = "kdfparams"))) || ((data0.mac === undefined) && (missing1 = "mac"))){validate11.errors = [{instancePath:instancePath+"/crypto",schemaPath:"#/properties/crypto/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"}];return false;}else {if(data0.cipher !== undefined){const _errs3 = errors;if(typeof data0.cipher !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/cipher",schemaPath:"#/properties/crypto/properties/cipher/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs3 === errors;}else {var valid1 = true;}if(valid1){if(data0.cipherparams !== undefined){let data2 = data0.cipherparams;const _errs5 = errors;if(!(data2 && typeof data2 == "object" && !Array.isArray(data2))){validate11.errors = [{instancePath:instancePath+"/crypto/cipherparams",schemaPath:"#/properties/crypto/properties/cipherparams/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid1 = _errs5 === errors;}else {var valid1 = true;}if(valid1){if(data0.ciphertext !== undefined){const _errs7 = errors;if(typeof data0.ciphertext !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/ciphertext",schemaPath:"#/properties/crypto/properties/ciphertext/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs7 === errors;}else {var valid1 = true;}if(valid1){if(data0.kdf !== undefined){const _errs9 = errors;if(typeof data0.kdf !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/kdf",schemaPath:"#/properties/crypto/properties/kdf/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs9 === errors;}else {var valid1 = true;}if(valid1){if(data0.kdfparams !== undefined){let data5 = data0.kdfparams;const _errs11 = errors;if(!(data5 && typeof data5 == "object" && !Array.isArray(data5))){validate11.errors = [{instancePath:instancePath+"/crypto/kdfparams",schemaPath:"#/properties/crypto/properties/kdfparams/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid1 = _errs11 === errors;}else {var valid1 = true;}if(valid1){if(data0.mac !== undefined){const _errs13 = errors;if(typeof data0.mac !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/mac",schemaPath:"#/properties/crypto/properties/mac/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs13 === errors;}else {var valid1 = true;}}}}}}}}else {validate11.errors = [{instancePath:instancePath+"/crypto",schemaPath:"#/properties/crypto/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}}}}else {validate11.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate11.errors = vErrors;return errors === 0;}
|
||||||
5
packages/rln/src/keystore/index.ts
Normal file
5
packages/rln/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 };
|
||||||
313
packages/rln/src/keystore/keystore.spec.ts
Normal file
313
packages/rln/src/keystore/keystore.spec.ts
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
import * as chai from "chai";
|
||||||
|
import chaiAsPromised from "chai-as-promised";
|
||||||
|
import chaiSubset from "chai-subset";
|
||||||
|
import deepEqualInAnyOrder from "deep-equal-in-any-order";
|
||||||
|
|
||||||
|
const { expect } = chai;
|
||||||
|
|
||||||
|
chai.use(chaiSubset);
|
||||||
|
chai.use(deepEqualInAnyOrder);
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
|
||||||
|
import { IdentityCredential } from "../identity.js";
|
||||||
|
import { buildBigIntFromUint8Array } from "../utils/bytes.js";
|
||||||
|
|
||||||
|
import { Keystore } from "./keystore.js";
|
||||||
|
import type { MembershipInfo } from "./types.js";
|
||||||
|
|
||||||
|
const DEFAULT_PASSWORD = "sup3rsecure";
|
||||||
|
const NWAKU_KEYSTORE = {
|
||||||
|
application: "waku-rln-relay",
|
||||||
|
appIdentifier: "01234567890abcdef",
|
||||||
|
version: "0.2",
|
||||||
|
credentials: {
|
||||||
|
"9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548": {
|
||||||
|
crypto: {
|
||||||
|
cipher: "aes-128-ctr",
|
||||||
|
cipherparams: {
|
||||||
|
iv: "fd6b39eb71d44c59f6bf5ff3d8945c80"
|
||||||
|
},
|
||||||
|
ciphertext:
|
||||||
|
"9c72f47ce95de03ed34502d0288e7576b66b51b9e7d5ae882c27bd89f94e6a03c2c44c2ddf0c982e72003d67212105f1b64614f57cabb0ceadab7e07be165eee1121ad6b81951368a9f3be2dd99ea294515f6013d5f2bd4702a40e36cfde2ea298b23b31e5ce719d8040c3331f73d6bf44f88bca39bac0e917d8bf545500e4f40d321c235426a80f315ac70666acbd3bdf803fbc1e7e7103fed466525ed332b25d72b2dbedf6fa383b2305987c1fe276b029570519b3e79930edf08c1029868d05c2c08ab61d7c64f63c054b4f6a5a12d43cdc79751b6fe58d3ed26b69443eb7c9f7efce27912340129c91b6b813ac94efd5776a40b1dda896d61357de208c7c47a14af911cc231355c8093ee6626e89c07e1037f9e0b22c690e3e049014399ca0212c509cb04c71c7860d1b17a0c47711c490c27bad2825926148a1f15a507f36ba2cdaa04897fce2914e53caed0beaf1bebd2a83af76511cc15bff2165ff0860ad6eca1f30022d7739b2a6b6a72f2feeef0f5941183cda015b4631469e1f4cf27003cab9a90920301cb30d95e4554686922dc5a05c13dfb575cdf113c700d607896011970e6ee7d6edb61210ab28ac8f0c84c606c097e3e300f0a5f5341edfd15432bef6225a498726b62a98283829ad51023b2987f30686cfb4ea3951f3957654035ec291f9b0964a3a8665d81b16cec20fb40f944d5f9bf03ac1e444ad45bae3fa85e7465ce620c0966d8148d6e2856f676c4fbbe3ebe470453efb4bbda1866680037917e37765f680e3da96ef3991f3fe5cda80c523996c2234758bf5f7b6d052dc6942f5a92c8b8eec5d2d8940203bbb6b1cba7b7ebc1334334ca69cdb509a5ea58ec6b2ebaea52307589eaae9430eb15ad234c0c39c83accdf3b77e52a616e345209c5bc9b442f9f0fa96836d9342f983a7",
|
||||||
|
kdf: "pbkdf2",
|
||||||
|
kdfparams: {
|
||||||
|
dklen: 32,
|
||||||
|
c: 1000000,
|
||||||
|
prf: "hmac-sha256",
|
||||||
|
salt: "60f0aa92fbf63a8356dfdbed2ab18058"
|
||||||
|
},
|
||||||
|
mac: "51a227ac6db7f2797c63925880b3db664e034231a4c68daa919ab42d8df38bc6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"263335559F0578FD785F9CDFEDBB45CFF276799A27580B8F580CDFDCB990257C": {
|
||||||
|
crypto: {
|
||||||
|
cipher: "aes-128-ctr",
|
||||||
|
cipherparams: {
|
||||||
|
iv: "69f95461f811ac35a21987b1fdaa605e"
|
||||||
|
},
|
||||||
|
ciphertext:
|
||||||
|
"edfe844f8e2aedd62f26753e7247554920352b6b167f54ea4f728cd715577e9d2b7192b782471914870794205e77c2708b6db2d0ada19fec6b3533098cb2b7350bbaf81526d6bde7f1d0e83c366e3a2ddcced942cfb09a3c7704db7041132c3b511fed2f6d8599e6cddf649250b240687c2c335bf0aa75c892bc97f81c537898aefed20d1488e816d54eec72572acf36f140dc98cba0430cdeb8a00b8e8c8edf9b1292ca0e9c9a606acec51ea3dbe46438cb74b95d708cec18f8f126aecabbff11dd068d9194b25803f959f0bb62d49785dbc694486754f46bfe084cfa780cae27eca48cdcc88f4083d166d1747b8e2e637619e5d3848b9b6cdf7c7161eda8e476edfc083d417691d47b84fb224bfd26bf7713958893b934388e50783e49c5c84999971538ccda14c54b48b0d4aa37503e2a40212e9a1407d5a1ea4e96760de3d87e1b2287465a4e51cf330b7f1d14e3f2fb6521d10d32c798856464927b1e0286086a78f07a8f6f436d8c0c7b530f585320515e276d82c7b1f244702fa9ca6e6ad164fd2b1d9badcbdc17e01e95abf58e6825d8eeba5bc22db3a66dd41c64887d4c862298e921b3bae17d9fb7be1f619c60c82bd60dee351b77514d36e25d4092d6cde8ab613c40a117f7b784c80d65310e5b9cf1a31ba555f848e6984cc0c2d48315167d60131f3ffaaca5c81e359134bbfc81fa217f29b533868604ced4a2c5da8c89bd1238147b9f348168864ebea40c36a6abbf3d59d43086f26777104ce0a9f60cbf350058a337bc66abd5e4976950e5908192f98a9a8c1913abbc0d918479aeaa99e89a0e5cd65fd84a347d73df1d9c829863728a6fcd90150e52ecdec48bd07802110384f6c0aff0ca05ad42feb521223b58719fd4fc4ae88df8225ea58e303e4c61e8288e80f854bf0b",
|
||||||
|
kdf: "pbkdf2",
|
||||||
|
kdfparams: {
|
||||||
|
dklen: 32,
|
||||||
|
c: 1000000,
|
||||||
|
prf: "hmac-sha256",
|
||||||
|
salt: "3cf796e4857f296bef3bdb9ca844b1bf"
|
||||||
|
},
|
||||||
|
mac: "3d6cb0492afcf89c891365f097ae8989dc50038010c419b18228be6816c24c32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
F62A7FCE85E5B796AFCB38F54A44210515CB688EA0224E9A436CCA0A542F2C9D: {
|
||||||
|
crypto: {
|
||||||
|
cipher: "aes-128-ctr",
|
||||||
|
cipherparams: {
|
||||||
|
iv: "49816dacf881c85db9f11f7f068dcf71"
|
||||||
|
},
|
||||||
|
ciphertext:
|
||||||
|
"d7d805ee24dd34368e3d1829aa6d0856ca2ea54d9bcdc2655f8f197af0d293aff56c6e06de3137b0eddffbcad7cc0b8e3f6ba761ac7983d8e59ce04c8936868297b9f70238cb295e17567f2404b278b93c985496a6e1e46185965491449ccbc1e7155224acdba354ed18b1b9867ff6f1a833a77c9b21e2e9c4b6af27d5bd6303efd574465920928e5c467bd3c7888c3f31e8bece6af2e0c35fa03661399e9b420eeecd4376cb2b3266692f46c03161bb32cc2c79521f7b19cb0e6ec911213e105967f8887d94c73e793b18e4c14ee045dca13fcfb62ae267d3175f8a4fefd0e8bd636bd9431cc0cc7119e75f116a16dcbdcac1c15a3dcec57e1c49dbf5dccd1c75c0cfcb3473e81e8546048ce5231a4d4c8dd5d66311354e9ab70ad5745d5be27746954a08b0b29218562bfb632ae0a498cf09d7955a27377ed7a50fc1b4adaa0a3fb3e87a3b4d923136be0767a1428050944b9fd247332dea1b5016dfa1ec4da167e70e11e07cd58034b8470366dc16d77978b49a61e213ab5a7817fd69af26c2a8c3cd3a488d6e1491e0215071e1f3e9d49d0dfab3a7e324644c98a088e20259980495dcc379dcdce2e61752711bdf8abf057a2e696624078601245828193d838cc806065ee3f2bb138302ec72c70f34f14c0ab816211011f0ac55423732875e220175c717f6bc86f071bb4fab51c1963eb5c5d70d504c1e4d2307a8c8c4b8b5a84566a4606deb3fc6d7a420adc2b2b37c0ef3018f82a3ce0044e082407e8e7cb6214a3abc139b7f75b2c36c6902080e7696c730ab062e75e597274e0c945b6a7a366d20bd210dd02b097071142d033597e2fc4174be683a866510fa1c2fe150a2fb81dbd2b5da25da27f29367fb22dd4e9d4785856e4deea56219f9495fb3ab772f7867db11cb14026b",
|
||||||
|
kdf: "pbkdf2",
|
||||||
|
kdfparams: {
|
||||||
|
dklen: 32,
|
||||||
|
c: 1000000,
|
||||||
|
prf: "hmac-sha256",
|
||||||
|
salt: "2dcb5ba5c98fe5e46d961dad36e79a5b"
|
||||||
|
},
|
||||||
|
mac: "2d6e9de6440f52c5db64b13f80399967c8770e82616294e14f40a2e213e7d925"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"8479C6B9125D43E7B7739F1BAB41779F2F5A4D27FF0E2B6F6CA353032010A22C": {
|
||||||
|
crypto: {
|
||||||
|
cipher: "aes-128-ctr",
|
||||||
|
cipherparams: {
|
||||||
|
iv: "b0eef2c385a04909c4ae9b318e179fa7"
|
||||||
|
},
|
||||||
|
ciphertext:
|
||||||
|
"90b982222072366566fa194be5c170506888e184cadbd52aa38f184ac4e9bc160cc719d809fb6a128e0cbd908e70a71efdc5d51c4dab8aab71e3e6a2ebd9ea4238cb47585137990e896dfa53961bb2b328abfbba82f49db6a9b6e3790cf9e29c145796c6dbf409dc875e7998db827c944a835a29ab4192a11ad1efde5ebdd1a775ecbfefd139c50fbdcbebd6c124d9d65ba6ddaaa83e57695293e7c85dfd6f418d58fa5ffb9ab9b2395c84b57da796d31b6351fde3f1dbab29da6c3f259859bd0719c34f5111a9a12075b53ee91b4598fb2f452dbea823ec094cb757f370b5386a8e5db25cf732681d0cd9bda651ae55cdd125138fd2c8f1ffe87a5eea14df7d355762b37e3e71c33c6fe46a10c2083538910fec12e294de84ff587cab2dd268203699cb180e481f4a3a093b86854cea64341dd9482305abd4a9d7bb304b078bc255bf7cde78689225f17006f24c2cd82d38a59f1e0899965c38fcfd1ec67069143ee05a34922963a527549a002e3221e1461463f573e5f66ba87dcb83a63cb8e3a721c13cd9d4d0c9a0334a558f32027424a5bc9fc12b91981a3f74ac4b62eea3aae8be6c44504696b96afadce5d9222bb67dddf5a7d98dd43d544d79f8720a946c37eba8eb5ae6d70f4bdbbe554cbd4b3abb35ed357c8cb8f55e016ab83bef12bf5c0cdf26c7624c86f16437f545d796addb1aa7370de329930c68b174c871706e7afdf78cc07e0f0c58e45495d0d3bcf3faf9fb6d20369b0adc89766b0c9132677e52112770d017da7658f2a0c0eaeac57416f203700f98bf7b30119407733d4f0bd4322c622120cdf81646c4a1adfb80e757954e41ba0e7816c403b2e4b9ceb2d36e4198921ea719a410ae6f6983e49e7b99c266deb0465af716799e36a5bab70923291da808edeba54267e31e8b64c37123fd45d86e0638",
|
||||||
|
kdf: "pbkdf2",
|
||||||
|
kdfparams: {
|
||||||
|
dklen: 32,
|
||||||
|
c: 1000000,
|
||||||
|
prf: "hmac-sha256",
|
||||||
|
salt: "142a0a65b7f6f480546cc4ef743d7ef9"
|
||||||
|
},
|
||||||
|
mac: "7119b7b78598850de5f6af742e42748a3b005394b6b8b272490f24527ebd8b15"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Keystore", () => {
|
||||||
|
it("shoud create empty store with predefined values", () => {
|
||||||
|
const store = Keystore.create();
|
||||||
|
|
||||||
|
expect(store.toObject()).to.deep.eq({
|
||||||
|
application: "waku-rln-relay",
|
||||||
|
appIdentifier: "01234567890abcdef",
|
||||||
|
version: "0.2",
|
||||||
|
credentials: {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: add more thorow credentials testing
|
||||||
|
[
|
||||||
|
{
|
||||||
|
some: "3123",
|
||||||
|
dadw: "1212"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
application: 123,
|
||||||
|
version: "01234567890abcdef",
|
||||||
|
appIdentifier: "0.2",
|
||||||
|
credentials: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
application: "waku-rln-relay",
|
||||||
|
version: 213,
|
||||||
|
appIdentifier: "0.2",
|
||||||
|
credentials: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
application: "waku-rln-relay",
|
||||||
|
version: "01234567890abcdef",
|
||||||
|
appIdentifier: 12,
|
||||||
|
credentials: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
application: "waku-rln-relay",
|
||||||
|
version: "01234567890abcdef",
|
||||||
|
appIdentifier: "12",
|
||||||
|
credentials: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
application: "waku-rln-relay",
|
||||||
|
version: "01234567890abcdef",
|
||||||
|
appIdentifier: "12",
|
||||||
|
credentials: 123
|
||||||
|
},
|
||||||
|
{
|
||||||
|
application: "waku-rln-relay",
|
||||||
|
version: "01234567890abcdef",
|
||||||
|
appIdentifier: "12",
|
||||||
|
credentials: "123"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
application: "waku-rln-relay",
|
||||||
|
version: "01234567890abcdef",
|
||||||
|
appIdentifier: "12",
|
||||||
|
credentials: {
|
||||||
|
hash: {
|
||||||
|
invalid: "here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].map((options) => {
|
||||||
|
it("should fail to create store from invalid object", () => {
|
||||||
|
expect(() => Keystore.fromObject(options as any)).to.throw(
|
||||||
|
"Invalid object, does not match Nwaku Keystore format."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shoud create store from valid object", () => {
|
||||||
|
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
|
||||||
|
expect(store.toObject()).to.deep.eq(NWAKU_KEYSTORE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail to create store from invalid string", () => {
|
||||||
|
expect(Keystore.fromString("/asdq}")).to.eq(undefined);
|
||||||
|
expect(Keystore.fromString('{ "name": "it" }')).to.eq(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shoud create store from valid string", async () => {
|
||||||
|
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
|
||||||
|
expect(store.toObject()).to.deep.eq(NWAKU_KEYSTORE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert keystore to string", async () => {
|
||||||
|
let store = Keystore.create();
|
||||||
|
|
||||||
|
expect(store.toString()).to.eq(
|
||||||
|
JSON.stringify({
|
||||||
|
application: "waku-rln-relay",
|
||||||
|
appIdentifier: "01234567890abcdef",
|
||||||
|
version: "0.2",
|
||||||
|
credentials: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
store = Keystore.fromObject(NWAKU_KEYSTORE as any);
|
||||||
|
|
||||||
|
expect(store.toString()).to.eq(JSON.stringify(NWAKU_KEYSTORE));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shoud add / read new credentials", async () => {
|
||||||
|
const expectedHash =
|
||||||
|
"9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548";
|
||||||
|
const identity = {
|
||||||
|
IDTrapdoor: new Uint8Array([
|
||||||
|
211, 23, 66, 42, 179, 130, 131, 111, 201, 205, 244, 34, 27, 238, 244,
|
||||||
|
216, 131, 240, 188, 45, 193, 172, 4, 168, 225, 225, 43, 197, 114, 176,
|
||||||
|
126, 9
|
||||||
|
]),
|
||||||
|
IDNullifier: new Uint8Array([
|
||||||
|
238, 168, 239, 65, 73, 63, 105, 19, 132, 62, 213, 205, 191, 255, 209, 9,
|
||||||
|
178, 155, 239, 201, 131, 125, 233, 136, 246, 217, 9, 237, 55, 89, 81, 42
|
||||||
|
]),
|
||||||
|
IDSecretHash: new Uint8Array([
|
||||||
|
150, 54, 194, 28, 18, 216, 138, 253, 95, 139, 120, 109, 98, 129, 146,
|
||||||
|
101, 41, 194, 36, 36, 96, 152, 152, 89, 151, 160, 118, 15, 222, 124,
|
||||||
|
187, 4
|
||||||
|
]),
|
||||||
|
IDCommitment: new Uint8Array([
|
||||||
|
112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229,
|
||||||
|
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15
|
||||||
|
]),
|
||||||
|
IDCommitmentBigInt: buildBigIntFromUint8Array(
|
||||||
|
new Uint8Array([
|
||||||
|
112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229,
|
||||||
|
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171,
|
||||||
|
15
|
||||||
|
])
|
||||||
|
)
|
||||||
|
} as unknown as IdentityCredential;
|
||||||
|
const membership = {
|
||||||
|
chainId: "0xAA36A7",
|
||||||
|
treeIndex: 8,
|
||||||
|
address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71"
|
||||||
|
} as unknown as MembershipInfo;
|
||||||
|
|
||||||
|
const store = Keystore.create();
|
||||||
|
const hash = await store.addCredential(
|
||||||
|
{ identity, membership },
|
||||||
|
DEFAULT_PASSWORD
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(hash).to.eq(expectedHash);
|
||||||
|
|
||||||
|
const actualCredentials = await store.readCredential(
|
||||||
|
expectedHash,
|
||||||
|
DEFAULT_PASSWORD
|
||||||
|
);
|
||||||
|
expect(actualCredentials).to.deep.equalInAnyOrder({
|
||||||
|
identity,
|
||||||
|
membership
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shoud fail to add credentials if already exist", async () => {
|
||||||
|
const identity = {
|
||||||
|
IDTrapdoor: [
|
||||||
|
211, 23, 66, 42, 179, 130, 131, 111, 201, 205, 244, 34, 27, 238, 244,
|
||||||
|
216, 131, 240, 188, 45, 193, 172, 4, 168, 225, 225, 43, 197, 114, 176,
|
||||||
|
126, 9
|
||||||
|
],
|
||||||
|
IDNullifier: [
|
||||||
|
238, 168, 239, 65, 73, 63, 105, 19, 132, 62, 213, 205, 191, 255, 209, 9,
|
||||||
|
178, 155, 239, 201, 131, 125, 233, 136, 246, 217, 9, 237, 55, 89, 81, 42
|
||||||
|
],
|
||||||
|
IDSecretHash: [
|
||||||
|
150, 54, 194, 28, 18, 216, 138, 253, 95, 139, 120, 109, 98, 129, 146,
|
||||||
|
101, 41, 194, 36, 36, 96, 152, 152, 89, 151, 160, 118, 15, 222, 124,
|
||||||
|
187, 4
|
||||||
|
],
|
||||||
|
IDCommitment: [
|
||||||
|
112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229,
|
||||||
|
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15
|
||||||
|
]
|
||||||
|
} as unknown as IdentityCredential;
|
||||||
|
const membership = {
|
||||||
|
chainId: "0xAA36A7",
|
||||||
|
treeIndex: 8,
|
||||||
|
address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71"
|
||||||
|
} as unknown as MembershipInfo;
|
||||||
|
|
||||||
|
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.addCredential({ identity, membership }, DEFAULT_PASSWORD);
|
||||||
|
} catch (e) {
|
||||||
|
expect((e as Error).message).to.eq(
|
||||||
|
"Credential already exists in the store."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shoud fail to read credentials with wrong password", async () => {
|
||||||
|
const expectedHash =
|
||||||
|
"9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548";
|
||||||
|
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.readCredential(expectedHash, "wrong-password");
|
||||||
|
} catch (e) {
|
||||||
|
expect((e as Error).message).to.eq("Password is invalid.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shoud fail to read missing credentials", async () => {
|
||||||
|
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
|
||||||
|
|
||||||
|
const result = await store.readCredential("wrong-hash", "wrong-password");
|
||||||
|
expect(result).to.eq(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
330
packages/rln/src/keystore/keystore.ts
Normal file
330
packages/rln/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 { Logger } from "@waku/utils";
|
||||||
|
import { sha256 } from "ethereum-cryptography/sha256";
|
||||||
|
import {
|
||||||
|
bytesToHex,
|
||||||
|
bytesToUtf8,
|
||||||
|
utf8ToBytes
|
||||||
|
} from "ethereum-cryptography/utils";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { v4 as uuidV4 } from "uuid";
|
||||||
|
|
||||||
|
import { buildBigIntFromUint8Array } from "../utils/bytes.js";
|
||||||
|
|
||||||
|
import { decryptEipKeystore, keccak256Checksum } from "./cipher.js";
|
||||||
|
import { isCredentialValid, isKeystoreValid } from "./schema_validator.js";
|
||||||
|
import type {
|
||||||
|
Keccak256Hash,
|
||||||
|
KeystoreEntity,
|
||||||
|
MembershipHash,
|
||||||
|
MembershipInfo,
|
||||||
|
Password,
|
||||||
|
Sha256Hash
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
const log = new Logger("waku:rln:keystore");
|
||||||
|
|
||||||
|
type NwakuCredential = {
|
||||||
|
crypto: {
|
||||||
|
cipher: ICipherModule["function"];
|
||||||
|
cipherparams: ICipherModule["params"];
|
||||||
|
ciphertext: ICipherModule["message"];
|
||||||
|
kdf: IPbkdf2KdfModule["function"];
|
||||||
|
kdfparams: IPbkdf2KdfModule["params"];
|
||||||
|
mac: Sha256Hash;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// examples from nwaku
|
||||||
|
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/tests/test_waku_keystore.nim#L43
|
||||||
|
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/keystore.nim#L154C35-L154C38
|
||||||
|
// important: each credential has it's own password
|
||||||
|
// important: not compatible with https://eips.ethereum.org/EIPS/eip-2335
|
||||||
|
interface NwakuKeystore {
|
||||||
|
application: string;
|
||||||
|
version: string;
|
||||||
|
appIdentifier: string;
|
||||||
|
credentials: {
|
||||||
|
[key: MembershipHash]: NwakuCredential;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeystoreCreateOptions = {
|
||||||
|
application?: string;
|
||||||
|
version?: string;
|
||||||
|
appIdentifier?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Keystore {
|
||||||
|
private data: NwakuKeystore;
|
||||||
|
|
||||||
|
private constructor(options: KeystoreCreateOptions | NwakuKeystore) {
|
||||||
|
this.data = Object.assign(
|
||||||
|
{
|
||||||
|
application: "waku-rln-relay",
|
||||||
|
appIdentifier: "01234567890abcdef",
|
||||||
|
version: "0.2",
|
||||||
|
credentials: {}
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(options: KeystoreCreateOptions = {}): Keystore {
|
||||||
|
return new Keystore(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// should be valid JSON string that contains Keystore file
|
||||||
|
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/keyfile.nim#L376
|
||||||
|
public static fromString(str: string): undefined | Keystore {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(str);
|
||||||
|
|
||||||
|
if (!Keystore.isValidNwakuStore(obj)) {
|
||||||
|
throw Error("Invalid string, does not match Nwaku Keystore format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Keystore(obj);
|
||||||
|
} catch (err) {
|
||||||
|
log.error("Cannot create Keystore from string:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromObject(obj: NwakuKeystore): Keystore {
|
||||||
|
if (!Keystore.isValidNwakuStore(obj)) {
|
||||||
|
throw Error("Invalid object, does not match Nwaku Keystore format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Keystore(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addCredential(
|
||||||
|
options: KeystoreEntity,
|
||||||
|
password: Password
|
||||||
|
): Promise<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.error("Cannot parse bytes to Nwaku Credentials:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static fromArraylikeToBytes(obj: {
|
||||||
|
[key: number]: number;
|
||||||
|
}): Uint8Array {
|
||||||
|
const bytes = [];
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let lastElement = obj[index];
|
||||||
|
|
||||||
|
while (lastElement !== undefined) {
|
||||||
|
bytes.push(lastElement);
|
||||||
|
index += 1;
|
||||||
|
lastElement = obj[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// follows nwaku implementation
|
||||||
|
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L111
|
||||||
|
private static computeMembershipHash(info: MembershipInfo): MembershipHash {
|
||||||
|
return bytesToHex(
|
||||||
|
sha256(utf8ToBytes(`${info.chainId}${info.address}${info.treeIndex}`))
|
||||||
|
).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// follows nwaku implementation
|
||||||
|
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L98
|
||||||
|
private static fromIdentityToBytes(options: KeystoreEntity): Uint8Array {
|
||||||
|
return utf8ToBytes(
|
||||||
|
JSON.stringify({
|
||||||
|
treeIndex: options.membership.treeIndex,
|
||||||
|
identityCredential: {
|
||||||
|
idCommitment: options.identity.IDCommitment,
|
||||||
|
idNullifier: options.identity.IDNullifier,
|
||||||
|
idSecretHash: options.identity.IDSecretHash,
|
||||||
|
idTrapdoor: options.identity.IDTrapdoor
|
||||||
|
},
|
||||||
|
membershipContract: {
|
||||||
|
chainId: options.membership.chainId,
|
||||||
|
address: options.membership.address
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
/* eslint eslint-comments/no-unlimited-disable: "off" */
|
||||||
|
// This file was generated by /scripts/schema-validation-codegen.ts
|
||||||
|
// Do not modify this file by hand.
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
"use strict";export const Keystore = validate11;const schema12 = {"type":"object","properties":{"credentials":{"type":"object"},"appIdentifier":{"type":"string"},"version":{"type":"string"},"application":{"type":"string"}},"required":["application","appIdentifier","credentials","version"]};function validate11(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((((data.application === undefined) && (missing0 = "application")) || ((data.appIdentifier === undefined) && (missing0 = "appIdentifier"))) || ((data.credentials === undefined) && (missing0 = "credentials"))) || ((data.version === undefined) && (missing0 = "version"))){validate11.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {if(data.credentials !== undefined){let data0 = data.credentials;const _errs1 = errors;if(!(data0 && typeof data0 == "object" && !Array.isArray(data0))){validate11.errors = [{instancePath:instancePath+"/credentials",schemaPath:"#/properties/credentials/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid0 = _errs1 === errors;}else {var valid0 = true;}if(valid0){if(data.appIdentifier !== undefined){const _errs3 = errors;if(typeof data.appIdentifier !== "string"){validate11.errors = [{instancePath:instancePath+"/appIdentifier",schemaPath:"#/properties/appIdentifier/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.version !== undefined){const _errs5 = errors;if(typeof data.version !== "string"){validate11.errors = [{instancePath:instancePath+"/version",schemaPath:"#/properties/version/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.application !== undefined){const _errs7 = errors;if(typeof data.application !== "string"){validate11.errors = [{instancePath:instancePath+"/application",schemaPath:"#/properties/application/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs7 === errors;}else {var valid0 = true;}}}}}}else {validate11.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate11.errors = vErrors;return errors === 0;}
|
||||||
34
packages/rln/src/keystore/schema_validator.ts
Normal file
34
packages/rln/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
packages/rln/src/keystore/types.ts
Normal file
36
packages/rln/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;
|
||||||
|
};
|
||||||
70
packages/rln/src/message.ts
Normal file
70
packages/rln/src/message.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import type {
|
||||||
|
IDecodedMessage,
|
||||||
|
IMessage,
|
||||||
|
IRateLimitProof
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import * as utils from "@waku/utils/bytes";
|
||||||
|
|
||||||
|
import { RLNInstance } from "./rln.js";
|
||||||
|
import { epochBytesToInt } from "./utils/index.js";
|
||||||
|
|
||||||
|
export function toRLNSignal(contentTopic: string, msg: IMessage): Uint8Array {
|
||||||
|
const contentTopicBytes = utils.utf8ToBytes(contentTopic ?? "");
|
||||||
|
return new Uint8Array([...(msg.payload ?? []), ...contentTopicBytes]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RlnMessage<T extends IDecodedMessage> implements IDecodedMessage {
|
||||||
|
public pubsubTopic = "";
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public rlnInstance: RLNInstance,
|
||||||
|
public msg: T,
|
||||||
|
public rateLimitProof: IRateLimitProof | undefined
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public verify(roots: Uint8Array[]): boolean | undefined {
|
||||||
|
return this.rateLimitProof
|
||||||
|
? this.rlnInstance.zerokit.verifyWithRoots(
|
||||||
|
this.rateLimitProof,
|
||||||
|
toRLNSignal(this.msg.contentTopic, this.msg),
|
||||||
|
...roots
|
||||||
|
) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public verifyNoRoot(): boolean | undefined {
|
||||||
|
return this.rateLimitProof
|
||||||
|
? this.rlnInstance.zerokit.verifyWithNoRoot(
|
||||||
|
this.rateLimitProof,
|
||||||
|
toRLNSignal(this.msg.contentTopic, this.msg)
|
||||||
|
) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get payload(): Uint8Array {
|
||||||
|
return this.msg.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get contentTopic(): string {
|
||||||
|
return this.msg.contentTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get timestamp(): Date | undefined {
|
||||||
|
return this.msg.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get ephemeral(): boolean | undefined {
|
||||||
|
return this.msg.ephemeral;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get meta(): Uint8Array | undefined {
|
||||||
|
return this.msg.meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get epoch(): number | undefined {
|
||||||
|
const bytes = this.rateLimitProof?.epoch;
|
||||||
|
if (!bytes) return undefined;
|
||||||
|
|
||||||
|
return epochBytesToInt(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
packages/rln/src/proof.ts
Normal file
69
packages/rln/src/proof.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import type { IRateLimitProof } from "@waku/interfaces";
|
||||||
|
|
||||||
|
import { concatenate, poseidonHash } from "./utils/index.js";
|
||||||
|
|
||||||
|
const proofOffset = 128;
|
||||||
|
const rootOffset = proofOffset + 32;
|
||||||
|
const epochOffset = rootOffset + 32;
|
||||||
|
const shareXOffset = epochOffset + 32;
|
||||||
|
const shareYOffset = shareXOffset + 32;
|
||||||
|
const nullifierOffset = shareYOffset + 32;
|
||||||
|
const rlnIdentifierOffset = nullifierOffset + 32;
|
||||||
|
|
||||||
|
class ProofMetadata {
|
||||||
|
public constructor(
|
||||||
|
public readonly nullifier: Uint8Array,
|
||||||
|
public readonly shareX: Uint8Array,
|
||||||
|
public readonly shareY: Uint8Array,
|
||||||
|
public readonly externalNullifier: Uint8Array
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Proof implements IRateLimitProof {
|
||||||
|
public readonly proof: Uint8Array;
|
||||||
|
public readonly merkleRoot: Uint8Array;
|
||||||
|
public readonly epoch: Uint8Array;
|
||||||
|
public readonly shareX: Uint8Array;
|
||||||
|
public readonly shareY: Uint8Array;
|
||||||
|
public readonly nullifier: Uint8Array;
|
||||||
|
public readonly rlnIdentifier: Uint8Array;
|
||||||
|
|
||||||
|
public constructor(proofBytes: Uint8Array) {
|
||||||
|
if (proofBytes.length < rlnIdentifierOffset) {
|
||||||
|
throw new Error("invalid proof");
|
||||||
|
}
|
||||||
|
// parse the proof as proof<128> | share_y<32> | nullifier<32> | root<32> | epoch<32> | share_x<32> | rln_identifier<32>
|
||||||
|
this.proof = proofBytes.subarray(0, proofOffset);
|
||||||
|
this.merkleRoot = proofBytes.subarray(proofOffset, rootOffset);
|
||||||
|
this.epoch = proofBytes.subarray(rootOffset, epochOffset);
|
||||||
|
this.shareX = proofBytes.subarray(epochOffset, shareXOffset);
|
||||||
|
this.shareY = proofBytes.subarray(shareXOffset, shareYOffset);
|
||||||
|
this.nullifier = proofBytes.subarray(shareYOffset, nullifierOffset);
|
||||||
|
this.rlnIdentifier = proofBytes.subarray(
|
||||||
|
nullifierOffset,
|
||||||
|
rlnIdentifierOffset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public extractMetadata(): ProofMetadata {
|
||||||
|
const externalNullifier = poseidonHash(this.epoch, this.rlnIdentifier);
|
||||||
|
return new ProofMetadata(
|
||||||
|
this.nullifier,
|
||||||
|
this.shareX,
|
||||||
|
this.shareY,
|
||||||
|
externalNullifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function proofToBytes(p: IRateLimitProof): Uint8Array {
|
||||||
|
return concatenate(
|
||||||
|
p.proof,
|
||||||
|
p.merkleRoot,
|
||||||
|
p.epoch,
|
||||||
|
p.shareX,
|
||||||
|
p.shareY,
|
||||||
|
p.nullifier,
|
||||||
|
p.rlnIdentifier
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
packages/rln/src/resources/rln.wasm
Normal file
BIN
packages/rln/src/resources/rln.wasm
Normal file
Binary file not shown.
BIN
packages/rln/src/resources/rln_final.zkey
Normal file
BIN
packages/rln/src/resources/rln_final.zkey
Normal file
Binary file not shown.
13
packages/rln/src/resources/verification_key.d.ts
vendored
Normal file
13
packages/rln/src/resources/verification_key.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
declare const verificationKey: {
|
||||||
|
protocol: string;
|
||||||
|
curve: string;
|
||||||
|
nPublic: number;
|
||||||
|
vk_alpha_1: string[];
|
||||||
|
vk_beta_2: string[][];
|
||||||
|
vk_gamma_2: string[][];
|
||||||
|
vk_delta_2: string[][];
|
||||||
|
vk_alphabeta_12: string[][][];
|
||||||
|
IC: string[][];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verificationKey;
|
||||||
112
packages/rln/src/resources/verification_key.js
Normal file
112
packages/rln/src/resources/verification_key.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
const verificationKey = {
|
||||||
|
protocol: "groth16",
|
||||||
|
curve: "bn128",
|
||||||
|
nPublic: 6,
|
||||||
|
vk_alpha_1: [
|
||||||
|
"20124996762962216725442980738609010303800849578410091356605067053491763969391",
|
||||||
|
"9118593021526896828671519912099489027245924097793322973632351264852174143923",
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
vk_beta_2: [
|
||||||
|
[
|
||||||
|
"4693952934005375501364248788849686435240706020501681709396105298107971354382",
|
||||||
|
"14346958885444710485362620645446987998958218205939139994511461437152241966681"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"16851772916911573982706166384196538392731905827088356034885868448550849804972",
|
||||||
|
"823612331030938060799959717749043047845343400798220427319188951998582076532"
|
||||||
|
],
|
||||||
|
["1", "0"]
|
||||||
|
],
|
||||||
|
vk_gamma_2: [
|
||||||
|
[
|
||||||
|
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
|
||||||
|
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
|
||||||
|
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
|
||||||
|
],
|
||||||
|
["1", "0"]
|
||||||
|
],
|
||||||
|
vk_delta_2: [
|
||||||
|
[
|
||||||
|
"8353516066399360694538747105302262515182301251524941126222712285088022964076",
|
||||||
|
"9329524012539638256356482961742014315122377605267454801030953882967973561832"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"16805391589556134376869247619848130874761233086443465978238468412168162326401",
|
||||||
|
"10111259694977636294287802909665108497237922060047080343914303287629927847739"
|
||||||
|
],
|
||||||
|
["1", "0"]
|
||||||
|
],
|
||||||
|
vk_alphabeta_12: [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"12608968655665301215455851857466367636344427685631271961542642719683786103711",
|
||||||
|
"9849575605876329747382930567422916152871921500826003490242628251047652318086"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"6322029441245076030714726551623552073612922718416871603535535085523083939021",
|
||||||
|
"8700115492541474338049149013125102281865518624059015445617546140629435818912"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"10674973475340072635573101639867487770811074181475255667220644196793546640210",
|
||||||
|
"2926286967251299230490668407790788696102889214647256022788211245826267484824"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"9660441540778523475944706619139394922744328902833875392144658911530830074820",
|
||||||
|
"19548113127774514328631808547691096362144426239827206966690021428110281506546"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1870837942477655969123169532603615788122896469891695773961478956740992497097",
|
||||||
|
"12536105729661705698805725105036536744930776470051238187456307227425796690780"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"21811903352654147452884857281720047789720483752548991551595462057142824037334",
|
||||||
|
"19021616763967199151052893283384285352200445499680068407023236283004353578353"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
IC: [
|
||||||
|
[
|
||||||
|
"11992897507809711711025355300535923222599547639134311050809253678876341466909",
|
||||||
|
"17181525095924075896332561978747020491074338784673526378866503154966799128110",
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"17018665030246167677911144513385572506766200776123272044534328594850561667818",
|
||||||
|
"18601114175490465275436712413925513066546725461375425769709566180981674884464",
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"18799470100699658367834559797874857804183288553462108031963980039244731716542",
|
||||||
|
"13064227487174191981628537974951887429496059857753101852163607049188825592007",
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"17432501889058124609368103715904104425610382063762621017593209214189134571156",
|
||||||
|
"13406815149699834788256141097399354592751313348962590382887503595131085938635",
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"10320964835612716439094703312987075811498239445882526576970512041988148264481",
|
||||||
|
"9024164961646353611176283204118089412001502110138072989569118393359029324867",
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"718355081067365548229685160476620267257521491773976402837645005858953849298",
|
||||||
|
"14635482993933988261008156660773180150752190597753512086153001683711587601974",
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"11777720285956632126519898515392071627539405001940313098390150593689568177535",
|
||||||
|
"8483603647274280691250972408211651407952870456587066148445913156086740744515",
|
||||||
|
"1"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verificationKey;
|
||||||
11
packages/rln/src/resources/witness_calculator.d.ts
vendored
Normal file
11
packages/rln/src/resources/witness_calculator.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export async function builder(
|
||||||
|
code: Uint8Array,
|
||||||
|
sanityCheck: boolean
|
||||||
|
): Promise<WitnessCalculator>;
|
||||||
|
|
||||||
|
export class WitnessCalculator {
|
||||||
|
public calculateWitness(
|
||||||
|
input: unknown,
|
||||||
|
sanityCheck: boolean
|
||||||
|
): Promise<Array<bigint>>;
|
||||||
|
}
|
||||||
328
packages/rln/src/resources/witness_calculator.js
Normal file
328
packages/rln/src/resources/witness_calculator.js
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
// File generated with https://github.com/iden3/circom
|
||||||
|
// following the instructions from:
|
||||||
|
// https://github.com/vacp2p/zerokit/tree/master/rln#compiling-circuits
|
||||||
|
|
||||||
|
export async function builder(code, options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
let wasmModule;
|
||||||
|
try {
|
||||||
|
wasmModule = await WebAssembly.compile(code);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
console.log(
|
||||||
|
"\nTry to run circom --c in order to generate c++ code instead\n"
|
||||||
|
);
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let wc;
|
||||||
|
|
||||||
|
let errStr = "";
|
||||||
|
let msgStr = "";
|
||||||
|
|
||||||
|
const instance = await WebAssembly.instantiate(wasmModule, {
|
||||||
|
runtime: {
|
||||||
|
exceptionHandler: function (code) {
|
||||||
|
let err;
|
||||||
|
if (code == 1) {
|
||||||
|
err = "Signal not found.\n";
|
||||||
|
} else if (code == 2) {
|
||||||
|
err = "Too many signals set.\n";
|
||||||
|
} else if (code == 3) {
|
||||||
|
err = "Signal already set.\n";
|
||||||
|
} else if (code == 4) {
|
||||||
|
err = "Assert Failed.\n";
|
||||||
|
} else if (code == 5) {
|
||||||
|
err = "Not enough memory.\n";
|
||||||
|
} else if (code == 6) {
|
||||||
|
err = "Input signal array access exceeds the size.\n";
|
||||||
|
} else {
|
||||||
|
err = "Unknown error.\n";
|
||||||
|
}
|
||||||
|
throw new Error(err + errStr);
|
||||||
|
},
|
||||||
|
printErrorMessage: function () {
|
||||||
|
errStr += getMessage() + "\n";
|
||||||
|
// console.error(getMessage());
|
||||||
|
},
|
||||||
|
writeBufferMessage: function () {
|
||||||
|
const msg = getMessage();
|
||||||
|
// Any calls to `log()` will always end with a `\n`, so that's when we print and reset
|
||||||
|
if (msg === "\n") {
|
||||||
|
console.log(msgStr);
|
||||||
|
msgStr = "";
|
||||||
|
} else {
|
||||||
|
// If we've buffered other content, put a space in between the items
|
||||||
|
if (msgStr !== "") {
|
||||||
|
msgStr += " ";
|
||||||
|
}
|
||||||
|
// Then append the message to the message we are creating
|
||||||
|
msgStr += msg;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showSharedRWMemory: function () {
|
||||||
|
printSharedRWMemory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sanityCheck = options;
|
||||||
|
// options &&
|
||||||
|
// (
|
||||||
|
// options.sanityCheck ||
|
||||||
|
// options.logGetSignal ||
|
||||||
|
// options.logSetSignal ||
|
||||||
|
// options.logStartComponent ||
|
||||||
|
// options.logFinishComponent
|
||||||
|
// );
|
||||||
|
|
||||||
|
wc = new WitnessCalculator(instance, sanityCheck);
|
||||||
|
return wc;
|
||||||
|
|
||||||
|
function getMessage() {
|
||||||
|
var message = "";
|
||||||
|
var c = instance.exports.getMessageChar();
|
||||||
|
while (c != 0) {
|
||||||
|
message += String.fromCharCode(c);
|
||||||
|
c = instance.exports.getMessageChar();
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printSharedRWMemory() {
|
||||||
|
const shared_rw_memory_size = instance.exports.getFieldNumLen32();
|
||||||
|
const arr = new Uint32Array(shared_rw_memory_size);
|
||||||
|
for (let j = 0; j < shared_rw_memory_size; j++) {
|
||||||
|
arr[shared_rw_memory_size - 1 - j] =
|
||||||
|
instance.exports.readSharedRWMemory(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've buffered other content, put a space in between the items
|
||||||
|
if (msgStr !== "") {
|
||||||
|
msgStr += " ";
|
||||||
|
}
|
||||||
|
// Then append the value to the message we are creating
|
||||||
|
msgStr += fromArray32(arr).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WitnessCalculator {
|
||||||
|
constructor(instance, sanityCheck) {
|
||||||
|
this.instance = instance;
|
||||||
|
|
||||||
|
this.version = this.instance.exports.getVersion();
|
||||||
|
this.n32 = this.instance.exports.getFieldNumLen32();
|
||||||
|
|
||||||
|
this.instance.exports.getRawPrime();
|
||||||
|
const arr = new Uint32Array(this.n32);
|
||||||
|
for (let i = 0; i < this.n32; i++) {
|
||||||
|
arr[this.n32 - 1 - i] = this.instance.exports.readSharedRWMemory(i);
|
||||||
|
}
|
||||||
|
this.prime = fromArray32(arr);
|
||||||
|
|
||||||
|
this.witnessSize = this.instance.exports.getWitnessSize();
|
||||||
|
|
||||||
|
this.sanityCheck = sanityCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
circom_version() {
|
||||||
|
return this.instance.exports.getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _doCalculateWitness(input, sanityCheck) {
|
||||||
|
//input is assumed to be a map from signals to arrays of bigints
|
||||||
|
this.instance.exports.init(this.sanityCheck || sanityCheck ? 1 : 0);
|
||||||
|
const keys = Object.keys(input);
|
||||||
|
var input_counter = 0;
|
||||||
|
keys.forEach((k) => {
|
||||||
|
const h = fnvHash(k);
|
||||||
|
const hMSB = parseInt(h.slice(0, 8), 16);
|
||||||
|
const hLSB = parseInt(h.slice(8, 16), 16);
|
||||||
|
const fArr = flatArray(input[k]);
|
||||||
|
let signalSize = this.instance.exports.getInputSignalSize(hMSB, hLSB);
|
||||||
|
if (signalSize < 0) {
|
||||||
|
throw new Error(`Signal ${k} not found\n`);
|
||||||
|
}
|
||||||
|
if (fArr.length < signalSize) {
|
||||||
|
throw new Error(`Not enough values for input signal ${k}\n`);
|
||||||
|
}
|
||||||
|
if (fArr.length > signalSize) {
|
||||||
|
throw new Error(`Too many values for input signal ${k}\n`);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < fArr.length; i++) {
|
||||||
|
const arrFr = toArray32(BigInt(fArr[i]) % this.prime, this.n32);
|
||||||
|
for (let j = 0; j < this.n32; j++) {
|
||||||
|
this.instance.exports.writeSharedRWMemory(j, arrFr[this.n32 - 1 - j]);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.instance.exports.setInputSignal(hMSB, hLSB, i);
|
||||||
|
input_counter++;
|
||||||
|
} catch (err) {
|
||||||
|
// console.log(`After adding signal ${i} of ${k}`)
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (input_counter < this.instance.exports.getInputSize()) {
|
||||||
|
throw new Error(
|
||||||
|
`Not all inputs have been set. Only ${input_counter} out of ${this.instance.exports.getInputSize()}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calculateWitness(input, sanityCheck) {
|
||||||
|
const w = [];
|
||||||
|
|
||||||
|
await this._doCalculateWitness(input, sanityCheck);
|
||||||
|
|
||||||
|
for (let i = 0; i < this.witnessSize; i++) {
|
||||||
|
this.instance.exports.getWitness(i);
|
||||||
|
const arr = new Uint32Array(this.n32);
|
||||||
|
for (let j = 0; j < this.n32; j++) {
|
||||||
|
arr[this.n32 - 1 - j] = this.instance.exports.readSharedRWMemory(j);
|
||||||
|
}
|
||||||
|
w.push(fromArray32(arr));
|
||||||
|
}
|
||||||
|
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
async calculateBinWitness(input, sanityCheck) {
|
||||||
|
const buff32 = new Uint32Array(this.witnessSize * this.n32);
|
||||||
|
const buff = new Uint8Array(buff32.buffer);
|
||||||
|
await this._doCalculateWitness(input, sanityCheck);
|
||||||
|
|
||||||
|
for (let i = 0; i < this.witnessSize; i++) {
|
||||||
|
this.instance.exports.getWitness(i);
|
||||||
|
const pos = i * this.n32;
|
||||||
|
for (let j = 0; j < this.n32; j++) {
|
||||||
|
buff32[pos + j] = this.instance.exports.readSharedRWMemory(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buff;
|
||||||
|
}
|
||||||
|
|
||||||
|
async calculateWTNSBin(input, sanityCheck) {
|
||||||
|
const buff32 = new Uint32Array(this.witnessSize * this.n32 + this.n32 + 11);
|
||||||
|
const buff = new Uint8Array(buff32.buffer);
|
||||||
|
await this._doCalculateWitness(input, sanityCheck);
|
||||||
|
|
||||||
|
//"wtns"
|
||||||
|
buff[0] = "w".charCodeAt(0);
|
||||||
|
buff[1] = "t".charCodeAt(0);
|
||||||
|
buff[2] = "n".charCodeAt(0);
|
||||||
|
buff[3] = "s".charCodeAt(0);
|
||||||
|
|
||||||
|
//version 2
|
||||||
|
buff32[1] = 2;
|
||||||
|
|
||||||
|
//number of sections: 2
|
||||||
|
buff32[2] = 2;
|
||||||
|
|
||||||
|
//id section 1
|
||||||
|
buff32[3] = 1;
|
||||||
|
|
||||||
|
const n8 = this.n32 * 4;
|
||||||
|
//id section 1 length in 64bytes
|
||||||
|
const idSection1length = 8 + n8;
|
||||||
|
const idSection1lengthHex = idSection1length.toString(16);
|
||||||
|
buff32[4] = parseInt(idSection1lengthHex.slice(0, 8), 16);
|
||||||
|
buff32[5] = parseInt(idSection1lengthHex.slice(8, 16), 16);
|
||||||
|
|
||||||
|
//this.n32
|
||||||
|
buff32[6] = n8;
|
||||||
|
|
||||||
|
//prime number
|
||||||
|
this.instance.exports.getRawPrime();
|
||||||
|
|
||||||
|
var pos = 7;
|
||||||
|
for (let j = 0; j < this.n32; j++) {
|
||||||
|
buff32[pos + j] = this.instance.exports.readSharedRWMemory(j);
|
||||||
|
}
|
||||||
|
pos += this.n32;
|
||||||
|
|
||||||
|
// witness size
|
||||||
|
buff32[pos] = this.witnessSize;
|
||||||
|
pos++;
|
||||||
|
|
||||||
|
//id section 2
|
||||||
|
buff32[pos] = 2;
|
||||||
|
pos++;
|
||||||
|
|
||||||
|
// section 2 length
|
||||||
|
const idSection2length = n8 * this.witnessSize;
|
||||||
|
const idSection2lengthHex = idSection2length.toString(16);
|
||||||
|
buff32[pos] = parseInt(idSection2lengthHex.slice(0, 8), 16);
|
||||||
|
buff32[pos + 1] = parseInt(idSection2lengthHex.slice(8, 16), 16);
|
||||||
|
|
||||||
|
pos += 2;
|
||||||
|
for (let i = 0; i < this.witnessSize; i++) {
|
||||||
|
this.instance.exports.getWitness(i);
|
||||||
|
for (let j = 0; j < this.n32; j++) {
|
||||||
|
buff32[pos + j] = this.instance.exports.readSharedRWMemory(j);
|
||||||
|
}
|
||||||
|
pos += this.n32;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toArray32(rem, size) {
|
||||||
|
const res = []; //new Uint32Array(size); //has no unshift
|
||||||
|
const radix = BigInt(0x100000000);
|
||||||
|
while (rem) {
|
||||||
|
res.unshift(Number(rem % radix));
|
||||||
|
rem = rem / radix;
|
||||||
|
}
|
||||||
|
if (size) {
|
||||||
|
var i = size - res.length;
|
||||||
|
while (i > 0) {
|
||||||
|
res.unshift(0);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromArray32(arr) {
|
||||||
|
//returns a BigInt
|
||||||
|
var res = BigInt(0);
|
||||||
|
const radix = BigInt(0x100000000);
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
res = res * radix + BigInt(arr[i]);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flatArray(a) {
|
||||||
|
var res = [];
|
||||||
|
fillArray(res, a);
|
||||||
|
return res;
|
||||||
|
|
||||||
|
function fillArray(res, a) {
|
||||||
|
if (Array.isArray(a)) {
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
fillArray(res, a[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fnvHash(str) {
|
||||||
|
const uint64_max = BigInt(2) ** BigInt(64);
|
||||||
|
let hash = BigInt("0xCBF29CE484222325");
|
||||||
|
for (var i = 0; i < str.length; i++) {
|
||||||
|
hash ^= BigInt(str[i].charCodeAt());
|
||||||
|
hash *= BigInt(0x100000001b3);
|
||||||
|
hash %= uint64_max;
|
||||||
|
}
|
||||||
|
let shash = hash.toString(16);
|
||||||
|
let n = 16 - shash.length;
|
||||||
|
shash = "0".repeat(n).concat(shash);
|
||||||
|
return shash;
|
||||||
|
}
|
||||||
298
packages/rln/src/rln.ts
Normal file
298
packages/rln/src/rln.ts
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import { createDecoder, createEncoder } from "@waku/core";
|
||||||
|
import type {
|
||||||
|
ContentTopic,
|
||||||
|
IDecodedMessage,
|
||||||
|
EncoderOptions as WakuEncoderOptions
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { Logger } from "@waku/utils";
|
||||||
|
import init from "@waku/zerokit-rln-wasm";
|
||||||
|
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createRLNDecoder,
|
||||||
|
createRLNEncoder,
|
||||||
|
type RLNDecoder,
|
||||||
|
type RLNEncoder
|
||||||
|
} from "./codec.js";
|
||||||
|
import { RLNContract, SEPOLIA_CONTRACT } from "./contract/index.js";
|
||||||
|
import { IdentityCredential } from "./identity.js";
|
||||||
|
import { Keystore } from "./keystore/index.js";
|
||||||
|
import type {
|
||||||
|
DecryptedCredentials,
|
||||||
|
EncryptedCredentials
|
||||||
|
} from "./keystore/index.js";
|
||||||
|
import { KeystoreEntity, Password } from "./keystore/types.js";
|
||||||
|
import verificationKey from "./resources/verification_key";
|
||||||
|
import * as wc from "./resources/witness_calculator";
|
||||||
|
import { extractMetaMaskSigner } from "./utils/index.js";
|
||||||
|
import { Zerokit } from "./zerokit.js";
|
||||||
|
|
||||||
|
const log = new Logger("waku:rln");
|
||||||
|
|
||||||
|
async function loadWitnessCalculator(): Promise<wc.WitnessCalculator> {
|
||||||
|
const res = await fetch("/base/rln.wasm");
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch rln.wasm: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
const witnessBuffer = await res.arrayBuffer();
|
||||||
|
return wc.builder(new Uint8Array(witnessBuffer), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadZkey(): Promise<Uint8Array> {
|
||||||
|
const res = await fetch("/base/rln_final.zkey");
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch rln_final.zkey: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
return new Uint8Array(await res.arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of RLN
|
||||||
|
* @returns RLNInstance
|
||||||
|
*/
|
||||||
|
export async function create(): Promise<RLNInstance> {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await (init as any)?.();
|
||||||
|
zerokitRLN.init_panic_hook();
|
||||||
|
|
||||||
|
const witnessCalculator = await loadWitnessCalculator();
|
||||||
|
const zkey = await loadZkey();
|
||||||
|
|
||||||
|
const stringEncoder = new TextEncoder();
|
||||||
|
const vkey = stringEncoder.encode(JSON.stringify(verificationKey));
|
||||||
|
|
||||||
|
const DEPTH = 20;
|
||||||
|
const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey);
|
||||||
|
const zerokit = new Zerokit(zkRLN, witnessCalculator);
|
||||||
|
|
||||||
|
return new RLNInstance(zerokit);
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to initialize RLN:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StartRLNOptions = {
|
||||||
|
/**
|
||||||
|
* If not set - will extract MetaMask account and get signer from it.
|
||||||
|
*/
|
||||||
|
signer?: ethers.Signer;
|
||||||
|
/**
|
||||||
|
* If not set - will use default SEPOLIA_CONTRACT address.
|
||||||
|
*/
|
||||||
|
registryAddress?: string;
|
||||||
|
/**
|
||||||
|
* Credentials to use for generating proofs and connecting to the contract and network.
|
||||||
|
* If provided used for validating the network chainId and connecting to registry contract.
|
||||||
|
*/
|
||||||
|
credentials?: EncryptedCredentials | DecryptedCredentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RegisterMembershipOptions =
|
||||||
|
| { signature: string }
|
||||||
|
| { identity: IdentityCredential };
|
||||||
|
|
||||||
|
type WakuRLNEncoderOptions = WakuEncoderOptions & {
|
||||||
|
credentials: EncryptedCredentials | DecryptedCredentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RLNInstance {
|
||||||
|
private started = false;
|
||||||
|
private starting = false;
|
||||||
|
|
||||||
|
private _contract: undefined | RLNContract;
|
||||||
|
private _signer: undefined | ethers.Signer;
|
||||||
|
|
||||||
|
private keystore = Keystore.create();
|
||||||
|
private _credentials: undefined | DecryptedCredentials;
|
||||||
|
|
||||||
|
public constructor(public zerokit: Zerokit) {}
|
||||||
|
|
||||||
|
public get contract(): undefined | RLNContract {
|
||||||
|
return this._contract;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get signer(): undefined | ethers.Signer {
|
||||||
|
return this._signer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(options: StartRLNOptions = {}): Promise<void> {
|
||||||
|
if (this.started || this.starting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.starting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { credentials, keystore } =
|
||||||
|
await RLNInstance.decryptCredentialsIfNeeded(options.credentials);
|
||||||
|
const { signer, registryAddress } = await this.determineStartOptions(
|
||||||
|
options,
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
|
||||||
|
if (keystore) {
|
||||||
|
this.keystore = keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._credentials = credentials;
|
||||||
|
this._signer = signer!;
|
||||||
|
this._contract = await RLNContract.init(this, {
|
||||||
|
registryAddress: registryAddress!,
|
||||||
|
signer: signer!
|
||||||
|
});
|
||||||
|
this.started = true;
|
||||||
|
} finally {
|
||||||
|
this.starting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async determineStartOptions(
|
||||||
|
options: StartRLNOptions,
|
||||||
|
credentials: KeystoreEntity | undefined
|
||||||
|
): Promise<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = options.signer || (await extractMetaMaskSigner());
|
||||||
|
const currentChainId = await signer.getChainId();
|
||||||
|
|
||||||
|
if (chainId && chainId !== currentChainId) {
|
||||||
|
throw Error(
|
||||||
|
`Failed to start RLN contract, chain ID of contract is different from current one: contract-${chainId}, current network-${currentChainId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
signer,
|
||||||
|
registryAddress
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async decryptCredentialsIfNeeded(
|
||||||
|
credentials?: EncryptedCredentials | DecryptedCredentials
|
||||||
|
): Promise<{ credentials?: DecryptedCredentials; keystore?: Keystore }> {
|
||||||
|
if (!credentials) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("identity" in credentials) {
|
||||||
|
return { credentials };
|
||||||
|
}
|
||||||
|
|
||||||
|
const keystore = Keystore.fromString(credentials.keystore);
|
||||||
|
|
||||||
|
if (!keystore) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedCredentials = await keystore.readCredential(
|
||||||
|
credentials.id,
|
||||||
|
credentials.password
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
keystore,
|
||||||
|
credentials: decryptedCredentials
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async registerMembership(
|
||||||
|
options: RegisterMembershipOptions
|
||||||
|
): Promise<undefined | DecryptedCredentials> {
|
||||||
|
if (!this.contract) {
|
||||||
|
throw Error("RLN Contract is not initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let identity = "identity" in options && options.identity;
|
||||||
|
|
||||||
|
if ("signature" in options) {
|
||||||
|
identity = this.zerokit.generateSeededIdentityCredential(
|
||||||
|
options.signature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw Error("Missing signature or identity to register membership.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.contract.registerWithIdentity(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes credentials in use by relying on provided Keystore earlier in rln.start
|
||||||
|
* @param id: string, hash of credentials to select from Keystore
|
||||||
|
* @param password: string or bytes to use to decrypt credentials from Keystore
|
||||||
|
*/
|
||||||
|
public async useCredentials(id: string, password: Password): Promise<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
packages/rln/src/root_tracker.spec.ts
Normal file
56
packages/rln/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]));
|
||||||
|
});
|
||||||
|
});
|
||||||
92
packages/rln/src/root_tracker.ts
Normal file
92
packages/rln/src/root_tracker.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
class RootPerBlock {
|
||||||
|
public 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>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private acceptableRootWindowSize: number,
|
||||||
|
initialRoot: Uint8Array
|
||||||
|
) {
|
||||||
|
this.pushRoot(0, initialRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public backFill(fromBlockNumber: number): void {
|
||||||
|
if (this.validMerkleRoots.length == 0) return;
|
||||||
|
|
||||||
|
let numBlocks = 0;
|
||||||
|
for (let i = this.validMerkleRoots.length - 1; i >= 0; i--) {
|
||||||
|
if (this.validMerkleRoots[i].blockNumber >= fromBlockNumber) {
|
||||||
|
numBlocks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numBlocks == 0) return;
|
||||||
|
|
||||||
|
const olderBlock = fromBlockNumber < this.validMerkleRoots[0].blockNumber;
|
||||||
|
|
||||||
|
// Remove last roots
|
||||||
|
let rootsToPop = numBlocks;
|
||||||
|
if (this.validMerkleRoots.length < rootsToPop) {
|
||||||
|
rootsToPop = this.validMerkleRoots.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validMerkleRoots = this.validMerkleRoots.slice(
|
||||||
|
0,
|
||||||
|
this.validMerkleRoots.length - rootsToPop
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.merkleRootBuffer.length == 0) return;
|
||||||
|
|
||||||
|
if (olderBlock) {
|
||||||
|
const idx = this.merkleRootBuffer.findIndex(
|
||||||
|
(x) => x.blockNumber == fromBlockNumber
|
||||||
|
);
|
||||||
|
if (idx > -1) {
|
||||||
|
this.merkleRootBuffer = this.merkleRootBuffer.slice(0, idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backfill the tree's acceptable roots
|
||||||
|
let rootsToRestore =
|
||||||
|
this.acceptableRootWindowSize - this.validMerkleRoots.length;
|
||||||
|
if (this.merkleRootBuffer.length < rootsToRestore) {
|
||||||
|
rootsToRestore = this.merkleRootBuffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < rootsToRestore; i++) {
|
||||||
|
const x = this.merkleRootBuffer.pop();
|
||||||
|
if (x) this.validMerkleRoots.unshift(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushRoot(blockNumber: number, root: Uint8Array): void {
|
||||||
|
this.validMerkleRoots.push(new RootPerBlock(root, blockNumber));
|
||||||
|
|
||||||
|
// Maintain valid merkle root window
|
||||||
|
if (this.validMerkleRoots.length > this.acceptableRootWindowSize) {
|
||||||
|
const x = this.validMerkleRoots.shift();
|
||||||
|
if (x) this.merkleRootBuffer.push(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintain merkle root buffer
|
||||||
|
if (this.merkleRootBuffer.length > maxBufferSize) {
|
||||||
|
this.merkleRootBuffer.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public roots(): Array<Uint8Array> {
|
||||||
|
return this.validMerkleRoots.map((x) => x.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public buffer(): Array<Uint8Array> {
|
||||||
|
return this.merkleRootBuffer.map((x) => x.root);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
packages/rln/src/utils/bytes.ts
Normal file
84
packages/rln/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;
|
||||||
|
}
|
||||||
17
packages/rln/src/utils/epoch.spec.ts
Normal file
17
packages/rln/src/utils/epoch.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { expect } from "chai";
|
||||||
|
import fc from "fast-check";
|
||||||
|
|
||||||
|
import { epochBytesToInt, epochIntToBytes } from "./epoch.js";
|
||||||
|
|
||||||
|
describe("epoch serialization", () => {
|
||||||
|
it("Round trip", async function () {
|
||||||
|
await fc.assert(
|
||||||
|
fc.asyncProperty(fc.integer({ min: 0 }), async (date) => {
|
||||||
|
const bytes = epochIntToBytes(date);
|
||||||
|
const _date = epochBytesToInt(bytes);
|
||||||
|
|
||||||
|
expect(_date.valueOf()).to.eq(date.valueOf());
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
30
packages/rln/src/utils/epoch.ts
Normal file
30
packages/rln/src/utils/epoch.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Logger } from "@waku/utils";
|
||||||
|
|
||||||
|
const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds
|
||||||
|
|
||||||
|
const log = new Logger("waku:rln:epoch");
|
||||||
|
|
||||||
|
export function dateToEpoch(
|
||||||
|
timestamp: Date,
|
||||||
|
epochUnitSeconds: number = DefaultEpochUnitSeconds
|
||||||
|
): number {
|
||||||
|
const time = timestamp.getTime();
|
||||||
|
const epoch = Math.floor(time / 1000 / epochUnitSeconds);
|
||||||
|
log.info("generated epoch", epoch);
|
||||||
|
return epoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function epochIntToBytes(epoch: number): Uint8Array {
|
||||||
|
const bytes = new Uint8Array(32);
|
||||||
|
const db = new DataView(bytes.buffer);
|
||||||
|
db.setUint32(0, epoch, true);
|
||||||
|
log.info("encoded epoch", epoch, bytes);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function epochBytesToInt(bytes: Uint8Array): number {
|
||||||
|
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||||
|
const epoch = dv.getUint32(0, true);
|
||||||
|
log.info("decoded epoch", epoch, bytes);
|
||||||
|
return epoch;
|
||||||
|
}
|
||||||
15
packages/rln/src/utils/hash.ts
Normal file
15
packages/rln/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
packages/rln/src/utils/index.ts
Normal file
9
packages/rln/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
packages/rln/src/utils/metamask.ts
Normal file
17
packages/rln/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();
|
||||||
|
};
|
||||||
184
packages/rln/src/zerokit.ts
Normal file
184
packages/rln/src/zerokit.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import type { IRateLimitProof } from "@waku/interfaces";
|
||||||
|
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
||||||
|
|
||||||
|
import { IdentityCredential } from "./identity.js";
|
||||||
|
import { Proof, proofToBytes } from "./proof.js";
|
||||||
|
import { WitnessCalculator } from "./resources/witness_calculator";
|
||||||
|
import {
|
||||||
|
concatenate,
|
||||||
|
dateToEpoch,
|
||||||
|
epochIntToBytes,
|
||||||
|
writeUIntLE
|
||||||
|
} from "./utils/index.js";
|
||||||
|
|
||||||
|
export class Zerokit {
|
||||||
|
public constructor(
|
||||||
|
private readonly zkRLN: number,
|
||||||
|
private readonly witnessCalculator: WitnessCalculator
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public generateIdentityCredentials(): IdentityCredential {
|
||||||
|
const memKeys = zerokitRLN.generateExtendedMembershipKey(this.zkRLN); // TODO: rename this function in zerokit rln-wasm
|
||||||
|
return IdentityCredential.fromBytes(memKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateSeededIdentityCredential(seed: string): IdentityCredential {
|
||||||
|
const stringEncoder = new TextEncoder();
|
||||||
|
const seedBytes = stringEncoder.encode(seed);
|
||||||
|
// TODO: rename this function in zerokit rln-wasm
|
||||||
|
const memKeys = zerokitRLN.generateSeededExtendedMembershipKey(
|
||||||
|
this.zkRLN,
|
||||||
|
seedBytes
|
||||||
|
);
|
||||||
|
return IdentityCredential.fromBytes(memKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
public insertMember(idCommitment: Uint8Array): void {
|
||||||
|
zerokitRLN.insertMember(this.zkRLN, idCommitment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public insertMembers(
|
||||||
|
index: number,
|
||||||
|
...idCommitments: Array<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteMember(index: number): void {
|
||||||
|
zerokitRLN.deleteLeaf(this.zkRLN, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMerkleRoot(): Uint8Array {
|
||||||
|
return zerokitRLN.getRoot(this.zkRLN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public serializeMessage(
|
||||||
|
uint8Msg: Uint8Array,
|
||||||
|
memIndex: number,
|
||||||
|
epoch: Uint8Array,
|
||||||
|
idKey: Uint8Array
|
||||||
|
): Uint8Array {
|
||||||
|
// calculate message length
|
||||||
|
const msgLen = writeUIntLE(new Uint8Array(8), uint8Msg.length, 0, 8);
|
||||||
|
|
||||||
|
// Converting index to LE bytes
|
||||||
|
const memIndexBytes = writeUIntLE(new Uint8Array(8), memIndex, 0, 8);
|
||||||
|
|
||||||
|
// [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> ]
|
||||||
|
return concatenate(idKey, memIndexBytes, epoch, msgLen, uint8Msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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 new Error("invalid epoch");
|
||||||
|
if (idSecretHash.length !== 32) throw new Error("invalid id secret hash");
|
||||||
|
if (index < 0) throw new Error("index must be >= 0");
|
||||||
|
|
||||||
|
const serialized_msg = this.serializeMessage(
|
||||||
|
msg,
|
||||||
|
index,
|
||||||
|
epoch,
|
||||||
|
idSecretHash
|
||||||
|
);
|
||||||
|
const rlnWitness = zerokitRLN.getSerializedRLNWitness(
|
||||||
|
this.zkRLN,
|
||||||
|
serialized_msg
|
||||||
|
);
|
||||||
|
const inputs = zerokitRLN.RLNWitnessToJson(this.zkRLN, rlnWitness);
|
||||||
|
const calculatedWitness = await this.witnessCalculator.calculateWitness(
|
||||||
|
inputs,
|
||||||
|
false
|
||||||
|
); // no sanity check being used in zerokit
|
||||||
|
|
||||||
|
const proofBytes = zerokitRLN.generate_rln_proof_with_witness(
|
||||||
|
this.zkRLN,
|
||||||
|
calculatedWitness,
|
||||||
|
rlnWitness
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Proof(proofBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public verifyRLNProof(
|
||||||
|
proof: IRateLimitProof | Uint8Array,
|
||||||
|
msg: Uint8Array
|
||||||
|
): boolean {
|
||||||
|
let pBytes: Uint8Array;
|
||||||
|
if (proof instanceof Uint8Array) {
|
||||||
|
pBytes = proof;
|
||||||
|
} else {
|
||||||
|
pBytes = proofToBytes(proof);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate message length
|
||||||
|
const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
||||||
|
|
||||||
|
return zerokitRLN.verifyRLNProof(
|
||||||
|
this.zkRLN,
|
||||||
|
concatenate(pBytes, msgLen, msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public verifyWithRoots(
|
||||||
|
proof: IRateLimitProof | Uint8Array,
|
||||||
|
msg: Uint8Array,
|
||||||
|
...roots: Array<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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public verifyWithNoRoot(
|
||||||
|
proof: IRateLimitProof | Uint8Array,
|
||||||
|
msg: Uint8Array
|
||||||
|
): boolean {
|
||||||
|
let pBytes: Uint8Array;
|
||||||
|
if (proof instanceof Uint8Array) {
|
||||||
|
pBytes = proof;
|
||||||
|
} else {
|
||||||
|
pBytes = proofToBytes(proof);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate message length
|
||||||
|
const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
||||||
|
|
||||||
|
return zerokitRLN.verifyWithRoots(
|
||||||
|
this.zkRLN,
|
||||||
|
concatenate(pBytes, msgLen, msg),
|
||||||
|
new Uint8Array()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/rln/tsconfig.dev.json
Normal file
3
packages/rln/tsconfig.dev.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.dev"
|
||||||
|
}
|
||||||
10
packages/rln/tsconfig.json
Normal file
10
packages/rln/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist/",
|
||||||
|
"rootDir": "src",
|
||||||
|
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["src/**/*.spec.ts", "src/test_utils"]
|
||||||
|
}
|
||||||
@ -51,7 +51,7 @@ export async function runNodes<T>(
|
|||||||
log.info("Starting js waku node with :", JSON.stringify(waku_options));
|
log.info("Starting js waku node with :", JSON.stringify(waku_options));
|
||||||
let waku: WakuNode | undefined;
|
let waku: WakuNode | undefined;
|
||||||
try {
|
try {
|
||||||
waku = (await createNode(waku_options)) as WakuNode;
|
waku = (await createNode(waku_options)) as unknown as WakuNode;
|
||||||
await waku.start();
|
await waku.start();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("jswaku node failed to start:", error);
|
log.error("jswaku node failed to start:", error);
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"incremental": true,
|
"incremental": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"module": "ES2022",
|
"module": "esnext",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user