feat: implement proof generation and verification

fix: update tests
fix: store merkle proof/root as constant, remove use of RPC in proof gen/verification test
This commit is contained in:
Arseniy Klempner 2025-10-23 13:19:49 -07:00
parent f2ad23ad43
commit 80bf606270
No known key found for this signature in database
GPG Key ID: 51653F18863BD24B
14 changed files with 554 additions and 71 deletions

67
package-lock.json generated
View File

@ -6872,9 +6872,9 @@
"license": "MIT"
},
"node_modules/@wagmi/cli": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@wagmi/cli/-/cli-2.7.0.tgz",
"integrity": "sha512-M0FDVK2/mQSOJne3nG7GiZrecw069GYFY6YGQZbG9IyxPgfOHRgVBvGkeXzGXmb3ezFlzn5jCCIQ2q/9lYh07g==",
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@wagmi/cli/-/cli-2.8.0.tgz",
"integrity": "sha512-2VhDj8u8vwLZwMZ8CX4pTuO0Qm28Z9uH9qOEWgF/xXUCeVV+4e4YsknEyGcoxYwEmTkdlCmuCvMj4up2XK6vxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -7143,6 +7143,11 @@
"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=="
},
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@ -17851,12 +17856,6 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-sha3": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz",
"integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==",
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -32504,9 +32503,9 @@
}
},
"node_modules/viem": {
"version": "2.38.4",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.38.4.tgz",
"integrity": "sha512-qnyPNg6Lz1EEC86si/1dq7GlOyZVFHSgAW+p8Q31R5idnAYCOdTM2q5KLE4/ykMeMXzY0bnp5MWTtR/wjCtWmQ==",
"version": "2.39.0",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.39.0.tgz",
"integrity": "sha512-rCN+IfnMESlrg/iPyyVL+M9NS/BHzyyNy72470tFmbTuscY3iPaZGMtJDcHKKV8TC6HV9DjWk0zWX6cpu0juyA==",
"funding": [
{
"type": "github",
@ -32569,27 +32568,6 @@
}
}
},
"node_modules/viem/node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
@ -34031,6 +34009,12 @@
"@esbuild/win32-x64": "0.21.5"
}
},
"packages/browser-tests/node_modules/js-sha3": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz",
"integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==",
"license": "MIT"
},
"packages/browser-tests/node_modules/undici-types": {
"version": "6.19.8",
"dev": true,
@ -34201,6 +34185,10 @@
}
}
},
"packages/enr/node_modules/js-sha3": {
"version": "0.9.3",
"license": "MIT"
},
"packages/headless-tests": {
"name": "@waku/headless-tests",
"version": "0.1.0",
@ -34269,6 +34257,10 @@
"node": ">=22"
}
},
"packages/message-encryption/node_modules/js-sha3": {
"version": "0.9.3",
"license": "MIT"
},
"packages/proto": {
"name": "@waku/proto",
"version": "0.0.15",
@ -34736,6 +34728,7 @@
"@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",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0",
@ -34757,7 +34750,6 @@
"@types/sinon": "^17.0.3",
"@wagmi/cli": "^2.7.0",
"@waku/build-utils": "^1.0.0",
"@waku/interfaces": "0.0.34",
"@waku/message-encryption": "^0.0.37",
"deep-equal-in-any-order": "^2.0.6",
"fast-check": "^3.23.2",
@ -34917,6 +34909,13 @@
"node": ">=0.3.1"
}
},
"packages/rln/node_modules/js-sha3": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz",
"integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==",
"dev": true,
"license": "MIT"
},
"packages/rln/node_modules/loupe": {
"version": "3.1.3",
"license": "MIT"

View File

@ -24,4 +24,4 @@ if (process.env.CI) {
console.log("Running tests serially. To enable parallel execution update mocha config");
}
module.exports = config;
module.exports = config;

View File

@ -38,6 +38,14 @@ 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
}
],
@ -82,6 +90,12 @@ 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":

