mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-04 06:43:12 +00:00
feat(rln)!: use zerokit for credential generation (#2632)
Co-authored-by: Danish Arora <danisharora099@gmail.com>
This commit is contained in:
parent
016a25d578
commit
bbcfc94879
@ -24,9 +24,11 @@
|
|||||||
"cipherparams",
|
"cipherparams",
|
||||||
"ciphertext",
|
"ciphertext",
|
||||||
"circleci",
|
"circleci",
|
||||||
|
"circom",
|
||||||
"codecov",
|
"codecov",
|
||||||
"codegen",
|
"codegen",
|
||||||
"commitlint",
|
"commitlint",
|
||||||
|
"cooldown",
|
||||||
"dependabot",
|
"dependabot",
|
||||||
"dialable",
|
"dialable",
|
||||||
"dingpu",
|
"dingpu",
|
||||||
@ -41,9 +43,7 @@
|
|||||||
"Encrypters",
|
"Encrypters",
|
||||||
"enr",
|
"enr",
|
||||||
"enrs",
|
"enrs",
|
||||||
"unsubscription",
|
|
||||||
"enrtree",
|
"enrtree",
|
||||||
"unhandle",
|
|
||||||
"ephem",
|
"ephem",
|
||||||
"esnext",
|
"esnext",
|
||||||
"ethersproject",
|
"ethersproject",
|
||||||
@ -62,7 +62,6 @@
|
|||||||
"ineed",
|
"ineed",
|
||||||
"IPAM",
|
"IPAM",
|
||||||
"ipfs",
|
"ipfs",
|
||||||
"cooldown",
|
|
||||||
"iwant",
|
"iwant",
|
||||||
"jdev",
|
"jdev",
|
||||||
"jswaku",
|
"jswaku",
|
||||||
@ -122,9 +121,11 @@
|
|||||||
"typedoc",
|
"typedoc",
|
||||||
"undialable",
|
"undialable",
|
||||||
"unencrypted",
|
"unencrypted",
|
||||||
|
"unhandle",
|
||||||
"unmarshal",
|
"unmarshal",
|
||||||
"unmount",
|
"unmount",
|
||||||
"unmounts",
|
"unmounts",
|
||||||
|
"unsubscription",
|
||||||
"untracked",
|
"untracked",
|
||||||
"upgrader",
|
"upgrader",
|
||||||
"vacp",
|
"vacp",
|
||||||
@ -139,6 +140,7 @@
|
|||||||
"weboko",
|
"weboko",
|
||||||
"websockets",
|
"websockets",
|
||||||
"wifi",
|
"wifi",
|
||||||
|
"WTNS",
|
||||||
"xsalsa20",
|
"xsalsa20",
|
||||||
"zerokit",
|
"zerokit",
|
||||||
"Привет",
|
"Привет",
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -8414,9 +8414,9 @@
|
|||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@waku/zerokit-rln-wasm": {
|
"node_modules/@waku/zerokit-rln-wasm": {
|
||||||
"version": "0.0.13",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.2.1.tgz",
|
||||||
"integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA==",
|
"integrity": "sha512-2Xp7e92y4qZpsiTPGBSVr4gVJ9mJTLaudlo0DQxNpxJUBtoJKpxdH5xDCQDiorbkWZC2j9EId+ohhxHO/xC1QQ==",
|
||||||
"license": "MIT or Apache2"
|
"license": "MIT or Apache2"
|
||||||
},
|
},
|
||||||
"node_modules/@webassemblyjs/ast": {
|
"node_modules/@webassemblyjs/ast": {
|
||||||
@ -37468,7 +37468,7 @@
|
|||||||
"@noble/hashes": "^1.2.0",
|
"@noble/hashes": "^1.2.0",
|
||||||
"@waku/core": "^0.0.39",
|
"@waku/core": "^0.0.39",
|
||||||
"@waku/utils": "^0.0.27",
|
"@waku/utils": "^0.0.27",
|
||||||
"@waku/zerokit-rln-wasm": "^0.0.13",
|
"@waku/zerokit-rln-wasm": "^0.2.1",
|
||||||
"chai": "^5.1.2",
|
"chai": "^5.1.2",
|
||||||
"chai-as-promised": "^8.0.1",
|
"chai-as-promised": "^8.0.1",
|
||||||
"chai-spies": "^1.1.0",
|
"chai-spies": "^1.1.0",
|
||||||
|
|||||||
@ -69,7 +69,8 @@ test.describe("waku", () => {
|
|||||||
console.log("Debug:", debug);
|
console.log("Debug:", debug);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can dial peers", async ({ page }) => {
|
// TODO: https://github.com/waku-org/js-waku/issues/2619
|
||||||
|
test.skip("can dial peers", async ({ page }) => {
|
||||||
const result = await page.evaluate((peerAddrs) => {
|
const result = await page.evaluate((peerAddrs) => {
|
||||||
return window.wakuAPI.dialPeers(window.waku, peerAddrs);
|
return window.wakuAPI.dialPeers(window.waku, peerAddrs);
|
||||||
}, ACTIVE_PEERS);
|
}, ACTIVE_PEERS);
|
||||||
|
|||||||
@ -79,7 +79,7 @@
|
|||||||
"@waku/core": "^0.0.39",
|
"@waku/core": "^0.0.39",
|
||||||
"@waku/utils": "^0.0.27",
|
"@waku/utils": "^0.0.27",
|
||||||
"@noble/hashes": "^1.2.0",
|
"@noble/hashes": "^1.2.0",
|
||||||
"@waku/zerokit-rln-wasm": "^0.0.13",
|
"@waku/zerokit-rln-wasm": "^0.2.1",
|
||||||
"ethereum-cryptography": "^3.1.0",
|
"ethereum-cryptography": "^3.1.0",
|
||||||
"ethers": "^5.7.2",
|
"ethers": "^5.7.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|||||||
@ -1,363 +0,0 @@
|
|||||||
import { createDecoder, createEncoder } from "@waku/core/lib/message/version_0";
|
|
||||||
import { IDecodedMessage } 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 {
|
|
||||||
createTestMetaSetter,
|
|
||||||
createTestRLNCodecSetup,
|
|
||||||
EMPTY_PROTO_MESSAGE,
|
|
||||||
TEST_CONSTANTS,
|
|
||||||
verifyRLNMessage
|
|
||||||
} from "./codec.test-utils.js";
|
|
||||||
import { RlnMessage } from "./message.js";
|
|
||||||
import { epochBytesToInt } from "./utils/index.js";
|
|
||||||
|
|
||||||
describe("RLN codec with version 0", () => {
|
|
||||||
it("toWire", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
|
|
||||||
const rlnEncoder = createRLNEncoder({
|
|
||||||
encoder: createEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
});
|
|
||||||
const rlnDecoder = createRLNDecoder({
|
|
||||||
rlnInstance,
|
|
||||||
decoder: createDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
protoResult!
|
|
||||||
))!;
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("toProtoObj", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createDecoder(TEST_CONSTANTS.contentTopic, TEST_CONSTANTS.routingInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const proto = await rlnEncoder.toProtoObj({ payload });
|
|
||||||
|
|
||||||
expect(proto).to.not.be.undefined;
|
|
||||||
const msg = (await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
proto!
|
|
||||||
)) as RlnMessage<IDecodedMessage>;
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("RLN codec with version 1", () => {
|
|
||||||
it("Symmetric, toWire", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const symKey = generateSymmetricKey();
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createSymEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
symKey
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createSymDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo,
|
|
||||||
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(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
protoResult!
|
|
||||||
))!;
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Symmetric, toProtoObj", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const symKey = generateSymmetricKey();
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createSymEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
symKey
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createSymDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo,
|
|
||||||
symKey
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const proto = await rlnEncoder.toProtoObj({ payload });
|
|
||||||
|
|
||||||
expect(proto).to.not.be.undefined;
|
|
||||||
const msg = await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
proto!
|
|
||||||
);
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Asymmetric, toWire", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const privateKey = generatePrivateKey();
|
|
||||||
const publicKey = getPublicKey(privateKey);
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createAsymEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
publicKey
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createAsymDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo,
|
|
||||||
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(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
protoResult!
|
|
||||||
))!;
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Asymmetric, toProtoObj", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const privateKey = generatePrivateKey();
|
|
||||||
const publicKey = getPublicKey(privateKey);
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createAsymEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
publicKey
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createAsymDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo,
|
|
||||||
privateKey
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const proto = await rlnEncoder.toProtoObj({ payload });
|
|
||||||
|
|
||||||
expect(proto).to.not.be.undefined;
|
|
||||||
const msg = await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
proto!
|
|
||||||
);
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("RLN Codec - epoch", () => {
|
|
||||||
it("toProtoObj", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createDecoder(TEST_CONSTANTS.contentTopic, TEST_CONSTANTS.routingInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const proto = await rlnEncoder.toProtoObj({ payload });
|
|
||||||
|
|
||||||
expect(proto).to.not.be.undefined;
|
|
||||||
const msg = (await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
proto!
|
|
||||||
)) as RlnMessage<IDecodedMessage>;
|
|
||||||
|
|
||||||
const epochBytes = proto!.rateLimitProof!.epoch;
|
|
||||||
const epoch = epochBytesToInt(epochBytes);
|
|
||||||
|
|
||||||
expect(msg.epoch!.toString(10).length).to.eq(9);
|
|
||||||
expect(msg.epoch).to.eq(epoch);
|
|
||||||
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("RLN codec with version 0 and meta setter", () => {
|
|
||||||
it("toWire", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const metaSetter = createTestMetaSetter();
|
|
||||||
|
|
||||||
const rlnEncoder = createRLNEncoder({
|
|
||||||
encoder: createEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
metaSetter
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
});
|
|
||||||
const rlnDecoder = createRLNDecoder({
|
|
||||||
rlnInstance,
|
|
||||||
decoder: createDecoder(
|
|
||||||
TEST_CONSTANTS.contentTopic,
|
|
||||||
TEST_CONSTANTS.routingInfo
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
protoResult!
|
|
||||||
))!;
|
|
||||||
|
|
||||||
const expectedMeta = metaSetter({
|
|
||||||
...EMPTY_PROTO_MESSAGE,
|
|
||||||
payload: protoResult!.payload
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(msg!.meta).to.deep.eq(expectedMeta);
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("toProtoObj", async function () {
|
|
||||||
const { rlnInstance, credential, index, payload } =
|
|
||||||
await createTestRLNCodecSetup();
|
|
||||||
const metaSetter = createTestMetaSetter();
|
|
||||||
|
|
||||||
const rlnEncoder = new RLNEncoder(
|
|
||||||
createEncoder({
|
|
||||||
contentTopic: TEST_CONSTANTS.contentTopic,
|
|
||||||
routingInfo: TEST_CONSTANTS.routingInfo,
|
|
||||||
metaSetter
|
|
||||||
}),
|
|
||||||
rlnInstance,
|
|
||||||
index,
|
|
||||||
credential
|
|
||||||
);
|
|
||||||
const rlnDecoder = new RLNDecoder(
|
|
||||||
rlnInstance,
|
|
||||||
createDecoder(TEST_CONSTANTS.contentTopic, TEST_CONSTANTS.routingInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const proto = await rlnEncoder.toProtoObj({ payload });
|
|
||||||
|
|
||||||
expect(proto).to.not.be.undefined;
|
|
||||||
const msg = (await rlnDecoder.fromProtoObj(
|
|
||||||
TEST_CONSTANTS.emptyPubsubTopic,
|
|
||||||
proto!
|
|
||||||
)) as RlnMessage<IDecodedMessage>;
|
|
||||||
|
|
||||||
const expectedMeta = metaSetter({
|
|
||||||
...EMPTY_PROTO_MESSAGE,
|
|
||||||
payload: msg!.payload
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(msg!.meta).to.deep.eq(expectedMeta);
|
|
||||||
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
import type { IProtoMessage } from "@waku/interfaces";
|
|
||||||
import { createRoutingInfo } from "@waku/utils";
|
|
||||||
import { expect } from "chai";
|
|
||||||
|
|
||||||
import { createRLN } from "./create.js";
|
|
||||||
import type { IdentityCredential } from "./identity.js";
|
|
||||||
|
|
||||||
export interface TestRLNCodecSetup {
|
|
||||||
rlnInstance: any;
|
|
||||||
credential: IdentityCredential;
|
|
||||||
index: number;
|
|
||||||
payload: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TEST_CONSTANTS = {
|
|
||||||
contentTopic: "/test/1/waku-message/utf8",
|
|
||||||
emptyPubsubTopic: "",
|
|
||||||
defaultIndex: 0,
|
|
||||||
defaultPayload: new Uint8Array([1, 2, 3, 4, 5]),
|
|
||||||
routingInfo: createRoutingInfo(
|
|
||||||
{
|
|
||||||
clusterId: 0,
|
|
||||||
numShardsInCluster: 2
|
|
||||||
},
|
|
||||||
{ contentTopic: "/test/1/waku-message/utf8" }
|
|
||||||
)
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const EMPTY_PROTO_MESSAGE = {
|
|
||||||
timestamp: undefined,
|
|
||||||
contentTopic: "",
|
|
||||||
ephemeral: undefined,
|
|
||||||
meta: undefined,
|
|
||||||
rateLimitProof: undefined,
|
|
||||||
version: undefined
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a basic RLN setup for codec tests
|
|
||||||
*/
|
|
||||||
export async function createTestRLNCodecSetup(): Promise<TestRLNCodecSetup> {
|
|
||||||
const rlnInstance = await createRLN();
|
|
||||||
const credential = rlnInstance.zerokit.generateIdentityCredentials();
|
|
||||||
rlnInstance.zerokit.insertMember(credential.IDCommitment);
|
|
||||||
|
|
||||||
return {
|
|
||||||
rlnInstance,
|
|
||||||
credential,
|
|
||||||
index: TEST_CONSTANTS.defaultIndex,
|
|
||||||
payload: TEST_CONSTANTS.defaultPayload
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a meta setter function for testing
|
|
||||||
*/
|
|
||||||
export function createTestMetaSetter(): (
|
|
||||||
msg: IProtoMessage & { meta: undefined }
|
|
||||||
) => Uint8Array {
|
|
||||||
return (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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies common RLN message properties
|
|
||||||
*/
|
|
||||||
export function verifyRLNMessage(
|
|
||||||
msg: any,
|
|
||||||
payload: Uint8Array,
|
|
||||||
contentTopic: string,
|
|
||||||
version: number,
|
|
||||||
rlnInstance: any
|
|
||||||
): void {
|
|
||||||
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(contentTopic);
|
|
||||||
expect(msg.msg.version).to.eq(version);
|
|
||||||
expect(msg.payload).to.deep.eq(payload);
|
|
||||||
expect(msg.timestamp).to.not.be.undefined;
|
|
||||||
}
|
|
||||||
@ -1,138 +0,0 @@
|
|||||||
import type {
|
|
||||||
IDecodedMessage,
|
|
||||||
IDecoder,
|
|
||||||
IEncoder,
|
|
||||||
IMessage,
|
|
||||||
IProtoMessage,
|
|
||||||
IRateLimitProof,
|
|
||||||
IRoutingInfo
|
|
||||||
} 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("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);
|
|
||||||
return this.rlnInstance.zerokit.generateRLNProof(
|
|
||||||
signal,
|
|
||||||
this.index,
|
|
||||||
message.timestamp,
|
|
||||||
this.idSecretHash
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get pubsubTopic(): string {
|
|
||||||
return this.encoder.pubsubTopic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get routingInfo(): IRoutingInfo {
|
|
||||||
return this.encoder.routingInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
@ -19,26 +19,16 @@ export const PRICE_CALCULATOR_CONTRACT = {
|
|||||||
* @see https://github.com/waku-org/specs/blob/master/standards/core/rln-contract.md#implementation-suggestions
|
* @see https://github.com/waku-org/specs/blob/master/standards/core/rln-contract.md#implementation-suggestions
|
||||||
*/
|
*/
|
||||||
export const RATE_LIMIT_TIERS = {
|
export const RATE_LIMIT_TIERS = {
|
||||||
LOW: 20, // Suggested minimum rate - 20 messages per epoch
|
STANDARD: 300,
|
||||||
MEDIUM: 200,
|
MAX: 600
|
||||||
HIGH: 600 // Suggested maximum rate - 600 messages per epoch
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Global rate limit parameters
|
// Global rate limit parameters
|
||||||
export const RATE_LIMIT_PARAMS = {
|
export const RATE_LIMIT_PARAMS = {
|
||||||
MIN_RATE: RATE_LIMIT_TIERS.LOW,
|
MIN_RATE: RATE_LIMIT_TIERS.STANDARD,
|
||||||
MAX_RATE: RATE_LIMIT_TIERS.HIGH,
|
MAX_RATE: RATE_LIMIT_TIERS.MAX,
|
||||||
MAX_TOTAL_RATE: 160_000, // Maximum total rate limit across all memberships
|
MAX_TOTAL_RATE: 160_000,
|
||||||
EPOCH_LENGTH: 600 // Epoch length in seconds (10 minutes)
|
EPOCH_LENGTH: 600
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Default Q value for the RLN contract
|
|
||||||
* This is the upper bound for the ID commitment
|
|
||||||
* @see https://github.com/waku-org/specs/blob/master/standards/core/rln-contract.md#implementation-suggestions
|
|
||||||
*/
|
|
||||||
export const RLN_Q = BigInt(
|
|
||||||
"21888242871839275222246405745257275088548364400416034343698204186575808495617"
|
|
||||||
);
|
|
||||||
|
|
||||||
export const DEFAULT_RATE_LIMIT = RATE_LIMIT_PARAMS.MAX_RATE;
|
export const DEFAULT_RATE_LIMIT = RATE_LIMIT_PARAMS.MAX_RATE;
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
export { RLNContract } from "./rln_contract.js";
|
|
||||||
export * from "./constants.js";
|
export * from "./constants.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { ethers } from "ethers";
|
|||||||
|
|
||||||
import { IdentityCredential } from "../identity.js";
|
import { IdentityCredential } from "../identity.js";
|
||||||
import { DecryptedCredentials } from "../keystore/types.js";
|
import { DecryptedCredentials } from "../keystore/types.js";
|
||||||
import { BytesUtils } from "../utils/bytes.js";
|
|
||||||
|
|
||||||
import { RLN_ABI } from "./abi/rln.js";
|
import { RLN_ABI } from "./abi/rln.js";
|
||||||
import {
|
import {
|
||||||
@ -632,7 +631,7 @@ export class RLNBaseContract {
|
|||||||
permit.v,
|
permit.v,
|
||||||
permit.r,
|
permit.r,
|
||||||
permit.s,
|
permit.s,
|
||||||
BytesUtils.buildBigIntFromUint8ArrayBE(identity.IDCommitment),
|
identity.IDCommitmentBigInt,
|
||||||
this.rateLimit,
|
this.rateLimit,
|
||||||
idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
|
idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,90 +0,0 @@
|
|||||||
import { hexToBytes } from "@waku/utils/bytes";
|
|
||||||
import { expect, use } from "chai";
|
|
||||||
import chaiAsPromised from "chai-as-promised";
|
|
||||||
import * as ethers from "ethers";
|
|
||||||
import sinon, { SinonSandbox } from "sinon";
|
|
||||||
|
|
||||||
import { createTestRLNInstance, initializeRLNContract } from "./test_setup.js";
|
|
||||||
import {
|
|
||||||
createMockRegistryContract,
|
|
||||||
createRegisterStub,
|
|
||||||
mockRLNRegisteredEvent,
|
|
||||||
verifyRegistration
|
|
||||||
} from "./test_utils.js";
|
|
||||||
|
|
||||||
use(chaiAsPromised);
|
|
||||||
|
|
||||||
describe("RLN Contract abstraction - RLN", () => {
|
|
||||||
let sandbox: SinonSandbox;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
sandbox = sinon.createSandbox();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Member Registration", () => {
|
|
||||||
it("should fetch members from events and store them in the RLN instance", async () => {
|
|
||||||
const { rlnInstance, insertMemberSpy } = await createTestRLNInstance();
|
|
||||||
const membershipRegisteredEvent = mockRLNRegisteredEvent();
|
|
||||||
const queryFilterStub = sinon.stub().returns([membershipRegisteredEvent]);
|
|
||||||
|
|
||||||
const mockedRegistryContract = createMockRegistryContract({
|
|
||||||
queryFilter: queryFilterStub
|
|
||||||
});
|
|
||||||
|
|
||||||
const rlnContract = await initializeRLNContract(
|
|
||||||
rlnInstance,
|
|
||||||
mockedRegistryContract
|
|
||||||
);
|
|
||||||
|
|
||||||
await rlnContract.fetchMembers({
|
|
||||||
fromBlock: 0,
|
|
||||||
fetchRange: 1000,
|
|
||||||
fetchChunks: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
insertMemberSpy.calledWith(
|
|
||||||
ethers.utils.zeroPad(
|
|
||||||
hexToBytes(membershipRegisteredEvent.args!.idCommitment),
|
|
||||||
32
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).to.be.true;
|
|
||||||
expect(queryFilterStub.called).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should register a member", async () => {
|
|
||||||
const { rlnInstance, identity, insertMemberSpy } =
|
|
||||||
await createTestRLNInstance();
|
|
||||||
|
|
||||||
const registerStub = createRegisterStub(identity);
|
|
||||||
const mockedRegistryContract = createMockRegistryContract({
|
|
||||||
register: registerStub,
|
|
||||||
queryFilter: () => []
|
|
||||||
});
|
|
||||||
|
|
||||||
const rlnContract = await initializeRLNContract(
|
|
||||||
rlnInstance,
|
|
||||||
mockedRegistryContract
|
|
||||||
);
|
|
||||||
|
|
||||||
const decryptedCredentials =
|
|
||||||
await rlnContract.registerWithIdentity(identity);
|
|
||||||
|
|
||||||
if (!decryptedCredentials) {
|
|
||||||
throw new Error("Failed to retrieve credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyRegistration(
|
|
||||||
decryptedCredentials,
|
|
||||||
identity,
|
|
||||||
registerStub,
|
|
||||||
insertMemberSpy
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
import { Logger } from "@waku/utils";
|
|
||||||
import { hexToBytes } from "@waku/utils/bytes";
|
|
||||||
import { ethers } from "ethers";
|
|
||||||
|
|
||||||
import type { RLNInstance } from "../rln.js";
|
|
||||||
import { MerkleRootTracker } from "../root_tracker.js";
|
|
||||||
import { BytesUtils } from "../utils/bytes.js";
|
|
||||||
|
|
||||||
import { RLNBaseContract } from "./rln_base_contract.js";
|
|
||||||
import { RLNContractInitOptions } from "./types.js";
|
|
||||||
|
|
||||||
const log = new Logger("rln:contract");
|
|
||||||
|
|
||||||
export class RLNContract extends RLNBaseContract {
|
|
||||||
private instance: RLNInstance;
|
|
||||||
private merkleRootTracker: MerkleRootTracker;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous initializer for RLNContract.
|
|
||||||
* Allows injecting a mocked contract for testing purposes.
|
|
||||||
*/
|
|
||||||
public static async init(
|
|
||||||
rlnInstance: RLNInstance,
|
|
||||||
options: RLNContractInitOptions
|
|
||||||
): Promise<RLNContract> {
|
|
||||||
const rlnContract = new RLNContract(rlnInstance, options);
|
|
||||||
|
|
||||||
return rlnContract;
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(
|
|
||||||
rlnInstance: RLNInstance,
|
|
||||||
options: RLNContractInitOptions
|
|
||||||
) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
this.instance = rlnInstance;
|
|
||||||
|
|
||||||
const initialRoot = rlnInstance.zerokit.getMerkleRoot();
|
|
||||||
this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override processEvents(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.event === "MembershipErased" ||
|
|
||||||
evt.event === "MembershipExpired"
|
|
||||||
) {
|
|
||||||
let index = evt.args.index;
|
|
||||||
|
|
||||||
if (!index) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof index === "number" || typeof index === "string") {
|
|
||||||
index = ethers.BigNumber.from(index);
|
|
||||||
} else {
|
|
||||||
log.error("Index is not a number or string", {
|
|
||||||
index,
|
|
||||||
event: evt
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 if (evt.event === "MembershipRegistered") {
|
|
||||||
let eventsPerBlock = toInsertTable.get(evt.blockNumber);
|
|
||||||
if (eventsPerBlock == undefined) {
|
|
||||||
eventsPerBlock = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
eventsPerBlock.push(evt);
|
|
||||||
toInsertTable.set(evt.blockNumber, eventsPerBlock);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.removeMembers(this.instance, toRemoveTable);
|
|
||||||
this.insertMembers(this.instance, toInsertTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
private insertMembers(
|
|
||||||
rlnInstance: RLNInstance,
|
|
||||||
toInsert: Map<number, ethers.Event[]>
|
|
||||||
): void {
|
|
||||||
toInsert.forEach((events: ethers.Event[], blockNumber: number) => {
|
|
||||||
events.forEach((evt) => {
|
|
||||||
if (!evt.args) return;
|
|
||||||
|
|
||||||
const _idCommitment = evt.args.idCommitment as string;
|
|
||||||
let index = evt.args.index;
|
|
||||||
|
|
||||||
if (!_idCommitment || !index) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof index === "number" || typeof index === "string") {
|
|
||||||
index = ethers.BigNumber.from(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
const idCommitment = BytesUtils.zeroPadLE(
|
|
||||||
hexToBytes(_idCommitment),
|
|
||||||
32
|
|
||||||
);
|
|
||||||
rlnInstance.zerokit.insertMember(idCommitment);
|
|
||||||
|
|
||||||
const numericIndex = index.toNumber();
|
|
||||||
this._members.set(numericIndex, {
|
|
||||||
index,
|
|
||||||
idCommitment: _idCommitment
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentRoot = rlnInstance.zerokit.getMerkleRoot();
|
|
||||||
this.merkleRootTracker.pushRoot(blockNumber, currentRoot);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeMembers(
|
|
||||||
rlnInstance: RLNInstance,
|
|
||||||
toRemove: Map<number, number[]>
|
|
||||||
): void {
|
|
||||||
const removeDescending = new Map([...toRemove].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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
import { hexToBytes } from "@waku/utils/bytes";
|
|
||||||
import { ethers } from "ethers";
|
|
||||||
import sinon from "sinon";
|
|
||||||
|
|
||||||
import { createRLN } from "../create.js";
|
|
||||||
import type { IdentityCredential } from "../identity.js";
|
|
||||||
|
|
||||||
import { DEFAULT_RATE_LIMIT, RLN_CONTRACT } from "./constants.js";
|
|
||||||
import { RLNContract } from "./rln_contract.js";
|
|
||||||
|
|
||||||
export interface TestRLNInstance {
|
|
||||||
rlnInstance: any;
|
|
||||||
identity: IdentityCredential;
|
|
||||||
insertMemberSpy: sinon.SinonStub;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a test RLN instance with basic setup
|
|
||||||
*/
|
|
||||||
export async function createTestRLNInstance(): Promise<TestRLNInstance> {
|
|
||||||
const rlnInstance = await createRLN();
|
|
||||||
const insertMemberSpy = sinon.stub();
|
|
||||||
rlnInstance.zerokit.insertMember = insertMemberSpy;
|
|
||||||
|
|
||||||
const mockSignature =
|
|
||||||
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c";
|
|
||||||
const identity =
|
|
||||||
rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature);
|
|
||||||
|
|
||||||
return {
|
|
||||||
rlnInstance,
|
|
||||||
identity,
|
|
||||||
insertMemberSpy
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes an RLN contract with the given registry contract
|
|
||||||
*/
|
|
||||||
export async function initializeRLNContract(
|
|
||||||
rlnInstance: any,
|
|
||||||
mockedRegistryContract: ethers.Contract
|
|
||||||
): Promise<RLNContract> {
|
|
||||||
const provider = new ethers.providers.JsonRpcProvider();
|
|
||||||
const voidSigner = new ethers.VoidSigner(RLN_CONTRACT.address, provider);
|
|
||||||
|
|
||||||
const originalRegister = mockedRegistryContract.register;
|
|
||||||
(mockedRegistryContract as any).register = function (...args: any[]) {
|
|
||||||
const result = originalRegister.apply(this, args);
|
|
||||||
|
|
||||||
if (args[0] && rlnInstance.zerokit) {
|
|
||||||
const idCommitmentBigInt = args[0];
|
|
||||||
const idCommitmentHex =
|
|
||||||
"0x" + idCommitmentBigInt.toString(16).padStart(64, "0");
|
|
||||||
const idCommitment = ethers.utils.zeroPad(
|
|
||||||
hexToBytes(idCommitmentHex),
|
|
||||||
32
|
|
||||||
);
|
|
||||||
rlnInstance.zerokit.insertMember(idCommitment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const contract = await RLNContract.init(rlnInstance, {
|
|
||||||
address: RLN_CONTRACT.address,
|
|
||||||
signer: voidSigner,
|
|
||||||
rateLimit: DEFAULT_RATE_LIMIT,
|
|
||||||
contract: mockedRegistryContract
|
|
||||||
});
|
|
||||||
|
|
||||||
return contract;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common test message data
|
|
||||||
*/
|
|
||||||
export const TEST_DATA = {
|
|
||||||
contentTopic: "/test/1/waku-message/utf8",
|
|
||||||
emptyPubsubTopic: "",
|
|
||||||
testMessage: Uint8Array.from(
|
|
||||||
"Hello World".split("").map((x) => x.charCodeAt(0))
|
|
||||||
),
|
|
||||||
mockSignature:
|
|
||||||
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c"
|
|
||||||
};
|
|
||||||
@ -1,179 +0,0 @@
|
|||||||
import { hexToBytes } from "@waku/utils/bytes";
|
|
||||||
import { expect } from "chai";
|
|
||||||
import * as ethers from "ethers";
|
|
||||||
import sinon from "sinon";
|
|
||||||
|
|
||||||
import type { IdentityCredential } from "../identity.js";
|
|
||||||
|
|
||||||
import { DEFAULT_RATE_LIMIT, RLN_CONTRACT } from "./constants.js";
|
|
||||||
|
|
||||||
export const mockRateLimits = {
|
|
||||||
minRate: 20,
|
|
||||||
maxRate: 600,
|
|
||||||
maxTotalRate: 1200,
|
|
||||||
currentTotalRate: 500
|
|
||||||
};
|
|
||||||
|
|
||||||
type MockProvider = {
|
|
||||||
getLogs: () => never[];
|
|
||||||
getBlockNumber: () => Promise<number>;
|
|
||||||
getNetwork: () => Promise<{ chainId: number }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MockFilters = {
|
|
||||||
MembershipRegistered: () => { address: string };
|
|
||||||
MembershipErased: () => { address: string };
|
|
||||||
MembershipExpired: () => { address: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createMockProvider(): MockProvider {
|
|
||||||
return {
|
|
||||||
getLogs: () => [],
|
|
||||||
getBlockNumber: () => Promise.resolve(1000),
|
|
||||||
getNetwork: () => Promise.resolve({ chainId: 11155111 })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMockFilters(): MockFilters {
|
|
||||||
return {
|
|
||||||
MembershipRegistered: () => ({ address: RLN_CONTRACT.address }),
|
|
||||||
MembershipErased: () => ({ address: RLN_CONTRACT.address }),
|
|
||||||
MembershipExpired: () => ({ address: RLN_CONTRACT.address })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContractOverrides = Partial<{
|
|
||||||
filters: Record<string, unknown>;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export function createMockRegistryContract(
|
|
||||||
overrides: ContractOverrides = {}
|
|
||||||
): ethers.Contract {
|
|
||||||
const filters = {
|
|
||||||
MembershipRegistered: () => ({ address: RLN_CONTRACT.address }),
|
|
||||||
MembershipErased: () => ({ address: RLN_CONTRACT.address }),
|
|
||||||
MembershipExpired: () => ({ address: RLN_CONTRACT.address })
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseContract = {
|
|
||||||
minMembershipRateLimit: () =>
|
|
||||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.minRate)),
|
|
||||||
maxMembershipRateLimit: () =>
|
|
||||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.maxRate)),
|
|
||||||
maxTotalRateLimit: () =>
|
|
||||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.maxTotalRate)),
|
|
||||||
currentTotalRateLimit: () =>
|
|
||||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.currentTotalRate)),
|
|
||||||
queryFilter: () => [],
|
|
||||||
provider: createMockProvider(),
|
|
||||||
filters,
|
|
||||||
on: () => ({}),
|
|
||||||
removeAllListeners: () => ({}),
|
|
||||||
register: () => ({
|
|
||||||
wait: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
events: [mockRLNRegisteredEvent()]
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
estimateGas: {
|
|
||||||
register: () => Promise.resolve(ethers.BigNumber.from(100000))
|
|
||||||
},
|
|
||||||
functions: {
|
|
||||||
register: () => Promise.resolve()
|
|
||||||
},
|
|
||||||
getMemberIndex: () => Promise.resolve(null),
|
|
||||||
interface: {
|
|
||||||
getEvent: (eventName: string) => ({
|
|
||||||
name: eventName,
|
|
||||||
format: () => {}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
address: RLN_CONTRACT.address
|
|
||||||
};
|
|
||||||
|
|
||||||
// Merge overrides while preserving filters
|
|
||||||
const merged = {
|
|
||||||
...baseContract,
|
|
||||||
...overrides,
|
|
||||||
filters: { ...filters, ...(overrides.filters || {}) }
|
|
||||||
};
|
|
||||||
|
|
||||||
return merged as unknown as ethers.Contract;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mockRLNRegisteredEvent(idCommitment?: string): ethers.Event {
|
|
||||||
return {
|
|
||||||
args: {
|
|
||||||
idCommitment:
|
|
||||||
idCommitment ||
|
|
||||||
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
||||||
membershipRateLimit: ethers.BigNumber.from(DEFAULT_RATE_LIMIT),
|
|
||||||
index: ethers.BigNumber.from(1)
|
|
||||||
},
|
|
||||||
event: "MembershipRegistered"
|
|
||||||
} as unknown as ethers.Event;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatIdCommitment(idCommitmentBigInt: bigint): string {
|
|
||||||
return "0x" + idCommitmentBigInt.toString(16).padStart(64, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createRegisterStub(
|
|
||||||
identity: IdentityCredential
|
|
||||||
): sinon.SinonStub {
|
|
||||||
return sinon.stub().callsFake(() => ({
|
|
||||||
wait: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
events: [
|
|
||||||
{
|
|
||||||
event: "MembershipRegistered",
|
|
||||||
args: {
|
|
||||||
idCommitment: formatIdCommitment(identity.IDCommitmentBigInt),
|
|
||||||
membershipRateLimit: ethers.BigNumber.from(DEFAULT_RATE_LIMIT),
|
|
||||||
index: ethers.BigNumber.from(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyRegistration(
|
|
||||||
decryptedCredentials: any,
|
|
||||||
identity: IdentityCredential,
|
|
||||||
registerStub: sinon.SinonStub,
|
|
||||||
insertMemberSpy: sinon.SinonStub
|
|
||||||
): void {
|
|
||||||
if (!decryptedCredentials) {
|
|
||||||
throw new Error("Decrypted credentials should not be undefined");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify registration call
|
|
||||||
expect(
|
|
||||||
registerStub.calledWith(
|
|
||||||
sinon.match.same(identity.IDCommitmentBigInt),
|
|
||||||
sinon.match.same(DEFAULT_RATE_LIMIT),
|
|
||||||
sinon.match.array,
|
|
||||||
sinon.match.object
|
|
||||||
)
|
|
||||||
).to.be.true;
|
|
||||||
|
|
||||||
// Verify credential properties
|
|
||||||
expect(decryptedCredentials).to.have.property("identity");
|
|
||||||
expect(decryptedCredentials).to.have.property("membership");
|
|
||||||
expect(decryptedCredentials.membership).to.include({
|
|
||||||
address: RLN_CONTRACT.address,
|
|
||||||
treeIndex: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify member insertion
|
|
||||||
const expectedIdCommitment = ethers.utils.zeroPad(
|
|
||||||
hexToBytes(formatIdCommitment(identity.IDCommitmentBigInt)),
|
|
||||||
32
|
|
||||||
);
|
|
||||||
expect(insertMemberSpy.callCount).to.equal(1);
|
|
||||||
expect(insertMemberSpy.getCall(0).args[0]).to.deep.equal(
|
|
||||||
expectedIdCommitment
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
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]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,11 +1,8 @@
|
|||||||
import { hmac } from "@noble/hashes/hmac";
|
|
||||||
import { sha256 } from "@noble/hashes/sha2";
|
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
import { ethers } from "ethers";
|
import { ethers } from "ethers";
|
||||||
|
|
||||||
import { RLN_CONTRACT, RLN_Q } from "./contract/constants.js";
|
import { RLN_CONTRACT } from "./contract/constants.js";
|
||||||
import { RLNBaseContract } from "./contract/rln_base_contract.js";
|
import { RLNBaseContract } from "./contract/rln_base_contract.js";
|
||||||
import { IdentityCredential } from "./identity.js";
|
|
||||||
import { Keystore } from "./keystore/index.js";
|
import { Keystore } from "./keystore/index.js";
|
||||||
import type {
|
import type {
|
||||||
DecryptedCredentials,
|
DecryptedCredentials,
|
||||||
@ -13,7 +10,6 @@ import type {
|
|||||||
} from "./keystore/index.js";
|
} from "./keystore/index.js";
|
||||||
import { KeystoreEntity, Password } from "./keystore/types.js";
|
import { KeystoreEntity, Password } from "./keystore/types.js";
|
||||||
import { RegisterMembershipOptions, StartRLNOptions } from "./types.js";
|
import { RegisterMembershipOptions, StartRLNOptions } from "./types.js";
|
||||||
import { BytesUtils } from "./utils/bytes.js";
|
|
||||||
import { extractMetaMaskSigner } from "./utils/index.js";
|
import { extractMetaMaskSigner } from "./utils/index.js";
|
||||||
import { Zerokit } from "./zerokit.js";
|
import { Zerokit } from "./zerokit.js";
|
||||||
|
|
||||||
@ -21,7 +17,6 @@ const log = new Logger("rln:credentials");
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages credentials for RLN
|
* Manages credentials for RLN
|
||||||
* This is a lightweight implementation of the RLN contract that doesn't require Zerokit
|
|
||||||
* It is used to register membership and generate identity credentials
|
* It is used to register membership and generate identity credentials
|
||||||
*/
|
*/
|
||||||
export class RLNCredentialsManager {
|
export class RLNCredentialsManager {
|
||||||
@ -34,9 +29,9 @@ export class RLNCredentialsManager {
|
|||||||
protected keystore = Keystore.create();
|
protected keystore = Keystore.create();
|
||||||
public credentials: undefined | DecryptedCredentials;
|
public credentials: undefined | DecryptedCredentials;
|
||||||
|
|
||||||
public zerokit: undefined | Zerokit;
|
public zerokit: Zerokit;
|
||||||
|
|
||||||
public constructor(zerokit?: Zerokit) {
|
public constructor(zerokit: Zerokit) {
|
||||||
log.info("RLNCredentialsManager initialized");
|
log.info("RLNCredentialsManager initialized");
|
||||||
this.zerokit = zerokit;
|
this.zerokit = zerokit;
|
||||||
}
|
}
|
||||||
@ -81,7 +76,7 @@ export class RLNCredentialsManager {
|
|||||||
this.contract = await RLNBaseContract.create({
|
this.contract = await RLNBaseContract.create({
|
||||||
address: address!,
|
address: address!,
|
||||||
signer: signer!,
|
signer: signer!,
|
||||||
rateLimit: rateLimit ?? this.zerokit?.rateLimit
|
rateLimit: rateLimit ?? this.zerokit.rateLimit
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("RLNCredentialsManager successfully started");
|
log.info("RLNCredentialsManager successfully started");
|
||||||
@ -106,18 +101,10 @@ export class RLNCredentialsManager {
|
|||||||
let identity = "identity" in options && options.identity;
|
let identity = "identity" in options && options.identity;
|
||||||
|
|
||||||
if ("signature" in options) {
|
if ("signature" in options) {
|
||||||
log.info("Generating identity from signature");
|
|
||||||
if (this.zerokit) {
|
|
||||||
log.info("Using Zerokit to generate identity");
|
log.info("Using Zerokit to generate identity");
|
||||||
identity = this.zerokit.generateSeededIdentityCredential(
|
identity = this.zerokit.generateSeededIdentityCredential(
|
||||||
options.signature
|
options.signature
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
log.info("Using local implementation to generate identity");
|
|
||||||
identity = await this.generateSeededIdentityCredential(
|
|
||||||
options.signature
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
@ -242,55 +229,4 @@ export class RLNCredentialsManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates an identity credential from a seed string
|
|
||||||
* This is a pure implementation that doesn't rely on Zerokit
|
|
||||||
* @param seed A string seed to generate the identity from
|
|
||||||
* @returns IdentityCredential
|
|
||||||
*/
|
|
||||||
private async generateSeededIdentityCredential(
|
|
||||||
seed: string
|
|
||||||
): Promise<IdentityCredential> {
|
|
||||||
log.info("Generating seeded identity credential");
|
|
||||||
// Convert the seed to bytes
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const seedBytes = encoder.encode(seed);
|
|
||||||
|
|
||||||
// Generate deterministic values using HMAC-SHA256
|
|
||||||
// We use different context strings for each component to ensure they're different
|
|
||||||
const idTrapdoorBE = hmac(sha256, seedBytes, encoder.encode("IDTrapdoor"));
|
|
||||||
const idNullifierBE = hmac(
|
|
||||||
sha256,
|
|
||||||
seedBytes,
|
|
||||||
encoder.encode("IDNullifier")
|
|
||||||
);
|
|
||||||
|
|
||||||
const combinedBytes = new Uint8Array([...idTrapdoorBE, ...idNullifierBE]);
|
|
||||||
const idSecretHashBE = sha256(combinedBytes);
|
|
||||||
|
|
||||||
const idCommitmentRawBE = sha256(idSecretHashBE);
|
|
||||||
const idCommitmentBE = this.reduceIdCommitment(idCommitmentRawBE);
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Successfully generated identity credential, storing in Big Endian format"
|
|
||||||
);
|
|
||||||
return new IdentityCredential(
|
|
||||||
idTrapdoorBE,
|
|
||||||
idNullifierBE,
|
|
||||||
idSecretHashBE,
|
|
||||||
idCommitmentBE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: take 32-byte BE, reduce mod Q, return 32-byte BE
|
|
||||||
*/
|
|
||||||
private reduceIdCommitment(
|
|
||||||
bytesBE: Uint8Array,
|
|
||||||
limit: bigint = RLN_Q
|
|
||||||
): Uint8Array {
|
|
||||||
const nBE = BytesUtils.buildBigIntFromUint8ArrayBE(bytesBE);
|
|
||||||
return BytesUtils.bigIntToUint8Array32BE(nBE % limit);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,7 @@ export class IdentityCredential {
|
|||||||
public readonly IDSecretHash: Uint8Array,
|
public readonly IDSecretHash: Uint8Array,
|
||||||
public readonly IDCommitment: Uint8Array
|
public readonly IDCommitment: Uint8Array
|
||||||
) {
|
) {
|
||||||
this.IDCommitmentBigInt =
|
this.IDCommitmentBigInt = BytesUtils.toBigInt(IDCommitment);
|
||||||
BytesUtils.buildBigIntFromUint8ArrayBE(IDCommitment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromBytes(memKeys: Uint8Array): IdentityCredential {
|
public static fromBytes(memKeys: Uint8Array): IdentityCredential {
|
||||||
|
|||||||
@ -1,28 +1,18 @@
|
|||||||
import { RLNDecoder, RLNEncoder } from "./codec.js";
|
|
||||||
import { RLN_ABI } from "./contract/abi/rln.js";
|
import { RLN_ABI } from "./contract/abi/rln.js";
|
||||||
import { RLN_CONTRACT, RLNContract } from "./contract/index.js";
|
import { RLN_CONTRACT } from "./contract/index.js";
|
||||||
import { RLNBaseContract } from "./contract/rln_base_contract.js";
|
import { RLNBaseContract } from "./contract/rln_base_contract.js";
|
||||||
import { createRLN } from "./create.js";
|
import { createRLN } from "./create.js";
|
||||||
import { RLNCredentialsManager } from "./credentials_manager.js";
|
|
||||||
import { IdentityCredential } from "./identity.js";
|
import { IdentityCredential } from "./identity.js";
|
||||||
import { Keystore } from "./keystore/index.js";
|
import { Keystore } from "./keystore/index.js";
|
||||||
import { Proof } from "./proof.js";
|
|
||||||
import { RLNInstance } from "./rln.js";
|
import { RLNInstance } from "./rln.js";
|
||||||
import { MerkleRootTracker } from "./root_tracker.js";
|
|
||||||
import { extractMetaMaskSigner } from "./utils/index.js";
|
import { extractMetaMaskSigner } from "./utils/index.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
RLNCredentialsManager,
|
|
||||||
RLNBaseContract,
|
RLNBaseContract,
|
||||||
createRLN,
|
createRLN,
|
||||||
Keystore,
|
Keystore,
|
||||||
RLNInstance,
|
RLNInstance,
|
||||||
IdentityCredential,
|
IdentityCredential,
|
||||||
Proof,
|
|
||||||
RLNEncoder,
|
|
||||||
RLNDecoder,
|
|
||||||
MerkleRootTracker,
|
|
||||||
RLNContract,
|
|
||||||
RLN_CONTRACT,
|
RLN_CONTRACT,
|
||||||
extractMetaMaskSigner,
|
extractMetaMaskSigner,
|
||||||
RLN_ABI
|
RLN_ABI
|
||||||
|
|||||||
@ -222,9 +222,7 @@ describe("Keystore", () => {
|
|||||||
])
|
])
|
||||||
} as unknown as IdentityCredential;
|
} as unknown as IdentityCredential;
|
||||||
// Add the missing property for test correctness
|
// Add the missing property for test correctness
|
||||||
identity.IDCommitmentBigInt = BytesUtils.buildBigIntFromUint8ArrayBE(
|
identity.IDCommitmentBigInt = BytesUtils.toBigInt(identity.IDCommitment);
|
||||||
identity.IDCommitment
|
|
||||||
);
|
|
||||||
const membership = {
|
const membership = {
|
||||||
chainId: "0xAA36A7",
|
chainId: "0xAA36A7",
|
||||||
treeIndex: 8,
|
treeIndex: 8,
|
||||||
@ -276,9 +274,7 @@ describe("Keystore", () => {
|
|||||||
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15
|
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15
|
||||||
]
|
]
|
||||||
} as unknown as IdentityCredential;
|
} as unknown as IdentityCredential;
|
||||||
identity.IDCommitmentBigInt = BytesUtils.buildBigIntFromUint8ArrayBE(
|
identity.IDCommitmentBigInt = BytesUtils.toBigInt(identity.IDCommitment);
|
||||||
identity.IDCommitment
|
|
||||||
);
|
|
||||||
const membership = {
|
const membership = {
|
||||||
chainId: "0xAA36A7",
|
chainId: "0xAA36A7",
|
||||||
treeIndex: 8,
|
treeIndex: 8,
|
||||||
|
|||||||
@ -264,20 +264,14 @@ export class Keystore {
|
|||||||
_.get(obj, "identityCredential.idSecretHash", [])
|
_.get(obj, "identityCredential.idSecretHash", [])
|
||||||
);
|
);
|
||||||
|
|
||||||
// Big Endian
|
const idCommitmentBigInt = BytesUtils.toBigInt(idCommitmentLE);
|
||||||
const idCommitmentBE = BytesUtils.switchEndianness(idCommitmentLE);
|
|
||||||
const idTrapdoorBE = BytesUtils.switchEndianness(idTrapdoorLE);
|
|
||||||
const idNullifierBE = BytesUtils.switchEndianness(idNullifierLE);
|
|
||||||
const idSecretHashBE = BytesUtils.switchEndianness(idSecretHashLE);
|
|
||||||
const idCommitmentBigInt =
|
|
||||||
BytesUtils.buildBigIntFromUint8ArrayBE(idCommitmentBE);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identity: {
|
identity: {
|
||||||
IDCommitment: idCommitmentBE,
|
IDCommitment: idCommitmentLE,
|
||||||
IDTrapdoor: idTrapdoorBE,
|
IDTrapdoor: idTrapdoorLE,
|
||||||
IDNullifier: idNullifierBE,
|
IDNullifier: idNullifierLE,
|
||||||
IDSecretHash: idSecretHashBE,
|
IDSecretHash: idSecretHashLE,
|
||||||
IDCommitmentBigInt: idCommitmentBigInt
|
IDCommitmentBigInt: idCommitmentBigInt
|
||||||
},
|
},
|
||||||
membership: {
|
membership: {
|
||||||
@ -329,35 +323,18 @@ export class Keystore {
|
|||||||
|
|
||||||
// follows nwaku implementation
|
// follows nwaku implementation
|
||||||
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L98
|
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L98
|
||||||
// IdentityCredential is stored in Big Endian format => switch to Little Endian
|
|
||||||
private static fromIdentityToBytes(options: KeystoreEntity): Uint8Array {
|
private static fromIdentityToBytes(options: KeystoreEntity): Uint8Array {
|
||||||
const { IDCommitment, IDNullifier, IDSecretHash, IDTrapdoor } =
|
const { IDCommitment, IDNullifier, IDSecretHash, IDTrapdoor } =
|
||||||
options.identity;
|
options.identity;
|
||||||
const idCommitmentLE = BytesUtils.switchEndianness(IDCommitment);
|
|
||||||
const idNullifierLE = BytesUtils.switchEndianness(IDNullifier);
|
|
||||||
const idSecretHashLE = BytesUtils.switchEndianness(IDSecretHash);
|
|
||||||
const idTrapdoorLE = BytesUtils.switchEndianness(IDTrapdoor);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log({
|
|
||||||
idCommitmentBE: IDCommitment,
|
|
||||||
idCommitmentLE,
|
|
||||||
idNullifierBE: IDNullifier,
|
|
||||||
idNullifierLE,
|
|
||||||
idSecretHashBE: IDSecretHash,
|
|
||||||
idSecretHashLE,
|
|
||||||
idTrapdoorBE: IDTrapdoor,
|
|
||||||
idTrapdoorLE
|
|
||||||
});
|
|
||||||
|
|
||||||
return utf8ToBytes(
|
return utf8ToBytes(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
treeIndex: options.membership.treeIndex,
|
treeIndex: options.membership.treeIndex,
|
||||||
identityCredential: {
|
identityCredential: {
|
||||||
idCommitment: Array.from(idCommitmentLE),
|
idCommitment: Array.from(IDCommitment),
|
||||||
idNullifier: Array.from(idNullifierLE),
|
idNullifier: Array.from(IDNullifier),
|
||||||
idSecretHash: Array.from(idSecretHashLE),
|
idSecretHash: Array.from(IDSecretHash),
|
||||||
idTrapdoor: Array.from(idTrapdoorLE)
|
idTrapdoor: Array.from(IDTrapdoor)
|
||||||
},
|
},
|
||||||
membershipContract: {
|
membershipContract: {
|
||||||
chainId: options.membership.chainId,
|
chainId: options.membership.chainId,
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
import { message } from "@waku/core";
|
|
||||||
import type {
|
|
||||||
IDecodedMessage,
|
|
||||||
IMessage,
|
|
||||||
IRateLimitProof,
|
|
||||||
IRlnMessage
|
|
||||||
} 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 IRlnMessage {
|
|
||||||
public pubsubTopic = "";
|
|
||||||
public version = message.version_0.Version;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
private rlnInstance: RLNInstance,
|
|
||||||
private 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 hash(): Uint8Array {
|
|
||||||
return this.msg.hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hashStr(): string {
|
|
||||||
return this.msg.hashStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
import type { IRateLimitProof } from "@waku/interfaces";
|
|
||||||
|
|
||||||
import { BytesUtils, 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 BytesUtils.concatenate(
|
|
||||||
p.proof,
|
|
||||||
p.merkleRoot,
|
|
||||||
p.epoch,
|
|
||||||
p.shareX,
|
|
||||||
p.shareY,
|
|
||||||
p.nullifier,
|
|
||||||
p.rlnIdentifier
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
13
packages/rln/src/resources/verification_key.d.ts
vendored
13
packages/rln/src/resources/verification_key.d.ts
vendored
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,11 +1,25 @@
|
|||||||
export async function builder(
|
export const builder: (
|
||||||
code: Uint8Array,
|
code: Uint8Array,
|
||||||
sanityCheck: boolean
|
sanityCheck?: boolean
|
||||||
): Promise<WitnessCalculator>;
|
) => Promise<WitnessCalculator>;
|
||||||
|
|
||||||
export class WitnessCalculator {
|
export class WitnessCalculator {
|
||||||
public calculateWitness(
|
constructor(instance: any, sanityCheck?: boolean);
|
||||||
input: unknown,
|
|
||||||
sanityCheck: boolean
|
circom_version(): number;
|
||||||
): Promise<Array<bigint>>;
|
|
||||||
|
calculateWitness(
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
sanityCheck?: boolean
|
||||||
|
): Promise<bigint[]>;
|
||||||
|
|
||||||
|
calculateBinWitness(
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
sanityCheck?: boolean
|
||||||
|
): Promise<Uint8Array>;
|
||||||
|
|
||||||
|
calculateWTNSBin(
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
sanityCheck?: boolean
|
||||||
|
): Promise<Uint8Array>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// File generated with https://github.com/iden3/circom
|
// File generated with https://github.com/iden3/circom
|
||||||
// following the instructions from:
|
// following the instructions from:
|
||||||
// https://github.com/vacp2p/zerokit/tree/master/rln#compiling-circuits
|
// https://github.com/vacp2p/zerokit/tree/master/rln#advanced-custom-circuit-compilation
|
||||||
|
|
||||||
export async function builder(code, options) {
|
export async function builder(code, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|||||||
@ -1,37 +1,14 @@
|
|||||||
import { createDecoder, createEncoder } from "@waku/core";
|
|
||||||
import type {
|
|
||||||
ContentTopic,
|
|
||||||
IDecodedMessage,
|
|
||||||
IRoutingInfo,
|
|
||||||
EncoderOptions as WakuEncoderOptions
|
|
||||||
} from "@waku/interfaces";
|
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
import init from "@waku/zerokit-rln-wasm";
|
import init, * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
||||||
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
|
||||||
|
|
||||||
import {
|
|
||||||
createRLNDecoder,
|
|
||||||
createRLNEncoder,
|
|
||||||
type RLNDecoder,
|
|
||||||
type RLNEncoder
|
|
||||||
} from "./codec.js";
|
|
||||||
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
|
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
|
||||||
import { RLNCredentialsManager } from "./credentials_manager.js";
|
import { RLNCredentialsManager } from "./credentials_manager.js";
|
||||||
import type {
|
|
||||||
DecryptedCredentials,
|
|
||||||
EncryptedCredentials
|
|
||||||
} from "./keystore/index.js";
|
|
||||||
import verificationKey from "./resources/verification_key";
|
|
||||||
import * as wc from "./resources/witness_calculator";
|
import * as wc from "./resources/witness_calculator";
|
||||||
import { WitnessCalculator } from "./resources/witness_calculator";
|
import { WitnessCalculator } from "./resources/witness_calculator";
|
||||||
import { Zerokit } from "./zerokit.js";
|
import { Zerokit } from "./zerokit.js";
|
||||||
|
|
||||||
const log = new Logger("rln");
|
const log = new Logger("rln");
|
||||||
|
|
||||||
type WakuRLNEncoderOptions = WakuEncoderOptions & {
|
|
||||||
credentials: EncryptedCredentials | DecryptedCredentials;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RLNInstance extends RLNCredentialsManager {
|
export class RLNInstance extends RLNCredentialsManager {
|
||||||
/**
|
/**
|
||||||
* Create an instance of RLN
|
* Create an instance of RLN
|
||||||
@ -39,18 +16,13 @@ export class RLNInstance extends RLNCredentialsManager {
|
|||||||
*/
|
*/
|
||||||
public static async create(): Promise<RLNInstance> {
|
public static async create(): Promise<RLNInstance> {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
await init();
|
||||||
await (init as any)?.();
|
zerokitRLN.initPanicHook();
|
||||||
zerokitRLN.init_panic_hook();
|
|
||||||
|
|
||||||
const witnessCalculator = await RLNInstance.loadWitnessCalculator();
|
const witnessCalculator = await RLNInstance.loadWitnessCalculator();
|
||||||
const zkey = await RLNInstance.loadZkey();
|
const zkey = await RLNInstance.loadZkey();
|
||||||
|
|
||||||
const stringEncoder = new TextEncoder();
|
const zkRLN = zerokitRLN.newRLN(zkey);
|
||||||
const vkey = stringEncoder.encode(JSON.stringify(verificationKey));
|
|
||||||
|
|
||||||
const DEPTH = 20;
|
|
||||||
const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey);
|
|
||||||
const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT);
|
const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT);
|
||||||
|
|
||||||
return new RLNInstance(zerokit);
|
return new RLNInstance(zerokit);
|
||||||
@ -64,39 +36,6 @@ export class RLNInstance extends RLNCredentialsManager {
|
|||||||
super(zerokit);
|
super(zerokit);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public createDecoder(
|
|
||||||
contentTopic: ContentTopic,
|
|
||||||
routingInfo: IRoutingInfo
|
|
||||||
): RLNDecoder<IDecodedMessage> {
|
|
||||||
return createRLNDecoder({
|
|
||||||
rlnInstance: this,
|
|
||||||
decoder: createDecoder(contentTopic, routingInfo)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async loadWitnessCalculator(): Promise<WitnessCalculator> {
|
public static async loadWitnessCalculator(): Promise<WitnessCalculator> {
|
||||||
try {
|
try {
|
||||||
const url = new URL("./resources/rln.wasm", import.meta.url);
|
const url = new URL("./resources/rln.wasm", import.meta.url);
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
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]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,56 +1,52 @@
|
|||||||
export class BytesUtils {
|
export class BytesUtils {
|
||||||
/**
|
/**
|
||||||
* Switches endianness of a byte array
|
* Concatenate Uint8Arrays
|
||||||
|
* @param input
|
||||||
|
* @returns concatenation of all Uint8Array received as input
|
||||||
*/
|
*/
|
||||||
public static switchEndianness(bytes: Uint8Array): Uint8Array {
|
public static concatenate(...input: Uint8Array[]): Uint8Array {
|
||||||
return new Uint8Array([...bytes].reverse());
|
let totalLength = 0;
|
||||||
|
for (const arr of input) {
|
||||||
|
totalLength += arr.length;
|
||||||
}
|
}
|
||||||
|
const result = new Uint8Array(totalLength);
|
||||||
/**
|
let offset = 0;
|
||||||
* Builds a BigInt from a big-endian Uint8Array
|
for (const arr of input) {
|
||||||
* @param bytes The big-endian bytes to convert
|
result.set(arr, offset);
|
||||||
* @returns The resulting BigInt in big-endian format
|
offset += arr.length;
|
||||||
*/
|
|
||||||
public static buildBigIntFromUint8ArrayBE(bytes: Uint8Array): bigint {
|
|
||||||
let result = 0n;
|
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
|
||||||
result = (result << 8n) + BigInt(bytes[i]);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switches endianness of a bigint value
|
* Convert a Uint8Array to a BigInt with configurable input endianness
|
||||||
* @param value The bigint value to switch endianness for
|
* @param bytes - The byte array to convert
|
||||||
* @returns The bigint value with reversed endianness
|
* @param inputEndianness - Endianness of the input bytes ('big' or 'little')
|
||||||
|
* @returns BigInt representation of the bytes
|
||||||
*/
|
*/
|
||||||
public static switchEndiannessBigInt(value: bigint): bigint {
|
public static toBigInt(
|
||||||
// Convert bigint to byte array
|
bytes: Uint8Array,
|
||||||
const bytes = [];
|
inputEndianness: "big" | "little" = "little"
|
||||||
let tempValue = value;
|
): bigint {
|
||||||
while (tempValue > 0n) {
|
if (bytes.length === 0) {
|
||||||
bytes.push(Number(tempValue & 0xffn));
|
return 0n;
|
||||||
tempValue >>= 8n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reverse bytes and convert back to bigint
|
// Create a copy to avoid modifying the original array
|
||||||
return bytes
|
const workingBytes = new Uint8Array(bytes);
|
||||||
.reverse()
|
|
||||||
.reduce((acc, byte) => (acc << 8n) + BigInt(byte), 0n);
|
// Reverse bytes if input is little-endian to work with big-endian internally
|
||||||
|
if (inputEndianness === "little") {
|
||||||
|
workingBytes.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Convert to BigInt
|
||||||
* Converts a big-endian bigint to a 32-byte big-endian Uint8Array
|
let result = 0n;
|
||||||
* @param value The big-endian bigint to convert
|
for (let i = 0; i < workingBytes.length; i++) {
|
||||||
* @returns A 32-byte big-endian Uint8Array
|
result = (result << 8n) | BigInt(workingBytes[i]);
|
||||||
*/
|
|
||||||
public static bigIntToUint8Array32BE(value: bigint): Uint8Array {
|
|
||||||
const bytes = new Uint8Array(32);
|
|
||||||
for (let i = 31; i >= 0; i--) {
|
|
||||||
bytes[i] = Number(value & 0xffn);
|
|
||||||
value >>= 8n;
|
|
||||||
}
|
}
|
||||||
return bytes;
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,20 +77,6 @@ export class BytesUtils {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
public static 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adapted from https://github.com/feross/buffer
|
// Adapted from https://github.com/feross/buffer
|
||||||
public static checkInt(
|
public static checkInt(
|
||||||
buf: Uint8Array,
|
buf: Uint8Array,
|
||||||
@ -108,23 +90,4 @@ export class BytesUtils {
|
|||||||
throw new RangeError('"value" argument is out of bounds');
|
throw new RangeError('"value" argument is out of bounds');
|
||||||
if (offset + ext > buf.length) throw new RangeError("Index out of range");
|
if (offset + ext > buf.length) throw new RangeError("Index out of range");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Concatenate Uint8Arrays
|
|
||||||
* @param input
|
|
||||||
* @returns concatenation of all Uint8Array received as input
|
|
||||||
*/
|
|
||||||
public static 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
26
packages/rln/src/zerokit.spec.ts
Normal file
26
packages/rln/src/zerokit.spec.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import { RLNInstance } from "./rln.js";
|
||||||
|
|
||||||
|
describe("@waku/rln", () => {
|
||||||
|
it("should generate the same membership key if the same seed is provided", async function () {
|
||||||
|
const rlnInstance = await RLNInstance.create();
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,11 +1,8 @@
|
|||||||
import type { IRateLimitProof } from "@waku/interfaces";
|
|
||||||
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
||||||
|
|
||||||
import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./contract/constants.js";
|
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
|
||||||
import { IdentityCredential } from "./identity.js";
|
import { IdentityCredential } from "./identity.js";
|
||||||
import { Proof, proofToBytes } from "./proof.js";
|
|
||||||
import { WitnessCalculator } from "./resources/witness_calculator";
|
import { WitnessCalculator } from "./resources/witness_calculator";
|
||||||
import { BytesUtils, dateToEpoch, epochIntToBytes } from "./utils/index.js";
|
|
||||||
|
|
||||||
export class Zerokit {
|
export class Zerokit {
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -26,226 +23,13 @@ export class Zerokit {
|
|||||||
return this._rateLimit;
|
return this._rateLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
public generateSeededIdentityCredential(seed: string): IdentityCredential {
|
||||||
const stringEncoder = new TextEncoder();
|
const stringEncoder = new TextEncoder();
|
||||||
const seedBytes = stringEncoder.encode(seed);
|
const seedBytes = stringEncoder.encode(seed);
|
||||||
// TODO: rename this function in zerokit rln-wasm
|
|
||||||
const memKeys = zerokitRLN.generateSeededExtendedMembershipKey(
|
const memKeys = zerokitRLN.generateSeededExtendedMembershipKey(
|
||||||
this.zkRLN,
|
this.zkRLN,
|
||||||
seedBytes
|
seedBytes
|
||||||
);
|
);
|
||||||
return IdentityCredential.fromBytes(memKeys);
|
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 = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
idCommitments.length,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
const idCommitmentBytes = BytesUtils.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,
|
|
||||||
rateLimit?: number
|
|
||||||
): Uint8Array {
|
|
||||||
// calculate message length
|
|
||||||
const msgLen = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
uint8Msg.length,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
const memIndexBytes = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
memIndex,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
const rateLimitBytes = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
rateLimit ?? this.rateLimit,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
|
|
||||||
// [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> | rate_limit<8> ]
|
|
||||||
return BytesUtils.concatenate(
|
|
||||||
idKey,
|
|
||||||
memIndexBytes,
|
|
||||||
epoch,
|
|
||||||
msgLen,
|
|
||||||
uint8Msg,
|
|
||||||
rateLimitBytes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async generateRLNProof(
|
|
||||||
msg: Uint8Array,
|
|
||||||
index: number,
|
|
||||||
epoch: Uint8Array | Date | undefined,
|
|
||||||
idSecretHash: Uint8Array,
|
|
||||||
rateLimit?: number
|
|
||||||
): Promise<IRateLimitProof> {
|
|
||||||
if (epoch === undefined) {
|
|
||||||
epoch = epochIntToBytes(dateToEpoch(new Date()));
|
|
||||||
} else if (epoch instanceof Date) {
|
|
||||||
epoch = epochIntToBytes(dateToEpoch(epoch));
|
|
||||||
}
|
|
||||||
|
|
||||||
const effectiveRateLimit = rateLimit ?? this.rateLimit;
|
|
||||||
|
|
||||||
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");
|
|
||||||
if (
|
|
||||||
effectiveRateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
|
|
||||||
effectiveRateLimit > RATE_LIMIT_PARAMS.MAX_RATE
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serialized_msg = this.serializeMessage(
|
|
||||||
msg,
|
|
||||||
index,
|
|
||||||
epoch,
|
|
||||||
idSecretHash,
|
|
||||||
effectiveRateLimit
|
|
||||||
);
|
|
||||||
const rlnWitness = zerokitRLN.getSerializedRLNWitness(
|
|
||||||
this.zkRLN,
|
|
||||||
serialized_msg
|
|
||||||
);
|
|
||||||
const inputs = zerokitRLN.RLNWitnessToJson(this.zkRLN, rlnWitness);
|
|
||||||
const calculatedWitness = await this.witnessCalculator.calculateWitness(
|
|
||||||
inputs,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const proofBytes = zerokitRLN.generate_rln_proof_with_witness(
|
|
||||||
this.zkRLN,
|
|
||||||
calculatedWitness,
|
|
||||||
rlnWitness
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Proof(proofBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyRLNProof(
|
|
||||||
proof: IRateLimitProof | Uint8Array,
|
|
||||||
msg: Uint8Array,
|
|
||||||
rateLimit?: number
|
|
||||||
): boolean {
|
|
||||||
let pBytes: Uint8Array;
|
|
||||||
if (proof instanceof Uint8Array) {
|
|
||||||
pBytes = proof;
|
|
||||||
} else {
|
|
||||||
pBytes = proofToBytes(proof);
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate message length
|
|
||||||
const msgLen = BytesUtils.writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
|
||||||
const rateLimitBytes = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
rateLimit ?? this.rateLimit,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
|
|
||||||
return zerokitRLN.verifyRLNProof(
|
|
||||||
this.zkRLN,
|
|
||||||
BytesUtils.concatenate(pBytes, msgLen, msg, rateLimitBytes)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyWithRoots(
|
|
||||||
proof: IRateLimitProof | Uint8Array,
|
|
||||||
msg: Uint8Array,
|
|
||||||
roots: Array<Uint8Array>,
|
|
||||||
rateLimit?: number
|
|
||||||
): boolean {
|
|
||||||
let pBytes: Uint8Array;
|
|
||||||
if (proof instanceof Uint8Array) {
|
|
||||||
pBytes = proof;
|
|
||||||
} else {
|
|
||||||
pBytes = proofToBytes(proof);
|
|
||||||
}
|
|
||||||
// calculate message length
|
|
||||||
const msgLen = BytesUtils.writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
|
||||||
const rateLimitBytes = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
rateLimit ?? this.rateLimit,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
|
|
||||||
const rootsBytes = BytesUtils.concatenate(...roots);
|
|
||||||
|
|
||||||
return zerokitRLN.verifyWithRoots(
|
|
||||||
this.zkRLN,
|
|
||||||
BytesUtils.concatenate(pBytes, msgLen, msg, rateLimitBytes),
|
|
||||||
rootsBytes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyWithNoRoot(
|
|
||||||
proof: IRateLimitProof | Uint8Array,
|
|
||||||
msg: Uint8Array,
|
|
||||||
rateLimit?: number
|
|
||||||
): boolean {
|
|
||||||
let pBytes: Uint8Array;
|
|
||||||
if (proof instanceof Uint8Array) {
|
|
||||||
pBytes = proof;
|
|
||||||
} else {
|
|
||||||
pBytes = proofToBytes(proof);
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate message length
|
|
||||||
const msgLen = BytesUtils.writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
|
||||||
const rateLimitBytes = BytesUtils.writeUIntLE(
|
|
||||||
new Uint8Array(8),
|
|
||||||
rateLimit ?? this.rateLimit,
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
);
|
|
||||||
|
|
||||||
return zerokitRLN.verifyWithRoots(
|
|
||||||
this.zkRLN,
|
|
||||||
BytesUtils.concatenate(pBytes, msgLen, msg, rateLimitBytes),
|
|
||||||
new Uint8Array()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -176,7 +176,8 @@ describe("Reliable Channel", () => {
|
|||||||
expect(messageAcknowledged).to.be.false;
|
expect(messageAcknowledged).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Outgoing message is possibly acknowledged", async () => {
|
// TODO: https://github.com/waku-org/js-waku/issues/2648
|
||||||
|
it.skip("Outgoing message is possibly acknowledged", async () => {
|
||||||
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
|
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
|
||||||
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
|
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
|
||||||
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
|
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
|
||||||
|
|||||||
@ -187,7 +187,8 @@ describe("Reliable Channel: Encryption", () => {
|
|||||||
expect(messageAcknowledged).to.be.false;
|
expect(messageAcknowledged).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Outgoing message is possibly acknowledged", async () => {
|
// TODO: https://github.com/waku-org/js-waku/issues/2648
|
||||||
|
it.skip("Outgoing message is possibly acknowledged", async () => {
|
||||||
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
|
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
|
||||||
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
|
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
|
||||||
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
|
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user