Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 33 additions & 34 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/rln/.mocharc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
14 changes: 14 additions & 0 deletions packages/rln/karma.conf.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
],

Expand Down Expand Up @@ -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":
Expand Down
2 changes: 1 addition & 1 deletion packages/rln/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @waku/interfaces package was removed from devDependencies, but there's no explanation in the PR description about why this dependency is no longer needed. If it was truly unused, this is good cleanup. However, if there are references to it elsewhere in the codebase, this could cause build failures. Please verify that this removal is intentional and that no code depends on this package.

Copilot uses AI. Check for mistakes.
"deep-equal-in-any-order": "^2.0.6",
"fast-check": "^3.23.2",
Expand All @@ -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",
Expand Down
47 changes: 33 additions & 14 deletions packages/rln/src/keystore/keystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,15 @@ export class Keystore {
}

public toString(): string {
return JSON.stringify(this.data);
// Custom replacer function to handle BigInt serialization
const bigIntReplacer = (_key: string, value: unknown): unknown => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove this duplication as there is literally the same function below

if (typeof value === "bigint") {
return value.toString();
}
return value;
};

return JSON.stringify(this.data, bigIntReplacer);
}

public toObject(): NwakuKeystore {
Expand Down Expand Up @@ -327,21 +335,32 @@ export class Keystore {
const { IDCommitment, IDNullifier, IDSecretHash, IDTrapdoor } =
options.identity;

// Custom replacer function to handle BigInt serialization
const bigIntReplacer = (_key: string, value: unknown): unknown => {
if (typeof value === "bigint") {
return value.toString();
}
return value;
};

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)
},
membershipContract: {
chainId: options.membership.chainId,
address: options.membership.address
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
},
userMessageLimit: options.membership.rateLimit
})
bigIntReplacer
)
);
}
}
101 changes: 101 additions & 0 deletions packages/rln/src/proof.spec.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: for loop would be better here

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
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function call to extractPathDirectionsFromProof does not pass the known membershipIndex as maxIndex parameter, which means it could potentially iterate through up to 2^20 (1,048,576) indices if the proof doesn't match early on. This could cause performance issues and test timeouts.

Consider passing the known index or a reasonable upper bound: extractPathDirectionsFromProof(merkleProof, rateCommitment, merkleRoot, membershipIndex + 100n) to limit the search space.

Suggested change
merkleRoot
merkleRoot,
membershipIndex + 100n

Copilot uses AI. Check for mistakes.
);
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")),
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name shadowing: the parameter name proof in the map callback shadows the outer variable merkleProof being mapped. This is confusing and could lead to errors. Consider renaming to: merkleProof.map((element) => BytesUtils.fromBigInt(element, 32, "little"))

Suggested change
merkleProof.map((proof) => BytesUtils.fromBigInt(proof, 32, "little")),
merkleProof.map((element) => BytesUtils.fromBigInt(element, 32, "little")),

Copilot uses AI. Check for mistakes.
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;
});
});
2 changes: 2 additions & 0 deletions packages/rln/src/rln.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -16,6 +17,7 @@ export class RLNInstance extends RLNCredentialsManager {
*/
public static async create(): Promise<RLNInstance> {
try {
await initUtils();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: how fast does it load?

await init();
zerokitRLN.initPanicHook();

Expand Down
Loading
Loading