diff --git a/package-lock.json b/package-lock.json index 33f6173..9a670a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "license": "MIT OR Apache-2.0", "dependencies": { "@chainsafe/bls-keystore": "^3.0.0", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "@waku/core": "^0.0.25", "@waku/utils": "^0.0.13", "@waku/zerokit-rln-wasm": "^0.0.13", @@ -1980,11 +1982,11 @@ } }, "node_modules/@noble/curves": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", "dependencies": { - "@noble/hashes": "1.3.3" + "@noble/hashes": "1.4.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -2002,9 +2004,9 @@ ] }, "node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "engines": { "node": ">= 16" }, @@ -2179,6 +2181,28 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "dependencies": { + "@noble/hashes": "1.3.3" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/bip39": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.2.tgz", @@ -2191,6 +2215,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sitespeed.io/tracium": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@sitespeed.io/tracium/-/tracium-0.3.3.tgz", @@ -5642,6 +5677,28 @@ "@scure/bip39": "1.2.2" } }, + "node_modules/ethereum-cryptography/node_modules/@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "dependencies": { + "@noble/hashes": "1.3.3" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ethers": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", @@ -13837,11 +13894,11 @@ } }, "@noble/curves": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", "requires": { - "@noble/hashes": "1.3.3" + "@noble/hashes": "1.4.0" } }, "@noble/ed25519": { @@ -13850,9 +13907,9 @@ "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==" }, "@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" }, "@noble/secp256k1": { "version": "1.7.1", @@ -13968,6 +14025,21 @@ "@noble/curves": "~1.3.0", "@noble/hashes": "~1.3.2", "@scure/base": "~1.1.4" + }, + "dependencies": { + "@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "requires": { + "@noble/hashes": "1.3.3" + } + }, + "@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + } } }, "@scure/bip39": { @@ -13977,6 +14049,13 @@ "requires": { "@noble/hashes": "~1.3.2", "@scure/base": "~1.1.4" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + } } }, "@sitespeed.io/tracium": { @@ -16609,6 +16688,21 @@ "@noble/hashes": "1.3.3", "@scure/bip32": "1.3.3", "@scure/bip39": "1.2.2" + }, + "dependencies": { + "@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "requires": { + "@noble/hashes": "1.3.3" + } + }, + "@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + } } }, "ethers": { diff --git a/package.json b/package.json index 643b341..5af4205 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,8 @@ }, "dependencies": { "@chainsafe/bls-keystore": "^3.0.0", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "@waku/core": "^0.0.25", "@waku/utils": "^0.0.13", "@waku/zerokit-rln-wasm": "^0.0.13", @@ -143,4 +145,4 @@ "lodash": "^4.17.21", "uuid": "^9.0.1" } -} \ No newline at end of file +} diff --git a/src/codec.spec.ts b/src/codec.spec.ts index df41f67..75a6a07 100644 --- a/src/codec.spec.ts +++ b/src/codec.spec.ts @@ -54,7 +54,8 @@ describe("RLN codec with version 0", () => { encoder: createEncoder({ contentTopic: TestContentTopic }), rlnInstance, index, - credential + credential, + fetchMembersFromService: false }); const rlnDecoder = createRLNDecoder({ rlnInstance, @@ -384,7 +385,8 @@ describe("RLN codec with version 0 and meta setter", () => { encoder: createEncoder({ contentTopic: TestContentTopic, metaSetter }), rlnInstance, index, - credential + credential, + fetchMembersFromService: false }); const rlnDecoder = createRLNDecoder({ rlnInstance, diff --git a/src/codec.ts b/src/codec.ts index 90d27bd..54fe96e 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -16,15 +16,18 @@ const log = debug("waku:rln:encoder"); export class RLNEncoder implements IEncoder { private readonly idSecretHash: Uint8Array; + private readonly idCommitment: bigint; constructor( private encoder: IEncoder, private rlnInstance: RLNInstance, private index: number, - identityCredential: IdentityCredential + identityCredential: IdentityCredential, + private readonly fetchMembersFromService: boolean = false ) { if (index < 0) throw "invalid membership index"; this.idSecretHash = identityCredential.IDSecretHash; + this.idCommitment = identityCredential.IDCommitmentBigInt; } async toWire(message: IMessage): Promise { @@ -49,7 +52,9 @@ export class RLNEncoder implements IEncoder { signal, this.index, message.timestamp, - this.idSecretHash + this.idSecretHash, + this.idCommitment, + this.fetchMembersFromService ); return proof; } @@ -72,6 +77,7 @@ type RLNEncoderOptions = { rlnInstance: RLNInstance; index: number; credential: IdentityCredential; + fetchMembersFromService: boolean; }; export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => { @@ -79,7 +85,8 @@ export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => { options.encoder, options.rlnInstance, options.index, - options.credential + options.credential, + options.fetchMembersFromService ); }; diff --git a/src/contract/rln_contract.spec.ts b/src/contract/rln_contract.spec.ts index 4b6e1d5..2dee69f 100644 --- a/src/contract/rln_contract.spec.ts +++ b/src/contract/rln_contract.spec.ts @@ -19,7 +19,8 @@ describe("RLN Contract abstraction", () => { const voidSigner = new ethers.VoidSigner(SEPOLIA_CONTRACT.address); const rlnContract = new RLNContract(rlnInstance, { registryAddress: SEPOLIA_CONTRACT.address, - signer: voidSigner + signer: voidSigner, + fetchMembersFromService: false }); rlnContract["storageContract"] = { @@ -43,7 +44,8 @@ describe("RLN Contract abstraction", () => { const voidSigner = new ethers.VoidSigner(SEPOLIA_CONTRACT.address); const rlnContract = new RLNContract(rlnInstance, { registryAddress: SEPOLIA_CONTRACT.address, - signer: voidSigner + signer: voidSigner, + fetchMembersFromService: false }); rlnContract["storageIndex"] = 1; diff --git a/src/contract/rln_contract.ts b/src/contract/rln_contract.ts index 65ca2ed..b23bffb 100644 --- a/src/contract/rln_contract.ts +++ b/src/contract/rln_contract.ts @@ -22,6 +22,7 @@ type Signer = ethers.Signer; type RLNContractOptions = { signer: Signer; registryAddress: string; + fetchMembersFromService: boolean; }; type RLNStorageOptions = { @@ -54,7 +55,9 @@ export class RLNContract { const rlnContract = new RLNContract(rlnInstance, options); await rlnContract.initStorageContract(options.signer); - await rlnContract.fetchMembers(rlnInstance); + if (!options.fetchMembersFromService) { + await rlnContract.fetchMembers(rlnInstance); + } rlnContract.subscribeToMembers(rlnInstance); return rlnContract; @@ -80,8 +83,9 @@ export class RLNContract { ): Promise { const storageIndex = options?.storageIndex ? options.storageIndex - : await this.registryContract.usingStorageIndex(); - const storageAddress = await this.registryContract.storages(storageIndex); + : await this.registryContract.callStatic.usingStorageIndex(); + const storageAddress = + await this.registryContract.callStatic.storages(storageIndex); if (!storageAddress || storageAddress === ethers.constants.AddressZero) { throw Error("No RLN Storage initialized on registry contract."); @@ -95,7 +99,8 @@ export class RLNContract { ); this._membersFilter = this.storageContract.filters.MemberRegistered(); - this.deployBlock = await this.storageContract.deployedBlockNumber(); + this.deployBlock = + await this.storageContract.callStatic.deployedBlockNumber(); } public get registry(): ethers.Contract { diff --git a/src/rln.ts b/src/rln.ts index 633cb9d..a3c6172 100644 --- a/src/rln.ts +++ b/src/rln.ts @@ -76,6 +76,7 @@ type StartRLNOptions = { * If provided used for validating the network chainId and connecting to registry contract. */ credentials?: EncryptedCredentials | DecryptedCredentials; + fetchMembersFromService?: boolean; }; type RegisterMembershipOptions = @@ -84,6 +85,7 @@ type RegisterMembershipOptions = type WakuRLNEncoderOptions = WakuEncoderOptions & { credentials: EncryptedCredentials | DecryptedCredentials; + fetchMembersFromService: boolean; }; export class RLNInstance { @@ -129,7 +131,8 @@ export class RLNInstance { this._signer = signer!; this._contract = await RLNContract.init(this, { registryAddress: registryAddress!, - signer: signer! + signer: signer!, + fetchMembersFromService: options.fetchMembersFromService ?? false }); this.started = true; } finally { @@ -244,7 +247,8 @@ export class RLNInstance { encoder: createEncoder(options), rlnInstance: this, index: credentials.membership.treeIndex, - credential: credentials.identity + credential: credentials.identity, + fetchMembersFromService: options.fetchMembersFromService }); } diff --git a/src/utils/hash.ts b/src/utils/hash.ts index 78422e2..4190543 100644 --- a/src/utils/hash.ts +++ b/src/utils/hash.ts @@ -1,3 +1,7 @@ +import * as mod from "@noble/curves/abstract/modular"; +import { bytesToNumberLE, numberToBytesLE } from "@noble/curves/abstract/utils"; +import { bn254 } from "@noble/curves/bn254"; +import { keccak_256 } from "@noble/hashes/sha3"; import * as zerokitRLN from "@waku/zerokit-rln-wasm"; import { concatenate, writeUIntLE } from "./bytes.js"; @@ -13,3 +17,16 @@ export function sha256(input: Uint8Array): Uint8Array { const lenPrefixedData = concatenate(inputLen, input); return zerokitRLN.hash(lenPrefixedData); } + +export function hashToBN254(data: Uint8Array): Uint8Array { + // Hash the data using Keccak256 + const hashed = keccak_256(data); + + // Convert hash to a field element (big integer modulo BN254 field order) + const fieldElement = mod.mod(bytesToNumberLE(hashed), bn254.CURVE.Fp.ORDER); + + // Convert the field element back to bytes, ensuring 32 bytes length + const fixedLenBytes = numberToBytesLE(fieldElement, 32); + + return fixedLenBytes; +} diff --git a/src/zerokit.ts b/src/zerokit.ts index feae304..da9b1d5 100644 --- a/src/zerokit.ts +++ b/src/zerokit.ts @@ -1,9 +1,11 @@ +import { concatBytes, hexToBytes } from "@noble/curves/abstract/utils"; import type { IRateLimitProof } from "@waku/interfaces"; import * as zerokitRLN from "@waku/zerokit-rln-wasm"; import { IdentityCredential } from "./identity.js"; import { Proof, proofToBytes } from "./proof.js"; import { WitnessCalculator } from "./resources/witness_calculator.js"; +import { hashToBN254 } from "./utils/hash.js"; import { concatenate, dateToEpoch, @@ -78,7 +80,9 @@ export class Zerokit { msg: Uint8Array, index: number, epoch: Uint8Array | Date | undefined, - idSecretHash: Uint8Array + idSecretHash: Uint8Array, + idCommitment?: bigint, + fetchMembersFromService: boolean = false ): Promise { if (epoch == undefined) { epoch = epochIntToBytes(dateToEpoch(new Date())); @@ -96,10 +100,61 @@ export class Zerokit { epoch, idSecretHash ); - const rlnWitness = zerokitRLN.getSerializedRLNWitness( - this.zkRLN, - serialized_msg - ); + + let rlnWitness; + if (!fetchMembersFromService) { + // Assumes merkle tree is maintained locally + rlnWitness = zerokitRLN.getSerializedRLNWitness( + this.zkRLN, + serialized_msg + ); + } else { + // Fetch merkle data from a service provider + if (!idCommitment) { + throw new Error( + "Must provide ID commitment if using service to get proof" + ); + } + const RLN_IDENTIFIER: Uint8Array = new TextEncoder().encode( + "zerokit/rln/010203040506070809" + ); + + const fetchUrl = `${process.env.MERKLE_PROOF_SERVICE_URL || "http://localhost:8645/debug/v1/merkleProof"}/${idCommitment}`; + const response = await fetch(fetchUrl); + + const proofData = await response.json(); + const pathElements: Uint8Array[] = proofData.pathElements.map(hexToBytes); + + // Serialize number of path lements and each hash in path elements to a single Uint8Array + const pathElementsBytes = new Uint8Array(8 + pathElements.length * 32); + writeUIntLE(pathElementsBytes, pathElements.length, 0, 8); + for (let i = 0; i < pathElements.length; i++) { + pathElementsBytes.set(pathElements[i], 8 + i * 32); + } + // Serialize number of path indexes and the indexes themselves to a single Uint8Array + const pathIndexesBytes = new Uint8Array(8 + proofData.pathIndexes.length); + writeUIntLE(pathIndexesBytes, proofData.pathIndexes.length, 0, 8); + for (let i = 0; i < proofData.pathIndexes.length; i++) { + writeUIntLE( + pathIndexesBytes, + parseInt(proofData.pathIndexes[i], 10), + 8 + i, + 1 + ); + } + + const hashToFieldMsg = hashToBN254(serialized_msg); + const hashToFieldRLNIdentifier = hashToBN254(RLN_IDENTIFIER); + // Append all Uint8Array elements to a single Uint8Array + rlnWitness = concatBytes( + idSecretHash, + pathElementsBytes, + pathIndexesBytes, + hashToFieldMsg, + epoch, + hashToFieldRLNIdentifier + ); + } const inputs = zerokitRLN.RLNWitnessToJson(this.zkRLN, rlnWitness); const calculatedWitness = await this.witnessCalculator.calculateWitness( inputs,