diff --git a/.cspell.json b/.cspell.json index e8582e38b0..dd57575bc2 100644 --- a/.cspell.json +++ b/.cspell.json @@ -24,9 +24,11 @@ "cipherparams", "ciphertext", "circleci", + "circom", "codecov", "codegen", "commitlint", + "cooldown", "dependabot", "dialable", "dingpu", @@ -41,9 +43,7 @@ "Encrypters", "enr", "enrs", - "unsubscription", "enrtree", - "unhandle", "ephem", "esnext", "ethersproject", @@ -62,7 +62,6 @@ "ineed", "IPAM", "ipfs", - "cooldown", "iwant", "jdev", "jswaku", @@ -122,9 +121,11 @@ "typedoc", "undialable", "unencrypted", + "unhandle", "unmarshal", "unmount", "unmounts", + "unsubscription", "untracked", "upgrader", "vacp", @@ -139,6 +140,7 @@ "weboko", "websockets", "wifi", + "WTNS", "xsalsa20", "zerokit", "Привет", diff --git a/package-lock.json b/package-lock.json index fa49a49a1b..f5eebd8d44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8414,9 +8414,9 @@ "link": true }, "node_modules/@waku/zerokit-rln-wasm": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz", - "integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.2.1.tgz", + "integrity": "sha512-2Xp7e92y4qZpsiTPGBSVr4gVJ9mJTLaudlo0DQxNpxJUBtoJKpxdH5xDCQDiorbkWZC2j9EId+ohhxHO/xC1QQ==", "license": "MIT or Apache2" }, "node_modules/@webassemblyjs/ast": { @@ -37468,7 +37468,7 @@ "@noble/hashes": "^1.2.0", "@waku/core": "^0.0.39", "@waku/utils": "^0.0.27", - "@waku/zerokit-rln-wasm": "^0.0.13", + "@waku/zerokit-rln-wasm": "^0.2.1", "chai": "^5.1.2", "chai-as-promised": "^8.0.1", "chai-spies": "^1.1.0", diff --git a/packages/browser-tests/tests/headless.spec.ts b/packages/browser-tests/tests/headless.spec.ts index 0817d1c1cf..dd4bcaee25 100644 --- a/packages/browser-tests/tests/headless.spec.ts +++ b/packages/browser-tests/tests/headless.spec.ts @@ -69,7 +69,8 @@ test.describe("waku", () => { 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) => { return window.wakuAPI.dialPeers(window.waku, peerAddrs); }, ACTIVE_PEERS); diff --git a/packages/rln/package.json b/packages/rln/package.json index bb34607c96..4ea0199115 100644 --- a/packages/rln/package.json +++ b/packages/rln/package.json @@ -79,7 +79,7 @@ "@waku/core": "^0.0.39", "@waku/utils": "^0.0.27", "@noble/hashes": "^1.2.0", - "@waku/zerokit-rln-wasm": "^0.0.13", + "@waku/zerokit-rln-wasm": "^0.2.1", "ethereum-cryptography": "^3.1.0", "ethers": "^5.7.2", "lodash": "^4.17.21", diff --git a/packages/rln/src/codec.spec.ts b/packages/rln/src/codec.spec.ts deleted file mode 100644 index dfb6c80f1b..0000000000 --- a/packages/rln/src/codec.spec.ts +++ /dev/null @@ -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; - - 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; - - 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; - - const expectedMeta = metaSetter({ - ...EMPTY_PROTO_MESSAGE, - payload: msg!.payload - }); - - expect(msg!.meta).to.deep.eq(expectedMeta); - verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance); - }); -}); diff --git a/packages/rln/src/codec.test-utils.ts b/packages/rln/src/codec.test-utils.ts deleted file mode 100644 index 4b16ae7131..0000000000 --- a/packages/rln/src/codec.test-utils.ts +++ /dev/null @@ -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 { - 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; -} diff --git a/packages/rln/src/codec.ts b/packages/rln/src/codec.ts deleted file mode 100644 index 067e2f382a..0000000000 --- a/packages/rln/src/codec.ts +++ /dev/null @@ -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 { - message.rateLimitProof = await this.generateProof(message); - log.info("Proof generated", message.rateLimitProof); - return this.encoder.toWire(message); - } - - public async toProtoObj( - message: IMessage - ): Promise { - const protoMessage = await this.encoder.toProtoObj(message); - if (!protoMessage) return; - - protoMessage.contentTopic = this.contentTopic; - protoMessage.rateLimitProof = await this.generateProof(message); - log.info("Proof generated", protoMessage.rateLimitProof); - return protoMessage; - } - - private async generateProof(message: IMessage): Promise { - const signal = toRLNSignal(this.contentTopic, message); - 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 - implements IDecoder> -{ - public constructor( - private readonly rlnInstance: RLNInstance, - private readonly decoder: IDecoder - ) {} - - public get pubsubTopic(): string { - return this.decoder.pubsubTopic; - } - - public get contentTopic(): string { - return this.decoder.contentTopic; - } - - public fromWireToProtoObj( - bytes: Uint8Array - ): Promise { - const protoMessage = this.decoder.fromWireToProtoObj(bytes); - log.info("Message decoded", protoMessage); - return Promise.resolve(protoMessage); - } - - public async fromProtoObj( - pubsubTopic: string, - proto: IProtoMessage - ): Promise | undefined> { - const msg: T | undefined = await this.decoder.fromProtoObj( - pubsubTopic, - proto - ); - if (!msg) return; - return new RlnMessage(this.rlnInstance, msg, proto.rateLimitProof); - } -} - -type RLNDecoderOptions = { - decoder: IDecoder; - rlnInstance: RLNInstance; -}; - -export const createRLNDecoder = ( - options: RLNDecoderOptions -): RLNDecoder => { - return new RLNDecoder(options.rlnInstance, options.decoder); -}; diff --git a/packages/rln/src/contract/constants.ts b/packages/rln/src/contract/constants.ts index 4808a88a14..7869e65bc6 100644 --- a/packages/rln/src/contract/constants.ts +++ b/packages/rln/src/contract/constants.ts @@ -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 */ export const RATE_LIMIT_TIERS = { - LOW: 20, // Suggested minimum rate - 20 messages per epoch - MEDIUM: 200, - HIGH: 600 // Suggested maximum rate - 600 messages per epoch + STANDARD: 300, + MAX: 600 } as const; // Global rate limit parameters export const RATE_LIMIT_PARAMS = { - MIN_RATE: RATE_LIMIT_TIERS.LOW, - MAX_RATE: RATE_LIMIT_TIERS.HIGH, - MAX_TOTAL_RATE: 160_000, // Maximum total rate limit across all memberships - EPOCH_LENGTH: 600 // Epoch length in seconds (10 minutes) + MIN_RATE: RATE_LIMIT_TIERS.STANDARD, + MAX_RATE: RATE_LIMIT_TIERS.MAX, + MAX_TOTAL_RATE: 160_000, + EPOCH_LENGTH: 600 } 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; diff --git a/packages/rln/src/contract/index.ts b/packages/rln/src/contract/index.ts index 7f96557dc9..5d4d612733 100644 --- a/packages/rln/src/contract/index.ts +++ b/packages/rln/src/contract/index.ts @@ -1,3 +1,2 @@ -export { RLNContract } from "./rln_contract.js"; export * from "./constants.js"; export * from "./types.js"; diff --git a/packages/rln/src/contract/rln_base_contract.ts b/packages/rln/src/contract/rln_base_contract.ts index 7546596cd0..9934360be3 100644 --- a/packages/rln/src/contract/rln_base_contract.ts +++ b/packages/rln/src/contract/rln_base_contract.ts @@ -3,7 +3,6 @@ import { ethers } from "ethers"; import { IdentityCredential } from "../identity.js"; import { DecryptedCredentials } from "../keystore/types.js"; -import { BytesUtils } from "../utils/bytes.js"; import { RLN_ABI } from "./abi/rln.js"; import { @@ -632,7 +631,7 @@ export class RLNBaseContract { permit.v, permit.r, permit.s, - BytesUtils.buildBigIntFromUint8ArrayBE(identity.IDCommitment), + identity.IDCommitmentBigInt, this.rateLimit, idCommitmentsToErase.map((id) => ethers.BigNumber.from(id)) ); diff --git a/packages/rln/src/contract/rln_contract.spec.ts b/packages/rln/src/contract/rln_contract.spec.ts deleted file mode 100644 index 7603e7cc2e..0000000000 --- a/packages/rln/src/contract/rln_contract.spec.ts +++ /dev/null @@ -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 - ); - }); - }); -}); diff --git a/packages/rln/src/contract/rln_contract.ts b/packages/rln/src/contract/rln_contract.ts deleted file mode 100644 index eae91b323e..0000000000 --- a/packages/rln/src/contract/rln_contract.ts +++ /dev/null @@ -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 { - 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(); - const toInsertTable = new Map(); - - 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 - ): 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 - ): 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); - }); - } -} diff --git a/packages/rln/src/contract/test_setup.ts b/packages/rln/src/contract/test_setup.ts deleted file mode 100644 index b5da3f6af6..0000000000 --- a/packages/rln/src/contract/test_setup.ts +++ /dev/null @@ -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 { - 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 { - 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" -}; diff --git a/packages/rln/src/contract/test_utils.ts b/packages/rln/src/contract/test_utils.ts deleted file mode 100644 index a2ac8bc403..0000000000 --- a/packages/rln/src/contract/test_utils.ts +++ /dev/null @@ -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; - 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; - [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 - ); -} diff --git a/packages/rln/src/create.spec.ts b/packages/rln/src/create.spec.ts deleted file mode 100644 index 7dc2e4fd5e..0000000000 --- a/packages/rln/src/create.spec.ts +++ /dev/null @@ -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]); - }); - }); -}); diff --git a/packages/rln/src/credentials_manager.ts b/packages/rln/src/credentials_manager.ts index c4fdceec70..b82d9421f1 100644 --- a/packages/rln/src/credentials_manager.ts +++ b/packages/rln/src/credentials_manager.ts @@ -1,11 +1,8 @@ -import { hmac } from "@noble/hashes/hmac"; -import { sha256 } from "@noble/hashes/sha2"; import { Logger } from "@waku/utils"; 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 { IdentityCredential } from "./identity.js"; import { Keystore } from "./keystore/index.js"; import type { DecryptedCredentials, @@ -13,7 +10,6 @@ import type { } from "./keystore/index.js"; import { KeystoreEntity, Password } from "./keystore/types.js"; import { RegisterMembershipOptions, StartRLNOptions } from "./types.js"; -import { BytesUtils } from "./utils/bytes.js"; import { extractMetaMaskSigner } from "./utils/index.js"; import { Zerokit } from "./zerokit.js"; @@ -21,7 +17,6 @@ const log = new Logger("rln:credentials"); /** * 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 */ export class RLNCredentialsManager { @@ -34,9 +29,9 @@ export class RLNCredentialsManager { protected keystore = Keystore.create(); public credentials: undefined | DecryptedCredentials; - public zerokit: undefined | Zerokit; + public zerokit: Zerokit; - public constructor(zerokit?: Zerokit) { + public constructor(zerokit: Zerokit) { log.info("RLNCredentialsManager initialized"); this.zerokit = zerokit; } @@ -81,7 +76,7 @@ export class RLNCredentialsManager { this.contract = await RLNBaseContract.create({ address: address!, signer: signer!, - rateLimit: rateLimit ?? this.zerokit?.rateLimit + rateLimit: rateLimit ?? this.zerokit.rateLimit }); log.info("RLNCredentialsManager successfully started"); @@ -106,18 +101,10 @@ export class RLNCredentialsManager { let identity = "identity" in options && options.identity; if ("signature" in options) { - log.info("Generating identity from signature"); - if (this.zerokit) { - log.info("Using Zerokit to generate identity"); - identity = this.zerokit.generateSeededIdentityCredential( - options.signature - ); - } else { - log.info("Using local implementation to generate identity"); - identity = await this.generateSeededIdentityCredential( - options.signature - ); - } + log.info("Using Zerokit to generate identity"); + identity = this.zerokit.generateSeededIdentityCredential( + options.signature + ); } 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 { - 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); - } } diff --git a/packages/rln/src/identity.ts b/packages/rln/src/identity.ts index 87167165ba..873463ea6e 100644 --- a/packages/rln/src/identity.ts +++ b/packages/rln/src/identity.ts @@ -11,8 +11,7 @@ export class IdentityCredential { public readonly IDSecretHash: Uint8Array, public readonly IDCommitment: Uint8Array ) { - this.IDCommitmentBigInt = - BytesUtils.buildBigIntFromUint8ArrayBE(IDCommitment); + this.IDCommitmentBigInt = BytesUtils.toBigInt(IDCommitment); } public static fromBytes(memKeys: Uint8Array): IdentityCredential { diff --git a/packages/rln/src/index.ts b/packages/rln/src/index.ts index 0a07db7810..3348e370f7 100644 --- a/packages/rln/src/index.ts +++ b/packages/rln/src/index.ts @@ -1,28 +1,18 @@ -import { RLNDecoder, RLNEncoder } from "./codec.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 { createRLN } from "./create.js"; -import { RLNCredentialsManager } from "./credentials_manager.js"; import { IdentityCredential } from "./identity.js"; import { Keystore } from "./keystore/index.js"; -import { Proof } from "./proof.js"; import { RLNInstance } from "./rln.js"; -import { MerkleRootTracker } from "./root_tracker.js"; import { extractMetaMaskSigner } from "./utils/index.js"; export { - RLNCredentialsManager, RLNBaseContract, createRLN, Keystore, RLNInstance, IdentityCredential, - Proof, - RLNEncoder, - RLNDecoder, - MerkleRootTracker, - RLNContract, RLN_CONTRACT, extractMetaMaskSigner, RLN_ABI diff --git a/packages/rln/src/keystore/keystore.spec.ts b/packages/rln/src/keystore/keystore.spec.ts index 02f2f20bd3..95543e8535 100644 --- a/packages/rln/src/keystore/keystore.spec.ts +++ b/packages/rln/src/keystore/keystore.spec.ts @@ -222,9 +222,7 @@ describe("Keystore", () => { ]) } as unknown as IdentityCredential; // Add the missing property for test correctness - identity.IDCommitmentBigInt = BytesUtils.buildBigIntFromUint8ArrayBE( - identity.IDCommitment - ); + identity.IDCommitmentBigInt = BytesUtils.toBigInt(identity.IDCommitment); const membership = { chainId: "0xAA36A7", 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 ] } as unknown as IdentityCredential; - identity.IDCommitmentBigInt = BytesUtils.buildBigIntFromUint8ArrayBE( - identity.IDCommitment - ); + identity.IDCommitmentBigInt = BytesUtils.toBigInt(identity.IDCommitment); const membership = { chainId: "0xAA36A7", treeIndex: 8, diff --git a/packages/rln/src/keystore/keystore.ts b/packages/rln/src/keystore/keystore.ts index 880d707bf3..f001f2938d 100644 --- a/packages/rln/src/keystore/keystore.ts +++ b/packages/rln/src/keystore/keystore.ts @@ -264,20 +264,14 @@ export class Keystore { _.get(obj, "identityCredential.idSecretHash", []) ); - // Big Endian - 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); + const idCommitmentBigInt = BytesUtils.toBigInt(idCommitmentLE); return { identity: { - IDCommitment: idCommitmentBE, - IDTrapdoor: idTrapdoorBE, - IDNullifier: idNullifierBE, - IDSecretHash: idSecretHashBE, + IDCommitment: idCommitmentLE, + IDTrapdoor: idTrapdoorLE, + IDNullifier: idNullifierLE, + IDSecretHash: idSecretHashLE, IDCommitmentBigInt: idCommitmentBigInt }, membership: { @@ -329,35 +323,18 @@ export class Keystore { // follows nwaku implementation // 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 { const { IDCommitment, IDNullifier, IDSecretHash, IDTrapdoor } = 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( JSON.stringify({ treeIndex: options.membership.treeIndex, identityCredential: { - idCommitment: Array.from(idCommitmentLE), - idNullifier: Array.from(idNullifierLE), - idSecretHash: Array.from(idSecretHashLE), - idTrapdoor: Array.from(idTrapdoorLE) + idCommitment: Array.from(IDCommitment), + idNullifier: Array.from(IDNullifier), + idSecretHash: Array.from(IDSecretHash), + idTrapdoor: Array.from(IDTrapdoor) }, membershipContract: { chainId: options.membership.chainId, diff --git a/packages/rln/src/message.ts b/packages/rln/src/message.ts deleted file mode 100644 index 1cca8a4ed2..0000000000 --- a/packages/rln/src/message.ts +++ /dev/null @@ -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 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); - } -} diff --git a/packages/rln/src/proof.ts b/packages/rln/src/proof.ts deleted file mode 100644 index ce3ad6f26d..0000000000 --- a/packages/rln/src/proof.ts +++ /dev/null @@ -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 - ); -} diff --git a/packages/rln/src/resources/rln.wasm b/packages/rln/src/resources/rln.wasm index 04aaeef783..ddca70ce2e 100644 Binary files a/packages/rln/src/resources/rln.wasm and b/packages/rln/src/resources/rln.wasm differ diff --git a/packages/rln/src/resources/rln_final.zkey b/packages/rln/src/resources/rln_final.zkey index c6cc7d491a..46489a11c0 100644 Binary files a/packages/rln/src/resources/rln_final.zkey and b/packages/rln/src/resources/rln_final.zkey differ diff --git a/packages/rln/src/resources/verification_key.d.ts b/packages/rln/src/resources/verification_key.d.ts deleted file mode 100644 index 99ec4f67d1..0000000000 --- a/packages/rln/src/resources/verification_key.d.ts +++ /dev/null @@ -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; diff --git a/packages/rln/src/resources/verification_key.js b/packages/rln/src/resources/verification_key.js deleted file mode 100644 index 15425bef8c..0000000000 --- a/packages/rln/src/resources/verification_key.js +++ /dev/null @@ -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; diff --git a/packages/rln/src/resources/witness_calculator.d.ts b/packages/rln/src/resources/witness_calculator.d.ts index eb6f86aab9..a54b1d62c8 100644 --- a/packages/rln/src/resources/witness_calculator.d.ts +++ b/packages/rln/src/resources/witness_calculator.d.ts @@ -1,11 +1,25 @@ -export async function builder( +export const builder: ( code: Uint8Array, - sanityCheck: boolean -): Promise; + sanityCheck?: boolean +) => Promise; export class WitnessCalculator { - public calculateWitness( - input: unknown, - sanityCheck: boolean - ): Promise>; + constructor(instance: any, sanityCheck?: boolean); + + circom_version(): number; + + calculateWitness( + input: Record, + sanityCheck?: boolean + ): Promise; + + calculateBinWitness( + input: Record, + sanityCheck?: boolean + ): Promise; + + calculateWTNSBin( + input: Record, + sanityCheck?: boolean + ): Promise; } diff --git a/packages/rln/src/resources/witness_calculator.js b/packages/rln/src/resources/witness_calculator.js index 47b218d881..1befc32e98 100644 --- a/packages/rln/src/resources/witness_calculator.js +++ b/packages/rln/src/resources/witness_calculator.js @@ -1,6 +1,6 @@ // File generated with https://github.com/iden3/circom // 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) { options = options || {}; diff --git a/packages/rln/src/rln.ts b/packages/rln/src/rln.ts index 00e7eaa249..491fd14d44 100644 --- a/packages/rln/src/rln.ts +++ b/packages/rln/src/rln.ts @@ -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 init from "@waku/zerokit-rln-wasm"; -import * as zerokitRLN from "@waku/zerokit-rln-wasm"; +import init, * 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 { 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 { WitnessCalculator } from "./resources/witness_calculator"; import { Zerokit } from "./zerokit.js"; const log = new Logger("rln"); -type WakuRLNEncoderOptions = WakuEncoderOptions & { - credentials: EncryptedCredentials | DecryptedCredentials; -}; - export class RLNInstance extends RLNCredentialsManager { /** * Create an instance of RLN @@ -39,18 +16,13 @@ export class RLNInstance extends RLNCredentialsManager { */ public static async create(): Promise { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (init as any)?.(); - zerokitRLN.init_panic_hook(); + await init(); + zerokitRLN.initPanicHook(); const witnessCalculator = await RLNInstance.loadWitnessCalculator(); const zkey = await RLNInstance.loadZkey(); - const stringEncoder = new TextEncoder(); - const vkey = stringEncoder.encode(JSON.stringify(verificationKey)); - - const DEPTH = 20; - const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey); + const zkRLN = zerokitRLN.newRLN(zkey); const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT); return new RLNInstance(zerokit); @@ -64,39 +36,6 @@ export class RLNInstance extends RLNCredentialsManager { super(zerokit); } - public async createEncoder( - options: WakuRLNEncoderOptions - ): Promise { - const { credentials: decryptedCredentials } = - await RLNInstance.decryptCredentialsIfNeeded(options.credentials); - const credentials = decryptedCredentials || this.credentials; - - if (!credentials) { - throw Error( - "Failed to create Encoder: missing RLN credentials. Use createRLNEncoder directly." - ); - } - - await this.verifyCredentialsAgainstContract(credentials); - - return createRLNEncoder({ - encoder: createEncoder(options), - rlnInstance: this, - index: credentials.membership.treeIndex, - credential: credentials.identity - }); - } - - public createDecoder( - contentTopic: ContentTopic, - routingInfo: IRoutingInfo - ): RLNDecoder { - return createRLNDecoder({ - rlnInstance: this, - decoder: createDecoder(contentTopic, routingInfo) - }); - } - public static async loadWitnessCalculator(): Promise { try { const url = new URL("./resources/rln.wasm", import.meta.url); diff --git a/packages/rln/src/root_tracker.spec.ts b/packages/rln/src/root_tracker.spec.ts deleted file mode 100644 index 8e3c03efe0..0000000000 --- a/packages/rln/src/root_tracker.spec.ts +++ /dev/null @@ -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])); - }); -}); diff --git a/packages/rln/src/root_tracker.ts b/packages/rln/src/root_tracker.ts deleted file mode 100644 index 79b71845c1..0000000000 --- a/packages/rln/src/root_tracker.ts +++ /dev/null @@ -1,92 +0,0 @@ -class RootPerBlock { - public constructor( - public root: Uint8Array, - public blockNumber: number - ) {} -} - -const maxBufferSize = 20; - -export class MerkleRootTracker { - private validMerkleRoots: Array = new Array(); - private merkleRootBuffer: Array = new Array(); - - public constructor( - private acceptableRootWindowSize: number, - initialRoot: Uint8Array - ) { - this.pushRoot(0, initialRoot); - } - - public backFill(fromBlockNumber: number): void { - if (this.validMerkleRoots.length == 0) return; - - let numBlocks = 0; - for (let i = this.validMerkleRoots.length - 1; i >= 0; i--) { - if (this.validMerkleRoots[i].blockNumber >= fromBlockNumber) { - numBlocks++; - } - } - - if (numBlocks == 0) return; - - const olderBlock = fromBlockNumber < this.validMerkleRoots[0].blockNumber; - - // Remove last roots - let rootsToPop = numBlocks; - if (this.validMerkleRoots.length < rootsToPop) { - rootsToPop = this.validMerkleRoots.length; - } - - this.validMerkleRoots = this.validMerkleRoots.slice( - 0, - this.validMerkleRoots.length - rootsToPop - ); - - if (this.merkleRootBuffer.length == 0) return; - - if (olderBlock) { - const idx = this.merkleRootBuffer.findIndex( - (x) => x.blockNumber == fromBlockNumber - ); - if (idx > -1) { - this.merkleRootBuffer = this.merkleRootBuffer.slice(0, idx); - } - } - - // Backfill the tree's acceptable roots - let rootsToRestore = - this.acceptableRootWindowSize - this.validMerkleRoots.length; - if (this.merkleRootBuffer.length < rootsToRestore) { - rootsToRestore = this.merkleRootBuffer.length; - } - - for (let i = 0; i < rootsToRestore; i++) { - const x = this.merkleRootBuffer.pop(); - if (x) this.validMerkleRoots.unshift(x); - } - } - - public pushRoot(blockNumber: number, root: Uint8Array): void { - this.validMerkleRoots.push(new RootPerBlock(root, blockNumber)); - - // Maintain valid merkle root window - if (this.validMerkleRoots.length > this.acceptableRootWindowSize) { - const x = this.validMerkleRoots.shift(); - if (x) this.merkleRootBuffer.push(x); - } - - // Maintain merkle root buffer - if (this.merkleRootBuffer.length > maxBufferSize) { - this.merkleRootBuffer.shift(); - } - } - - public roots(): Array { - return this.validMerkleRoots.map((x) => x.root); - } - - public buffer(): Array { - return this.merkleRootBuffer.map((x) => x.root); - } -} diff --git a/packages/rln/src/utils/bytes.ts b/packages/rln/src/utils/bytes.ts index d871e8f564..4df17bd380 100644 --- a/packages/rln/src/utils/bytes.ts +++ b/packages/rln/src/utils/bytes.ts @@ -1,56 +1,52 @@ 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 { - return new Uint8Array([...bytes].reverse()); - } - - /** - * Builds a BigInt from a big-endian Uint8Array - * @param bytes The big-endian bytes to convert - * @returns The resulting BigInt in big-endian format - */ - public static buildBigIntFromUint8ArrayBE(bytes: Uint8Array): bigint { - let result = 0n; - for (let i = 0; i < bytes.length; i++) { - result = (result << 8n) + BigInt(bytes[i]); + 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; } /** - * Switches endianness of a bigint value - * @param value The bigint value to switch endianness for - * @returns The bigint value with reversed endianness + * Convert a Uint8Array to a BigInt with configurable input endianness + * @param bytes - The byte array to convert + * @param inputEndianness - Endianness of the input bytes ('big' or 'little') + * @returns BigInt representation of the bytes */ - public static switchEndiannessBigInt(value: bigint): bigint { - // Convert bigint to byte array - const bytes = []; - let tempValue = value; - while (tempValue > 0n) { - bytes.push(Number(tempValue & 0xffn)); - tempValue >>= 8n; + public static toBigInt( + bytes: Uint8Array, + inputEndianness: "big" | "little" = "little" + ): bigint { + if (bytes.length === 0) { + return 0n; } - // Reverse bytes and convert back to bigint - return bytes - .reverse() - .reduce((acc, byte) => (acc << 8n) + BigInt(byte), 0n); - } + // Create a copy to avoid modifying the original array + const workingBytes = new Uint8Array(bytes); - /** - * Converts a big-endian bigint to a 32-byte big-endian Uint8Array - * @param value The big-endian bigint to convert - * @returns A 32-byte big-endian Uint8Array - */ - 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; + // Reverse bytes if input is little-endian to work with big-endian internally + if (inputEndianness === "little") { + workingBytes.reverse(); } - return bytes; + + // Convert to BigInt + let result = 0n; + for (let i = 0; i < workingBytes.length; i++) { + result = (result << 8n) | BigInt(workingBytes[i]); + } + + return result; } /** @@ -81,20 +77,6 @@ export class BytesUtils { 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 public static checkInt( buf: Uint8Array, @@ -108,23 +90,4 @@ export class BytesUtils { throw new RangeError('"value" argument is out of bounds'); 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; - } } diff --git a/packages/rln/src/zerokit.spec.ts b/packages/rln/src/zerokit.spec.ts new file mode 100644 index 0000000000..6126c7c6ac --- /dev/null +++ b/packages/rln/src/zerokit.spec.ts @@ -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]); + }); + }); +}); diff --git a/packages/rln/src/zerokit.ts b/packages/rln/src/zerokit.ts index a7e7e628f4..47df182f00 100644 --- a/packages/rln/src/zerokit.ts +++ b/packages/rln/src/zerokit.ts @@ -1,11 +1,8 @@ -import type { IRateLimitProof } from "@waku/interfaces"; 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 { Proof, proofToBytes } from "./proof.js"; import { WitnessCalculator } from "./resources/witness_calculator"; -import { BytesUtils, dateToEpoch, epochIntToBytes } from "./utils/index.js"; export class Zerokit { public constructor( @@ -26,226 +23,13 @@ export class Zerokit { 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 { const stringEncoder = new TextEncoder(); const seedBytes = stringEncoder.encode(seed); - // TODO: rename this function in zerokit rln-wasm const memKeys = zerokitRLN.generateSeededExtendedMembershipKey( this.zkRLN, seedBytes ); return IdentityCredential.fromBytes(memKeys); } - - public insertMember(idCommitment: Uint8Array): void { - zerokitRLN.insertMember(this.zkRLN, idCommitment); - } - - public insertMembers( - index: number, - ...idCommitments: Array - ): void { - // serializes a seq of IDCommitments to a byte seq - // the order of serialization is |id_commitment_len<8>|id_commitment| - const idCommitmentLen = 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 | 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 { - 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, - 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() - ); - } } diff --git a/packages/sdk/src/reliable_channel/reliable_channel.spec.ts b/packages/sdk/src/reliable_channel/reliable_channel.spec.ts index b6d83123e6..28563feebf 100644 --- a/packages/sdk/src/reliable_channel/reliable_channel.spec.ts +++ b/packages/sdk/src/reliable_channel/reliable_channel.spec.ts @@ -176,7 +176,8 @@ describe("Reliable Channel", () => { 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(); const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter); const mockWakuNodeBob = new MockWakuNode(commonEventEmitter); diff --git a/packages/sdk/src/reliable_channel/reliable_channel_encryption.spec.ts b/packages/sdk/src/reliable_channel/reliable_channel_encryption.spec.ts index 978d357ec6..628c99bfaa 100644 --- a/packages/sdk/src/reliable_channel/reliable_channel_encryption.spec.ts +++ b/packages/sdk/src/reliable_channel/reliable_channel_encryption.spec.ts @@ -187,7 +187,8 @@ describe("Reliable Channel: Encryption", () => { 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(); const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter); const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);