Skip to content

Commit c6b6d03

Browse files
committed
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
1 parent f2ad23a commit c6b6d03

File tree

16 files changed

+573
-70
lines changed

16 files changed

+573
-70
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "packages/rln/waku-rlnv2-contract"]
2+
path = packages/rln/waku-rlnv2-contract
3+
url = [email protected]:waku-org/waku-rlnv2-contract.git

package-lock.json

Lines changed: 33 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/rln/.mocha.reporters.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"reporterEnabled": "spec, allure-mocha",
3+
"allureMochaReporter": {
4+
"outputDir": "allure-results"
5+
}
6+
}

packages/rln/.mocharc.cjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ if (process.env.CI) {
2020
config.reporterOptions = {
2121
configFile: '.mocha.reporters.json'
2222
};
23+
// Exclude integration tests in CI (they require RPC access)
24+
console.log("Excluding integration tests in CI environment");
25+
config.ignore = ['src/**/*.integration.spec.ts', 'src/**/*.browser.spec.ts'];
2326
} else {
2427
console.log("Running tests serially. To enable parallel execution update mocha config");
2528
}
2629

27-
module.exports = config;
30+
module.exports = config;

packages/rln/karma.conf.cjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,19 @@ module.exports = function (config) {
3838
watched: false,
3939
type: "wasm",
4040
nocache: true
41+
},
42+
{
43+
pattern: "../../node_modules/@waku/zerokit-rln-wasm-utils/*.wasm",
44+
included: false,
45+
served: true,
46+
watched: false,
47+
type: "wasm",
48+
nocache: true
4149
}
4250
],
4351

52+
exclude: process.env.CI ? ["src/**/*.integration.spec.ts"] : [],
53+
4454
preprocessors: {
4555
"src/**/*.spec.ts": ["webpack"]
4656
},
@@ -82,6 +92,12 @@ module.exports = function (config) {
8292
__dirname,
8393
"../../node_modules/@waku/zerokit-rln-wasm/rln_wasm_bg.wasm"
8494
),
95+
"/base/rln_wasm_utils_bg.wasm":
96+
"/absolute" +
97+
path.resolve(
98+
__dirname,
99+
"../../node_modules/@waku/zerokit-rln-wasm-utils/rln_wasm_utils_bg.wasm"
100+
),
85101
"/base/rln.wasm":
86102
"/absolute" + path.resolve(__dirname, "src/resources/rln.wasm"),
87103
"/base/rln_final.zkey":

packages/rln/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
"@types/sinon": "^17.0.3",
6161
"@wagmi/cli": "^2.7.0",
6262
"@waku/build-utils": "^1.0.0",
63-
"@waku/interfaces": "0.0.34",
6463
"@waku/message-encryption": "^0.0.37",
6564
"deep-equal-in-any-order": "^2.0.6",
6665
"fast-check": "^3.23.2",
@@ -83,6 +82,7 @@
8382
"@waku/core": "^0.0.40",
8483
"@waku/utils": "^0.0.27",
8584
"@waku/zerokit-rln-wasm": "^0.2.1",
85+
"@waku/zerokit-rln-wasm-utils": "^0.1.0",
8686
"chai": "^5.1.2",
8787
"chai-as-promised": "^8.0.1",
8888
"chai-spies": "^1.1.0",

packages/rln/src/keystore/keystore.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,15 @@ export class Keystore {
160160
}
161161

162162
public toString(): string {
163-
return JSON.stringify(this.data);
163+
// Custom replacer function to handle BigInt serialization
164+
const bigIntReplacer = (_key: string, value: unknown): unknown => {
165+
if (typeof value === "bigint") {
166+
return value.toString();
167+
}
168+
return value;
169+
};
170+
171+
return JSON.stringify(this.data, bigIntReplacer);
164172
}
165173

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

338+
// Custom replacer function to handle BigInt serialization
339+
const bigIntReplacer = (_key: string, value: unknown): unknown => {
340+
if (typeof value === "bigint") {
341+
return value.toString();
342+
}
343+
return value;
344+
};
345+
330346
return utf8ToBytes(
331-
JSON.stringify({
332-
treeIndex: options.membership.treeIndex,
333-
identityCredential: {
334-
idCommitment: Array.from(IDCommitment),
335-
idNullifier: Array.from(IDNullifier),
336-
idSecretHash: Array.from(IDSecretHash),
337-
idTrapdoor: Array.from(IDTrapdoor)
338-
},
339-
membershipContract: {
340-
chainId: options.membership.chainId,
341-
address: options.membership.address
347+
JSON.stringify(
348+
{
349+
treeIndex: options.membership.treeIndex,
350+
identityCredential: {
351+
idCommitment: Array.from(IDCommitment),
352+
idNullifier: Array.from(IDNullifier),
353+
idSecretHash: Array.from(IDSecretHash),
354+
idTrapdoor: Array.from(IDTrapdoor)
355+
},
356+
membershipContract: {
357+
chainId: options.membership.chainId,
358+
address: options.membership.address
359+
},
360+
userMessageLimit: options.membership.rateLimit
342361
},
343-
userMessageLimit: options.membership.rateLimit
344-
})
362+
bigIntReplacer
363+
)
345364
);
346365
}
347366
}