View File

@ -60,7 +60,6 @@
"@types/sinon": "^17.0.3",
"@wagmi/cli": "^2.7.0",
"@waku/build-utils": "^1.0.0",
"@waku/interfaces": "0.0.34",
"@waku/message-encryption": "^0.0.37",
"deep-equal-in-any-order": "^2.0.6",
"fast-check": "^3.23.2",
@ -83,6 +82,7 @@
"@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",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0",

View File

@ -29,6 +29,16 @@ import type {
const log = new Logger("rln:keystore");
/**
* Custom replacer function to handle BigInt serialization in JSON.stringify
*/
const bigIntReplacer = (_key: string, value: unknown): unknown => {
if (typeof value === "bigint") {
return value.toString();
}
return value;
};
type NwakuCredential = {
crypto: {
cipher: ICipherModule["function"];
@ -160,7 +170,7 @@ export class Keystore {
}
public toString(): string {
return JSON.stringify(this.data);
return JSON.stringify(this.data, bigIntReplacer);
}
public toObject(): NwakuKeystore {
@ -328,20 +338,23 @@ export class Keystore {
options.identity;
return utf8ToBytes(
JSON.stringify({
treeIndex: options.membership.treeIndex,
identityCredential: {
idCommitment: Array.from(IDCommitment),
idNullifier: Array.from(IDNullifier),
idSecretHash: Array.from(IDSecretHash),
idTrapdoor: Array.from(IDTrapdoor)
JSON.stringify(
{
treeIndex: options.membership.treeIndex,
identityCredential: {
idCommitment: Array.from(IDCommitment),
idNullifier: Array.from(IDNullifier),
idSecretHash: Array.from(IDSecretHash),
idTrapdoor: Array.from(IDTrapdoor)
},
membershipContract: {
chainId: options.membership.chainId,
address: options.membership.address
},
userMessageLimit: options.membership.rateLimit
},
membershipContract: {
chainId: options.membership.chainId,
address: options.membership.address
},
userMessageLimit: options.membership.rateLimit
})
bigIntReplacer
)
);
}
}

View File

@ -0,0 +1,101 @@
import { expect } from "chai";
import { Keystore } from "./keystore/index.js";
import { RLNInstance } from "./rln.js";
import { BytesUtils } from "./utils/index.js";
import {
calculateRateCommitment,
extractPathDirectionsFromProof,
MERKLE_TREE_DEPTH,
reconstructMerkleRoot
} from "./utils/merkle.js";
import { TEST_KEYSTORE_DATA } from "./utils/test_keystore.js";
describe("RLN Proof Integration Tests", function () {
this.timeout(30000);
it("validate stored merkle proof data", function () {
// Convert stored merkle proof strings to bigints
const merkleProof = TEST_KEYSTORE_DATA.merkleProof.map((p) => BigInt(p));
expect(merkleProof).to.be.an("array");
expect(merkleProof).to.have.lengthOf(MERKLE_TREE_DEPTH); // RLN uses fixed depth merkle tree
merkleProof.forEach((element, i) => {
expect(element).to.be.a(
"bigint",
`Proof element ${i} should be a bigint`
);
expect(element).to.not.equal(0n, `Proof element ${i} should not be zero`);
});
});
it("should generate a valid RLN proof", async function () {
const rlnInstance = await RLNInstance.create();
// Load credential from test keystore
const keystore = Keystore.fromString(TEST_KEYSTORE_DATA.keystoreJson);
if (!keystore) {
throw new Error("Failed to load test keystore");
}
const credentialHash = TEST_KEYSTORE_DATA.credentialHash;
const password = TEST_KEYSTORE_DATA.password;
const credential = await keystore.readCredential(credentialHash, password);
if (!credential) {
throw new Error("Failed to unlock credential with provided password");
}
const idCommitment = credential.identity.IDCommitmentBigInt;
const merkleProof = TEST_KEYSTORE_DATA.merkleProof.map((p) => BigInt(p));
const merkleRoot = BigInt(TEST_KEYSTORE_DATA.merkleRoot);
const membershipIndex = BigInt(TEST_KEYSTORE_DATA.membershipIndex);
const rateLimit = BigInt(TEST_KEYSTORE_DATA.rateLimit);
const rateCommitment = calculateRateCommitment(idCommitment, rateLimit);
const proofElementIndexes = extractPathDirectionsFromProof(
merkleProof,
rateCommitment,
merkleRoot
);
if (!proofElementIndexes) {
throw new Error("Failed to extract proof element indexes");
}
expect(proofElementIndexes).to.have.lengthOf(MERKLE_TREE_DEPTH);
const reconstructedRoot = reconstructMerkleRoot(
merkleProof,
membershipIndex,
rateCommitment
);
expect(reconstructedRoot).to.equal(
merkleRoot,
"Reconstructed root should match stored root"
);
const testMessage = new TextEncoder().encode("test");
const proof = await rlnInstance.zerokit.generateRLNProof(
testMessage,
Number(membershipIndex),
new Date(),
credential.identity.IDSecretHash,
merkleProof.map((proof) => BytesUtils.fromBigInt(proof, 32, "little")),
proofElementIndexes.map((index) =>
BytesUtils.writeUIntLE(new Uint8Array(1), index, 0, 1)
),
Number(rateLimit),
0
);
const isValid = rlnInstance.zerokit.verifyRLNProof(
BytesUtils.writeUIntLE(new Uint8Array(8), testMessage.length, 0, 8),
testMessage,
proof,
[BytesUtils.fromBigInt(merkleRoot, 32, "little")]
);
expect(isValid).to.be.true;
});
});

View File

@ -1,5 +1,6 @@
import { Logger } from "@waku/utils";
import init, * as zerokitRLN from "@waku/zerokit-rln-wasm";
import initUtils from "@waku/zerokit-rln-wasm-utils";
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
import { RLNCredentialsManager } from "./credentials_manager.js";
@ -16,6 +17,7 @@ export class RLNInstance extends RLNCredentialsManager {
*/
public static async create(): Promise<RLNInstance> {
try {
await initUtils();
await init();
zerokitRLN.initPanicHook();

View File

@ -49,6 +49,43 @@ export class BytesUtils {
return result;
}
/**
* Convert a BigInt to a Uint8Array with configurable output endianness
* @param value - The BigInt to convert
* @param byteLength - The desired byte length of the output (optional, auto-calculated if not provided)
* @param outputEndianness - Endianness of the output bytes ('big' or 'little')
* @returns Uint8Array representation of the BigInt
*/
public static fromBigInt(
value: bigint,
byteLength: number,
outputEndianness: "big" | "little" = "little"
): Uint8Array {
if (value < 0n) {
throw new Error("Cannot convert negative BigInt to bytes");
}
if (value === 0n) {
return new Uint8Array(byteLength);
}
const result = new Uint8Array(byteLength);
let workingValue = value;
// Extract bytes in big-endian order
for (let i = byteLength - 1; i >= 0; i--) {
result[i] = Number(workingValue & 0xffn);
workingValue = workingValue >> 8n;
}
// If we need little-endian output, reverse the array
if (outputEndianness === "little") {
result.reverse();
}
return result;
}
/**
* Writes an unsigned integer to a buffer in little-endian format
*/

View File

@ -1,5 +1,7 @@
import { Logger } from "@waku/utils";
import { BytesUtils } from "./bytes.js";
const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds
const log = new Logger("rln:epoch");
@ -15,11 +17,7 @@ export function dateToEpoch(
}
export function epochIntToBytes(epoch: number): Uint8Array {
const bytes = new Uint8Array(32);
const db = new DataView(bytes.buffer);
db.setUint32(0, epoch, true);
log.info("encoded epoch", epoch, bytes);
return bytes;
return BytesUtils.writeUIntLE(new Uint8Array(32), epoch, 0, 32);
}
export function epochBytesToInt(bytes: Uint8Array): number {

View File

@ -1,4 +1,4 @@
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
import { hash, poseidonHash as poseidon } from "@waku/zerokit-rln-wasm-utils";
import { BytesUtils } from "./bytes.js";
@ -10,16 +10,9 @@ export function poseidonHash(...input: Array<Uint8Array>): Uint8Array {
8
);
const lenPrefixedData = BytesUtils.concatenate(inputLen, ...input);
return zerokitRLN.poseidonHash(lenPrefixedData);
return poseidon(lenPrefixedData, true);
}
export function sha256(input: Uint8Array): Uint8Array {
const inputLen = BytesUtils.writeUIntLE(
new Uint8Array(8),
input.length,
0,
8
);
const lenPrefixedData = BytesUtils.concatenate(inputLen, input);
return zerokitRLN.hash(lenPrefixedData);
return hash(input, true);
}

View File

@ -0,0 +1,179 @@
import { BytesUtils } from "./bytes.js";
import { poseidonHash } from "./hash.js";
/**
* The fixed depth of the Merkle tree used in the RLN contract
* This is a constant that will never change for the on-chain implementation
*/
export const MERKLE_TREE_DEPTH = 20;
/**
* Reconstructs a Merkle tree root from a proof and leaf information
*
* @param proof - Array of MERKLE_TREE_DEPTH bigint elements representing the Merkle proof
* @param leafIndex - The index of the leaf in the tree (used to determine left/right positioning)
* @param leafValue - The value of the leaf (typically the rate commitment)
* @returns The reconstructed root as a bigint
*/
export function reconstructMerkleRoot(
proof: readonly bigint[],
leafIndex: bigint,
leafValue: bigint
): bigint {
if (proof.length !== MERKLE_TREE_DEPTH) {
throw new Error(
`Expected proof of length ${MERKLE_TREE_DEPTH}, got ${proof.length}`
);
}
let currentValue = leafValue;
// Process each level of the tree (0 to MERKLE_TREE_DEPTH-1)
for (let level = 0; level < MERKLE_TREE_DEPTH; level++) {
// Check if bit `level` is set in the leaf index
const bit = (leafIndex >> BigInt(level)) & 1n;
// Convert bigints to Uint8Array for hashing
const currentBytes = bigIntToBytes32(currentValue);
const proofBytes = bigIntToBytes32(proof[level]);
let hashResult: Uint8Array;
if (bit === 0n) {
// Current node is a left child: hash(current, proof[level])
hashResult = poseidonHash(currentBytes, proofBytes);
} else {
// Current node is a right child: hash(proof[level], current)
hashResult = poseidonHash(proofBytes, currentBytes);
}
// Convert hash result back to bigint for next iteration
currentValue = BytesUtils.toBigInt(hashResult, "little");
}
return currentValue;
}
/**
* Extracts index information from a Merkle proof by attempting to reconstruct
* the root with different possible indices and comparing against the expected root
*
* @param proof - Array of MERKLE_TREE_DEPTH bigint elements representing the Merkle proof
* @param leafValue - The value of the leaf (typically the rate commitment)
* @param expectedRoot - The expected root to match against
* @param maxIndex - Maximum index to try (default: 2^MERKLE_TREE_DEPTH - 1)
* @returns The index that produces the expected root, or null if not found
*/
function extractIndexFromProof(
proof: readonly bigint[],
leafValue: bigint,
expectedRoot: bigint,
maxIndex: bigint = (1n << BigInt(MERKLE_TREE_DEPTH)) - 1n
): bigint | null {
// Try different indices to see which one produces the expected root
for (let index = 0n; index <= maxIndex; index++) {
try {
const reconstructedRoot = reconstructMerkleRoot(proof, index, leafValue);
if (reconstructedRoot === expectedRoot) {
return index;
}
} catch (error) {
// Continue trying other indices if reconstruction fails
continue;
}
}
return null;
}
/**
* Calculates the rate commitment from an ID commitment and rate limit
* This matches the contract's calculation: PoseidonT3.hash([idCommitment, rateLimit])
*
* @param idCommitment - The identity commitment as a bigint
* @param rateLimit - The rate limit as a bigint
* @returns The rate commitment as a bigint
*/
export function calculateRateCommitment(
idCommitment: bigint,
rateLimit: bigint
): bigint {
const idBytes = bigIntToBytes32(idCommitment);
const rateLimitBytes = bigIntToBytes32(rateLimit);
const hashResult = poseidonHash(idBytes, rateLimitBytes);
return BytesUtils.toBigInt(hashResult, "little");
}
/**
* Converts a bigint to a 32-byte Uint8Array in little-endian format
*
* @param value - The bigint value to convert
* @returns 32-byte Uint8Array representation
*/
function bigIntToBytes32(value: bigint): Uint8Array {
const bytes = new Uint8Array(32);
let temp = value;
for (let i = 0; i < 32; i++) {
bytes[i] = Number(temp & 0xffn);
temp >>= 8n;
}
return bytes;
}
/**
* Extracts the path direction bits from a Merkle proof by finding the leaf index
* that produces the expected root, then converting that index to path directions
*
* @param proof - Array of MERKLE_TREE_DEPTH bigint elements representing the Merkle proof
* @param leafValue - The value of the leaf (typically the rate commitment)
* @param expectedRoot - The expected root to match against
* @param maxIndex - Maximum index to try (default: 2^MERKLE_TREE_DEPTH - 1)
* @returns Array of MERKLE_TREE_DEPTH numbers (0 or 1) representing path directions, or null if no valid path found
* - 0 means the node is a left child (hash order: current, sibling)
* - 1 means the node is a right child (hash order: sibling, current)
*/
export function extractPathDirectionsFromProof(
proof: readonly bigint[],
leafValue: bigint,
expectedRoot: bigint,
maxIndex: bigint = (1n << BigInt(MERKLE_TREE_DEPTH)) - 1n
): number[] | null {
// First, find the leaf index that produces the expected root
const leafIndex = extractIndexFromProof(
proof,
leafValue,
expectedRoot,
maxIndex
);
if (leafIndex === null) {
return null;
}
// Convert the leaf index to path directions
return getPathDirectionsFromIndex(leafIndex);
}
/**
* Converts a leaf index to an array of path direction bits
*
* @param leafIndex - The index of the leaf in the tree
* @returns Array of MERKLE_TREE_DEPTH numbers (0 or 1) representing path directions
* - 0 means the node is a left child (hash order: current, sibling)
* - 1 means the node is a right child (hash order: sibling, current)
*/
function getPathDirectionsFromIndex(leafIndex: bigint): number[] {
const pathDirections: number[] = [];
// For each level (0 to MERKLE_TREE_DEPTH-1), extract the bit that determines left/right
for (let level = 0; level < MERKLE_TREE_DEPTH; level++) {
// Check if bit `level` is set in the leaf index
const bit = (leafIndex >> BigInt(level)) & 1n;
pathDirections.push(Number(bit));
}
return pathDirections;
}

View File

@ -0,0 +1,33 @@
export const TEST_KEYSTORE_DATA = {
keystoreJson:
'{"application":"waku-rln-relay","appIdentifier":"01234567890abcdef","version":"0.2","credentials":{"E0A8AC077B95F64C1B2C4B116468B22EFA3B1CFF250069AE07422F645BAA555E":{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"96aff104d7bb23cefb57a4c5e816a3b9"},"ciphertext":"1ae2c7a47274d12d6a4b439da48abfa89be29e4ba3308d153e2e808d3e120cc85da472ab1e0278c945231092162d31d753ecb48484ac0c3a7efe6380d08f5dedecc9cda26bd156a30d232b9da4313c5ec92b21cd3dc3ca03cff68afde94a063799b658cc3e4a5c648e620d584a8a184d2d473e3e94c897e21e0de7580639dcf40c0133f36896ac5bee2dd5fe8810a5441e31e1938ecc4b195db57c1b6d320a374508406dfb7a4879081b70100140515b4c6c551f25f9b4c9a7214ac2dc222410bf74666407343dfd4af477c85cf2f316bb7a512a88948d88f5474374563d51d02c13eede6b6cf64fab7991e529157d7de39033099d26f323d9710159b47d2511695b4fb428e3b02c760e1470a3ece712c6a03692d067e0e17930bc25ce7dc4ad2634e07ef51fa7369de6b4d495c7ae1d8ad8dccdd2fa12802db4203c527887adf5eb42e2551e120b8a455892d0ac9369faf708465a983c03c7c8f77c268f85cacc7b718a1e9e2800b160ca1f7a78f2c160cbc97396f5dfe0e0f3b35addb4f8d667021c79eec5248122d8c983075b9e8ca20679e90a12bdbeefb33df21523b4e1ea7ab57ddc706b43bf4827fbc3530d20cb906468af5c5c31ac08815f3ed1d00341be7e287a3fb7ef67aecf2e56f694c51ba6db8641ac873e26659c92a8527c42df2d5ac15ff6201bdfa8a5ee34b6a90ff864fba89370a8c51efcb4ed1b69f3ed0e37ee97c66eb84763f107e1214e088e3149b2433a8da595293343b2290b0a84b7f796b70005d1672446d98d45da7c89c3eb8d91ece94ee41099f9f43c6810ce71d9f75ac3dffe1de0c79e40baad486ecaefbd0cc0e89aed7e0a16ea271a371d3f5927a1c7b813608de5715692e58322260a4bcd4ccba4b2376df01f58645c16a7b37c8473b94c7577ae774e5c72132ed15507ab2027ddabf137aa417b134b653eda247314","kdf":"pbkdf2","kdfparams":{"dklen":32,"c":262144,"prf":"hmac-sha256","salt":"5f2081f089e9e277873bf1f538c60d714749a2bb910d8f1ed119d8d403235a8c"},"mac":"8d0667893b7d3b5f0b37c43edef616a8d295dc58292c98655eec8b5fe2ad69c3"}}}}',
credentialHash:
"E0A8AC077B95F64C1B2C4B116468B22EFA3B1CFF250069AE07422F645BAA555E",
password: "12345678",
merkleProof: [
"21837427992620339064281119305700224965155897361776876451171527491637273262703",
"2849928341676773476316761863425436901389023422598778907382563142042850204484",
"21699429914184421678079077958020273488709892845081201722564329942861605328226",
"8522396354694062508299995669286882048091268903835874022564768254605186873188",
"4967828252976847302563643214799688359334626491919847999565033460501719790119",
"985039452502497454598906195897243897432778848314526706136284672198477696437",
"3565679202982155915846059790230166166058846233389836779083891288518797717794",
"1241870589869015758600129850815671823696180350556207862318506998039540071293",
"21551820661461729022865262380882070649935529853313286572328683688269863701601",
"16870197621778677478951480138572599814910741341994641594346262317677658226992",
"12413880268183407374852357075976609371175688755676981206018884971008854919922",
"14271763308400718165336499097156975241954733520325982997864342600795471836726",
"20066985985293572387227381049700832219069292839614107140851619262827735677018",
"9394776414966240069580838672673694685292165040808226440647796406499139370960",
"11331146992410411304059858900317123658895005918277453009197229807340014528524",
"15819538789928229930262697811477882737253464456578333862691129291651619515538",
"19217088683336594659449020493828377907203207941212636669271704950158751593251",
"21035245323335827719745544373081896983162834604456827698288649288827293579666",
"6939770416153240137322503476966641397417391950902474480970945462551409848591",
"10941962436777715901943463195175331263348098796018438960955633645115732864202"
],
merkleRoot:
"3281768056038133311055294993138164819435524453040629949691729675724822631973",
membershipIndex: "703",
rateLimit: "300"
};

View File

@ -1,14 +1,21 @@
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
import { generateSeededExtendedMembershipKey } from "@waku/zerokit-rln-wasm-utils";
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
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 { dateToEpoch, epochIntToBytes } from "./utils/epoch.js";
import { poseidonHash, sha256 } from "./utils/hash.js";
export class Zerokit {
public constructor(
private readonly zkRLN: number,
private readonly witnessCalculator: WitnessCalculator,
private readonly _rateLimit: number = DEFAULT_RATE_LIMIT
private readonly _rateLimit: number = DEFAULT_RATE_LIMIT,
private readonly rlnIdentifier: Uint8Array = new TextEncoder().encode(
"rln/waku-rln-relay/v2.0.0"
)
) {}
public get getZkRLN(): number {
@ -26,10 +33,117 @@ export class Zerokit {
public generateSeededIdentityCredential(seed: string): IdentityCredential {
const stringEncoder = new TextEncoder();
const seedBytes = stringEncoder.encode(seed);
const memKeys = zerokitRLN.generateSeededExtendedMembershipKey(
this.zkRLN,
seedBytes
);
const memKeys = generateSeededExtendedMembershipKey(seedBytes, true);
return IdentityCredential.fromBytes(memKeys);
}
public async serializeWitness(
idSecretHash: Uint8Array,
pathElements: Uint8Array[],
identityPathIndex: Uint8Array[],
x: Uint8Array,
epoch: Uint8Array,
rateLimit: number,
messageId: number // number of message sent by the user in this epoch
): Promise<Uint8Array> {
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);
}
return BytesUtils.concatenate(
idSecretHash,
BytesUtils.writeUIntLE(new Uint8Array(32), rateLimit, 0, 32),
BytesUtils.writeUIntLE(new Uint8Array(32), messageId, 0, 32),
pathElementsBytes,
identityPathIndexBytes,
x,
externalNullifier
);
}
public async generateRLNProof(
msg: Uint8Array,
index: number, // index of the leaf in the merkle tree
epoch: Uint8Array | Date | undefined,
idSecretHash: Uint8Array,
pathElements: Uint8Array[],
identityPathIndex: Uint8Array[],
rateLimit: number,
messageId: number // number of message sent by the user in this epoch
): Promise<Uint8Array> {
if (epoch === undefined) {
epoch = epochIntToBytes(dateToEpoch(new Date()));
} else if (epoch instanceof Date) {
epoch = epochIntToBytes(dateToEpoch(epoch));
}
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 (
rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
rateLimit > 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 x = sha256(msg);
const serializedWitness = await this.serializeWitness(
idSecretHash,
pathElements,
identityPathIndex,
x,
epoch,
rateLimit,
messageId
);
const witnessJson: Record<string, unknown> = zerokitRLN.rlnWitnessToJson(
this.zkRLN,
serializedWitness
) as Record<string, unknown>;
const calculatedWitness: bigint[] =
await this.witnessCalculator.calculateWitness(witnessJson);
return zerokitRLN.generateRLNProofWithWitness(
this.zkRLN,
calculatedWitness,
serializedWitness
);
}
public verifyRLNProof(
signalLength: Uint8Array,
signal: Uint8Array,
proof: Uint8Array,
roots: Uint8Array[]
): boolean {
if (signalLength.length !== 8)
throw new Error("signalLength must be 8 bytes");
return zerokitRLN.verifyWithRoots(
this.zkRLN,
BytesUtils.concatenate(proof, signalLength, signal),
BytesUtils.concatenate(...roots)
);
}
}