diff --git a/package-lock.json b/package-lock.json index b34148a402..bc4eec6e36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7138,15 +7138,10 @@ "link": true }, "node_modules/@waku/zerokit-rln-wasm": { - "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/@waku/zerokit-rln-wasm-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm-utils/-/zerokit-rln-wasm-utils-0.1.0.tgz", - "integrity": "sha512-3ccyg9+CtRXFJfWaxI/kx8Aec5B2S9YUmZAVhPRdN1EG6iQYG2hgvAurx8ZF9/zOppdrhzzyvCgDPg5kRUlOfQ==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-1.0.0.tgz", + "integrity": "sha512-kRAeUePAY3++i5XXniCx+tqDH+3rdfPKED/lFRrbQ8ZiNWpu059fKxtPQqqvd8jNZQUOWDc7HRTpq2TVbWd8yQ==", + "license": "MIT OR Apache-2.0" }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", @@ -34727,8 +34722,7 @@ "@wagmi/core": "^2.22.1", "@waku/core": "^0.0.40", "@waku/utils": "^0.0.27", - "@waku/zerokit-rln-wasm": "^0.2.1", - "@waku/zerokit-rln-wasm-utils": "^0.1.0", + "@waku/zerokit-rln-wasm": "^1.0.0", "chai": "^5.1.2", "chai-as-promised": "^8.0.1", "chai-spies": "^1.1.0", @@ -34752,6 +34746,7 @@ "@waku/build-utils": "^1.0.0", "@waku/message-encryption": "^0.0.37", "@waku/sdk": "^0.0.36", + "@waku/tests": "*", "deep-equal-in-any-order": "^2.0.6", "fast-check": "^3.23.2", "rollup-plugin-copy": "^3.5.0" diff --git a/packages/rln/karma.conf.cjs b/packages/rln/karma.conf.cjs index d878f4355d..68743eb0da 100644 --- a/packages/rln/karma.conf.cjs +++ b/packages/rln/karma.conf.cjs @@ -26,7 +26,7 @@ module.exports = function (config) { nocache: true }, { - pattern: "src/resources/**/*.zkey", + pattern: "src/resources/**/*.arkzkey", included: false, served: true, watched: false, @@ -39,14 +39,6 @@ module.exports = function (config) { watched: false, type: "wasm", nocache: true - }, - { - pattern: "../../node_modules/@waku/zerokit-rln-wasm-utils/*.wasm", - included: false, - served: true, - watched: false, - type: "wasm", - nocache: true } ], @@ -68,7 +60,7 @@ module.exports = function (config) { mime: { "application/wasm": ["wasm"], - "application/octet-stream": ["zkey"] + "application/octet-stream": ["arkzkey"] }, customHeaders: [ @@ -78,7 +70,7 @@ module.exports = function (config) { value: "application/wasm" }, { - match: ".*\\.zkey$", + match: ".*\\.arkzkey$", name: "Content-Type", value: "application/octet-stream" } @@ -91,16 +83,10 @@ module.exports = function (config) { __dirname, "../../node_modules/@waku/zerokit-rln-wasm/rln_wasm_bg.wasm" ), - "/base/rln_wasm_utils_bg.wasm": - "/absolute" + - path.resolve( - __dirname, - "../../node_modules/@waku/zerokit-rln-wasm-utils/rln_wasm_utils_bg.wasm" - ), "/base/rln.wasm": "/absolute" + path.resolve(__dirname, "src/resources/rln.wasm"), - "/base/rln_final.zkey": - "/absolute" + path.resolve(__dirname, "src/resources/rln_final.zkey") + "/base/rln_final.arkzkey": + "/absolute" + path.resolve(__dirname, "src/resources/rln_final.arkzkey") }, webpack: { @@ -131,7 +117,7 @@ module.exports = function (config) { } }, { - test: /\.zkey$/, + test: /\.arkzkey$/, type: "asset/resource", generator: { filename: "[name][ext]" diff --git a/packages/rln/karma.node.conf.cjs b/packages/rln/karma.node.conf.cjs index a25cf309cf..81a984d358 100644 --- a/packages/rln/karma.node.conf.cjs +++ b/packages/rln/karma.node.conf.cjs @@ -33,7 +33,7 @@ module.exports = function (config) { nocache: true }, { - pattern: "src/resources/**/*.zkey", + pattern: "src/resources/**/*.arkzkey", included: false, served: true, watched: false, @@ -47,14 +47,6 @@ module.exports = function (config) { type: "wasm", nocache: true }, - { - pattern: "../../node_modules/@waku/zerokit-rln-wasm-utils/*.wasm", - included: false, - served: true, - watched: false, - type: "wasm", - nocache: true - }, { // Fleet info is written by the integration test runner pattern: "fleet-info.json", @@ -83,7 +75,7 @@ module.exports = function (config) { mime: { "application/wasm": ["wasm"], - "application/octet-stream": ["zkey"] + "application/octet-stream": ["arkzkey"] }, customHeaders: [ @@ -93,7 +85,7 @@ module.exports = function (config) { value: "application/wasm" }, { - match: ".*\\.zkey$", + match: ".*\\.arkzkey$", name: "Content-Type", value: "application/octet-stream" } @@ -106,16 +98,10 @@ module.exports = function (config) { __dirname, "../../node_modules/@waku/zerokit-rln-wasm/rln_wasm_bg.wasm" ), - "/base/rln_wasm_utils_bg.wasm": - "/absolute" + - path.resolve( - __dirname, - "../../node_modules/@waku/zerokit-rln-wasm-utils/rln_wasm_utils_bg.wasm" - ), "/base/rln.wasm": "/absolute" + path.resolve(__dirname, "src/resources/rln.wasm"), - "/base/rln_final.zkey": - "/absolute" + path.resolve(__dirname, "src/resources/rln_final.zkey") + "/base/rln_final.arkzkey": + "/absolute" + path.resolve(__dirname, "src/resources/rln_final.arkzkey") }, webpack: { @@ -146,7 +132,7 @@ module.exports = function (config) { } }, { - test: /\.zkey$/, + test: /\.arkzkey$/, type: "asset/resource", generator: { filename: "[name][ext]" diff --git a/packages/rln/package.json b/packages/rln/package.json index 53f548050d..b28f457539 100644 --- a/packages/rln/package.json +++ b/packages/rln/package.json @@ -85,8 +85,7 @@ "@wagmi/core": "^2.22.1", "@waku/core": "^0.0.40", "@waku/utils": "^0.0.27", - "@waku/zerokit-rln-wasm": "^0.2.1", - "@waku/zerokit-rln-wasm-utils": "^0.1.0", + "@waku/zerokit-rln-wasm": "^1.0.0", "chai": "^5.1.2", "chai-as-promised": "^8.0.1", "chai-spies": "^1.1.0", diff --git a/packages/rln/src/codec.ts b/packages/rln/src/codec.ts index 824736c6e1..84208cfa98 100644 --- a/packages/rln/src/codec.ts +++ b/packages/rln/src/codec.ts @@ -84,7 +84,7 @@ export class RLNEncoder implements IEncoder { 0 // TODO: need to track messages sent per epoch ); - return new Proof(proof, epoch, rlnIdentifier); + return new Proof(proof.toBytesLE(), epoch, rlnIdentifier); } public get pubsubTopic(): string { diff --git a/packages/rln/src/credentials_manager.ts b/packages/rln/src/credentials_manager.ts index 7e03dd3dfa..d04df82797 100644 --- a/packages/rln/src/credentials_manager.ts +++ b/packages/rln/src/credentials_manager.ts @@ -3,6 +3,7 @@ import { publicActions } from "viem"; 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, @@ -111,9 +112,10 @@ export class RLNCredentialsManager { if ("signature" in options) { log.info("Using Zerokit to generate identity"); - identity = this.zerokit.generateSeededIdentityCredential( + const extendedIdentity = this.zerokit.generateSeededIdentityCredential( options.signature ); + identity = IdentityCredential.fromBytes(extendedIdentity.toBytesLE()); } if (!identity) { diff --git a/packages/rln/src/proof.spec.ts b/packages/rln/src/proof.spec.ts index 7fff9deb09..0dbcb4013c 100644 --- a/packages/rln/src/proof.spec.ts +++ b/packages/rln/src/proof.spec.ts @@ -133,7 +133,7 @@ describe("RLN Proof Unit Tests", function () { ); // Parse proof bytes into Proof class - const parsedProof = new Proof(proof, epoch, rlnIdentifier); + const parsedProof = new Proof(proof.toBytesLE(), epoch, rlnIdentifier); // Verify all fields have correct lengths according to Nim format: // proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> @@ -154,7 +154,7 @@ describe("RLN Proof Unit Tests", function () { // Verify round-trip: proofToBytes should reconstruct original bytes const reconstructedBytes = proofToBytes(parsedProof); expect(reconstructedBytes).to.deep.equal( - proof, + proof.toBytesLE(), "Reconstructed bytes should match original" ); diff --git a/packages/rln/src/resources/rln_final.arkzkey b/packages/rln/src/resources/rln_final.arkzkey new file mode 100644 index 0000000000..f5454f6ba8 Binary files /dev/null and b/packages/rln/src/resources/rln_final.arkzkey differ diff --git a/packages/rln/src/resources/rln_final.zkey b/packages/rln/src/resources/rln_final.zkey deleted file mode 100644 index 46489a11c0..0000000000 Binary files a/packages/rln/src/resources/rln_final.zkey and /dev/null differ diff --git a/packages/rln/src/rln.ts b/packages/rln/src/rln.ts index 739dcefd3a..dbed447759 100644 --- a/packages/rln/src/rln.ts +++ b/packages/rln/src/rln.ts @@ -1,6 +1,5 @@ import { Logger } from "@waku/utils"; -import init, * as zerokitRLN from "@waku/zerokit-rln-wasm"; -import initUtils from "@waku/zerokit-rln-wasm-utils"; +import init, { WasmRLN } from "@waku/zerokit-rln-wasm"; import { DEFAULT_RATE_LIMIT } from "./contract/constants.js"; import { RLNCredentialsManager } from "./credentials_manager.js"; @@ -17,14 +16,12 @@ export class RLNInstance extends RLNCredentialsManager { */ public static async create(): Promise { try { - await initUtils(); await init(); - zerokitRLN.initPanicHook(); const witnessCalculator = await RLNInstance.loadWitnessCalculator(); const zkey = await RLNInstance.loadZkey(); - const zkRLN = zerokitRLN.newRLN(zkey); + const zkRLN = new WasmRLN(zkey); const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT); return new RLNInstance(zerokit); @@ -63,7 +60,7 @@ export class RLNInstance extends RLNCredentialsManager { public static async loadZkey(): Promise { try { - const url = new URL("./resources/rln_final.zkey", import.meta.url); + const url = new URL("./resources/rln_final.arkzkey", import.meta.url); const response = await fetch(url); if (!response.ok) { diff --git a/packages/rln/src/utils/hash.ts b/packages/rln/src/utils/hash.ts deleted file mode 100644 index 43908ce45d..0000000000 --- a/packages/rln/src/utils/hash.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { hash, poseidonHash as poseidon } from "@waku/zerokit-rln-wasm-utils"; - -import { BytesUtils } from "./bytes.js"; - -export function poseidonHash(...input: Array): Uint8Array { - const inputLen = BytesUtils.writeUIntLE( - new Uint8Array(8), - input.length, - 0, - 8 - ); - const lenPrefixedData = BytesUtils.concatenate(inputLen, ...input); - return poseidon(lenPrefixedData, true); -} - -export function sha256(input: Uint8Array): Uint8Array { - return hash(input, true); -} diff --git a/packages/rln/src/utils/index.ts b/packages/rln/src/utils/index.ts index cf6f3e7bf1..1e53eea2f0 100644 --- a/packages/rln/src/utils/index.ts +++ b/packages/rln/src/utils/index.ts @@ -1,6 +1,5 @@ export { createViemClientFromWindow, RpcClient } from "./rpcClient.js"; export { BytesUtils } from "./bytes.js"; -export { sha256, poseidonHash } from "./hash.js"; export { dateToEpoch, epochIntToBytes, diff --git a/packages/rln/src/utils/merkle.ts b/packages/rln/src/utils/merkle.ts index fda9af680b..2169c93a7f 100644 --- a/packages/rln/src/utils/merkle.ts +++ b/packages/rln/src/utils/merkle.ts @@ -1,5 +1,6 @@ +import { Hasher, WasmFr } from "@waku/zerokit-rln-wasm"; + import { BytesUtils } from "./bytes.js"; -import { poseidonHash } from "./hash.js"; /** * The fixed depth of the Merkle tree used in the RLN contract @@ -26,23 +27,27 @@ export function reconstructMerkleRoot( ); } - let currentValue = BytesUtils.bytes32FromBigInt(leafValue); + let currentValue = WasmFr.fromBytesLE( + BytesUtils.bytes32FromBigInt(leafValue) + ); for (let level = 0; level < MERKLE_TREE_DEPTH; level++) { const bit = (leafIndex >> BigInt(level)) & 1n; - const proofBytes = BytesUtils.bytes32FromBigInt(proof[level]); + const proofFr = WasmFr.fromBytesLE( + BytesUtils.bytes32FromBigInt(proof[level]) + ); if (bit === 0n) { // Current node is a left child: hash(current, proof[level]) - currentValue = poseidonHash(currentValue, proofBytes); + currentValue = Hasher.poseidonHashPair(currentValue, proofFr); } else { // Current node is a right child: hash(proof[level], current) - currentValue = poseidonHash(proofBytes, currentValue); + currentValue = Hasher.poseidonHashPair(proofFr, currentValue); } } - return BytesUtils.toBigInt(currentValue, "little"); + return BytesUtils.toBigInt(currentValue.toBytesLE(), "little"); } /** @@ -60,8 +65,11 @@ export function calculateRateCommitment( const idBytes = BytesUtils.bytes32FromBigInt(idCommitment); const rateLimitBytes = BytesUtils.bytes32FromBigInt(rateLimit); - const hashResult = poseidonHash(idBytes, rateLimitBytes); - return BytesUtils.toBigInt(hashResult, "little"); + const hashResult = Hasher.poseidonHashPair( + WasmFr.fromBytesLE(idBytes), + WasmFr.fromBytesLE(rateLimitBytes) + ); + return BytesUtils.toBigInt(hashResult.toBytesLE(), "little"); } /** diff --git a/packages/rln/src/zerokit.browser.spec.ts b/packages/rln/src/zerokit.browser.spec.ts index 6126c7c6ac..171e1305bf 100644 --- a/packages/rln/src/zerokit.browser.spec.ts +++ b/packages/rln/src/zerokit.browser.spec.ts @@ -10,17 +10,29 @@ describe("@waku/rln", () => { 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]); - }); + memKeys1 + .getCommitment() + .toBytesLE() + .forEach((element, index) => { + expect(element).to.equal(memKeys2.getCommitment().toBytesLE()[index]); + }); + memKeys1 + .getNullifier() + .toBytesLE() + .forEach((element, index) => { + expect(element).to.equal(memKeys2.getNullifier().toBytesLE()[index]); + }); + memKeys1 + .getSecretHash() + .toBytesLE() + .forEach((element, index) => { + expect(element).to.equal(memKeys2.getSecretHash().toBytesLE()[index]); + }); + memKeys1 + .getTrapdoor() + .toBytesLE() + .forEach((element, index) => { + expect(element).to.equal(memKeys2.getTrapdoor().toBytesLE()[index]); + }); }); }); diff --git a/packages/rln/src/zerokit.ts b/packages/rln/src/zerokit.ts index 328412bfe0..77c1a48cec 100644 --- a/packages/rln/src/zerokit.ts +++ b/packages/rln/src/zerokit.ts @@ -1,17 +1,21 @@ -import * as zerokitRLN from "@waku/zerokit-rln-wasm"; -import { generateSeededExtendedMembershipKey } from "@waku/zerokit-rln-wasm-utils"; +import { + ExtendedIdentity, + Hasher, + VecWasmFr, + WasmFr, + WasmRLN, + WasmRLNProof, + WasmRLNWitnessInput +} from "@waku/zerokit-rln-wasm"; import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./contract/constants.js"; -import { IdentityCredential } from "./identity.js"; import { WitnessCalculator } from "./resources/witness_calculator"; -import { BytesUtils } from "./utils/bytes.js"; import { dateToEpochBytes } from "./utils/epoch.js"; -import { poseidonHash, sha256 } from "./utils/hash.js"; import { MERKLE_TREE_DEPTH } from "./utils/merkle.js"; export class Zerokit { public constructor( - private readonly zkRLN: number, + private readonly zkRLN: WasmRLN, private readonly witnessCalculator: WitnessCalculator, public readonly rateLimit: number = DEFAULT_RATE_LIMIT, public readonly rlnIdentifier: Uint8Array = (() => { @@ -22,63 +26,14 @@ export class Zerokit { })() ) {} - public get getZkRLN(): number { - return this.zkRLN; - } - public get getWitnessCalculator(): WitnessCalculator { return this.witnessCalculator; } - public generateSeededIdentityCredential(seed: string): IdentityCredential { + public generateSeededIdentityCredential(seed: string): ExtendedIdentity { const stringEncoder = new TextEncoder(); const seedBytes = stringEncoder.encode(seed); - const memKeys = generateSeededExtendedMembershipKey(seedBytes, true); - return IdentityCredential.fromBytes(memKeys); - } - - private async serializeWitness( - idSecretHash: Uint8Array, - pathElements: Uint8Array[], - identityPathIndex: Uint8Array[], - msg: Uint8Array, - epoch: Uint8Array, - rateLimit: number, - messageId: number // number of message sent by the user in this epoch - ): Promise { - const externalNullifier = poseidonHash( - sha256(epoch), - sha256(this.rlnIdentifier) - ); - const pathElementsBytes = new Uint8Array(8 + pathElements.length * 32); - BytesUtils.writeUIntLE(pathElementsBytes, pathElements.length, 0, 8); - for (let i = 0; i < pathElements.length; i++) { - // We assume that the path elements are already in little-endian format - pathElementsBytes.set(pathElements[i], 8 + i * 32); - } - const identityPathIndexBytes = new Uint8Array( - 8 + identityPathIndex.length * 1 - ); - BytesUtils.writeUIntLE( - identityPathIndexBytes, - identityPathIndex.length, - 0, - 8 - ); - for (let i = 0; i < identityPathIndex.length; i++) { - // We assume that each identity path index is already in little-endian format - identityPathIndexBytes.set(identityPathIndex[i], 8 + i * 1); - } - const x = sha256(msg); - return BytesUtils.concatenate( - idSecretHash, - BytesUtils.writeUIntLE(new Uint8Array(32), rateLimit, 0, 32), - BytesUtils.writeUIntLE(new Uint8Array(32), messageId, 0, 32), - pathElementsBytes, - identityPathIndexBytes, - x, - externalNullifier - ); + return ExtendedIdentity.generateSeeded(seedBytes); } public async generateRLNProof( @@ -90,7 +45,7 @@ export class Zerokit { rateLimit: number, messageId: number // number of message sent by the user in this epoch ): Promise<{ - proof: Uint8Array; + proof: WasmRLNProof; epoch: Uint8Array; rlnIdentifier: Uint8Array; }> { @@ -120,26 +75,37 @@ export class Zerokit { `messageId must be an integer between 0 and ${rateLimit - 1}, got ${messageId}` ); } - - const serializedWitness = await this.serializeWitness( - idSecretHash, - pathElements, - identityPathIndex, - msg, - epoch, - rateLimit, - messageId + const pathElementsVec = new VecWasmFr(); + for (const element of pathElements) { + pathElementsVec.push(WasmFr.fromBytesLE(element)); + } + const identityPathIndexBytes = new Uint8Array(identityPathIndex.length); + for (let i = 0; i < identityPathIndex.length; i++) { + // We assume that each identity path index is already in little-endian format + identityPathIndexBytes.set(identityPathIndex[i], i); + } + const x = Hasher.hashToFieldLE(msg); + const externalNullifier = Hasher.poseidonHashPair( + Hasher.hashToFieldLE(epoch), + Hasher.hashToFieldLE(this.rlnIdentifier) ); - const witnessJson: Record = zerokitRLN.rlnWitnessToJson( - this.zkRLN, - serializedWitness - ) as Record; + const witness = new WasmRLNWitnessInput( + WasmFr.fromBytesLE(idSecretHash), + WasmFr.fromUint(rateLimit), + WasmFr.fromUint(messageId), + pathElementsVec, + identityPathIndexBytes, + x, + externalNullifier + ); + const calculatedWitness: bigint[] = - await this.witnessCalculator.calculateWitness(witnessJson); - const proof = zerokitRLN.generateRLNProofWithWitness( - this.zkRLN, + await this.witnessCalculator.calculateWitness( + witness.toBigIntJson() as Record + ); + const proof = this.zkRLN.generateRLNProofWithWitness( calculatedWitness, - serializedWitness + witness ); return { proof, @@ -151,21 +117,21 @@ export class Zerokit { public verifyRLNProof( signalLength: Uint8Array, signal: Uint8Array, - proof: Uint8Array, + proof: WasmRLNProof, roots: Uint8Array[] ): boolean { if (signalLength.length !== 8) throw new Error("signalLength must be 8 bytes"); - if (proof.length !== 288) throw new Error("proof must be 288 bytes"); if (roots.length == 0) throw new Error("roots array is empty"); if (roots.find((root) => root.length !== 32)) { throw new Error("All roots must be 32 bytes"); } - return zerokitRLN.verifyWithRoots( - this.zkRLN, - BytesUtils.concatenate(proof, signalLength, signal), - BytesUtils.concatenate(...roots) - ); + const rootsVec = new VecWasmFr(); + for (const root of roots) { + rootsVec.push(WasmFr.fromBytesLE(root)); + } + const x = Hasher.hashToFieldLE(signal); + return this.zkRLN.verifyWithRoots(proof, rootsVec, x); } }