packages/rln/src/proof.spec.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { expect } from "chai";
2+
3+
import { Keystore } from "./keystore/index.js";
4+
import { RLNInstance } from "./rln.js";
5+
import { BytesUtils } from "./utils/index.js";
6+
import {
7+
calculateRateCommitment,
8+
extractPathDirectionsFromProof,
9+
MERKLE_TREE_DEPTH,
10+
reconstructMerkleRoot
11+
} from "./utils/merkle.js";
12+
import { TEST_KEYSTORE_DATA } from "./utils/test_keystore.js";
13+
14+
describe("RLN Proof Integration Tests", function () {
15+
this.timeout(30000);
16+
17+
it("validate stored merkle proof data", function () {
18+
// Convert stored merkle proof strings to bigints
19+
const merkleProof = TEST_KEYSTORE_DATA.merkleProof.map((p) => BigInt(p));
20+
21+
expect(merkleProof).to.be.an("array");
22+
expect(merkleProof).to.have.lengthOf(MERKLE_TREE_DEPTH); // RLN uses fixed depth merkle tree
23+
24+
merkleProof.forEach((element, i) => {
25+
expect(element).to.be.a(
26+
"bigint",
27+
`Proof element ${i} should be a bigint`
28+
);
29+
expect(element).to.not.equal(0n, `Proof element ${i} should not be zero`);
30+
});
31+
});
32+
33+
it("should generate a valid RLN proof", async function () {
34+
const rlnInstance = await RLNInstance.create();
35+
// Load credential from test keystore
36+
const keystore = Keystore.fromString(TEST_KEYSTORE_DATA.keystoreJson);
37+
if (!keystore) {
38+
throw new Error("Failed to load test keystore");
39+
}
40+
const credentialHash = TEST_KEYSTORE_DATA.credentialHash;
41+
const password = TEST_KEYSTORE_DATA.password;
42+
const credential = await keystore.readCredential(credentialHash, password);
43+
if (!credential) {
44+
throw new Error("Failed to unlock credential with provided password");
45+
}
46+
47+
const idCommitment = credential.identity.IDCommitmentBigInt;
48+
49+
const merkleProof = TEST_KEYSTORE_DATA.merkleProof.map((p) => BigInt(p));
50+
const merkleRoot = BigInt(TEST_KEYSTORE_DATA.merkleRoot);
51+
const membershipIndex = BigInt(TEST_KEYSTORE_DATA.membershipIndex);
52+
const rateLimit = BigInt(TEST_KEYSTORE_DATA.rateLimit);
53+
54+
const rateCommitment = calculateRateCommitment(idCommitment, rateLimit);
55+
56+
const proofElementIndexes = extractPathDirectionsFromProof(
57+
merkleProof,
58+
rateCommitment,
59+
merkleRoot
60+
);
61+
if (!proofElementIndexes) {
62+
throw new Error("Failed to extract proof element indexes");
63+
}
64+
65+
expect(proofElementIndexes).to.have.lengthOf(MERKLE_TREE_DEPTH);
66+
67+
const reconstructedRoot = reconstructMerkleRoot(
68+
merkleProof,
69+
membershipIndex,
70+
rateCommitment
71+
);
72+
73+
expect(reconstructedRoot).to.equal(
74+
merkleRoot,
75+
"Reconstructed root should match stored root"
76+
);
77+
78+
const testMessage = new TextEncoder().encode("test");
79+
80+
const proof = await rlnInstance.zerokit.generateRLNProof(
81+
testMessage,
82+
Number(membershipIndex),
83+
new Date(),
84+
credential.identity.IDSecretHash,
85+
merkleProof.map((proof) => BytesUtils.fromBigInt(proof, 32, "little")),
86+
proofElementIndexes.map((index) =>
87+
BytesUtils.writeUIntLE(new Uint8Array(1), index, 0, 1)
88+
),
89+
Number(rateLimit),
90+
0
91+
);
92+
93+
const isValid = rlnInstance.zerokit.verifyRLNProof(
94+
BytesUtils.writeUIntLE(new Uint8Array(8), testMessage.length, 0, 8),
95+
testMessage,
96+
proof,
97+
[BytesUtils.fromBigInt(merkleRoot, 32, "little")]
98+
);
99+
expect(isValid).to.be.true;
100+
});
101+
});

packages/rln/src/rln.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Logger } from "@waku/utils";
22
import init, * as zerokitRLN from "@waku/zerokit-rln-wasm";
3+
import initUtils from "@waku/zerokit-rln-wasm-utils";
34

45
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
56
import { RLNCredentialsManager } from "./credentials_manager.js";
@@ -16,6 +17,7 @@ export class RLNInstance extends RLNCredentialsManager {
1617
*/
1718
public static async create(): Promise<RLNInstance> {
1819
try {
20+
await initUtils();
1921
await init();
2022
zerokitRLN.initPanicHook();
2123

0 commit comments

Comments
 (0)