feat: add option to use service for merkle proof in generateRLNProof

This commit is contained in:
Arseniy Klempner 2024-03-28 00:23:08 -07:00
parent 10d463a512
commit 6b20df5257
9 changed files with 221 additions and 33 deletions

122
package-lock.json generated
View File

@ -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": {

View File

@ -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"
}
}
}

View File

@ -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,

View File

@ -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<Uint8Array | undefined> {
@ -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
);
};

View File

@ -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;

View File

@ -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<void> {
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 {

View File

@ -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
});
}

View File

@ -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;
}

View File

@ -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<IRateLimitProof> {
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,