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
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@
"proto",
"*.spec.ts",
"*.log",
"CHANGELOG.md"
"CHANGELOG.md",
"test_keystore.ts"
],
"patterns": [
{
Expand Down
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 @@ -61,7 +61,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 @@ -84,6 +83,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
41 changes: 27 additions & 14 deletions packages/rln/src/keystore/keystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -160,7 +170,7 @@ export class Keystore {
}

public toString(): string {
return JSON.stringify(this.data);
return JSON.stringify(this.data, bigIntReplacer);
}

public toObject(): NwakuKeystore {
Expand Down Expand Up @@ -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)
},
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
)
);
}
}
104 changes: 104 additions & 0 deletions packages/rln/src/proof.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { expect } from "chai";

import { Keystore } from "./keystore/index.js";
import { RLNInstance } from "./rln.js";
import { BytesUtils } from "./utils/index.js";
import {
calculateRateCommitment,
getPathDirectionsFromIndex,
MERKLE_TREE_DEPTH,
reconstructMerkleRoot
} from "./utils/merkle.js";
import {
TEST_CREDENTIALS,
TEST_KEYSTORE_PASSWORD,
TEST_MERKLE_ROOT
} from "./utils/test_keystore.js";

describe("RLN Proof Integration Tests", function () {
this.timeout(30000);

TEST_CREDENTIALS.forEach((credential, index) => {
describe(`Credential ${index + 1}`, function () {
it("validate stored merkle proof data", function () {
const merkleProof = credential.merkleProof.map((p) => BigInt(p));

expect(merkleProof).to.be.an("array");
expect(merkleProof).to.have.lengthOf(MERKLE_TREE_DEPTH);

for (let i = 0; i < merkleProof.length; i++) {
const element = merkleProof[i];
expect(element).to.be.a(
"bigint",
`Proof element ${i} should be a bigint`
);
// Note: First element can be 0 for some tree positions (e.g., credential 3)
}
});

it("should generate a valid RLN proof", async function () {
const rlnInstance = await RLNInstance.create();
const keystore = Keystore.fromString(credential.keystoreJson);
if (!keystore) {
throw new Error("Failed to load test keystore");
}
const credentialHash = credential.credentialHash;
const decrypted = await keystore.readCredential(
credentialHash,
TEST_KEYSTORE_PASSWORD
);
if (!decrypted) {
throw new Error("Failed to unlock credential with provided password");
}

const idCommitment = decrypted.identity.IDCommitmentBigInt;

const merkleProof = credential.merkleProof.map((p) => BigInt(p));
const merkleRoot = BigInt(TEST_MERKLE_ROOT);
const membershipIndex = BigInt(credential.membershipIndex);
const rateLimit = BigInt(credential.rateLimit);

const rateCommitment = calculateRateCommitment(idCommitment, rateLimit);

const proofElementIndexes = getPathDirectionsFromIndex(membershipIndex);

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,
new Date(),
decrypted.identity.IDSecretHash,
merkleProof.map((element) =>
BytesUtils.bytes32FromBigInt(element, "little")
),
proofElementIndexes.map((idx) =>
BytesUtils.writeUintLE(new Uint8Array(1), idx, 0, 1)
),
Number(rateLimit),
0
);

const isValid = rlnInstance.zerokit.verifyRLNProof(
BytesUtils.writeUintLE(new Uint8Array(8), testMessage.length, 0, 8),
testMessage,
proof,
[BytesUtils.bytes32FromBigInt(merkleRoot, "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
Copy Markdown
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?

Copy link
Copy Markdown
Member Author

@adklempner adklempner Dec 15, 2025

Choose a reason for hiding this comment

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

I tried adding benchmarks and it logged 0.00 ms for both init functions. However, this is likely because of how karma prepares the bundles for each test. If the corresponding wasm files are not already loaded and need to be fetched, then this would probably take longer.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

how fast it works in the browser? karma won't show accurate numbers

await init();
zerokitRLN.initPanicHook();

Expand Down
Loading
Loading