Skip to content

Commit f546674

Browse files
committed
feat: implement proof generation and verification
1 parent 3c0149b commit f546674

File tree

14 files changed

+590
-359
lines changed

14 files changed

+590
-359
lines changed

package-lock.json

Lines changed: 6 additions & 1 deletion
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';
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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"check:lint": "eslint \"src/!(resources)/**/*.{ts,js}\" *.js",
4040
"check:spelling": "cspell \"{README.md,src/**/*.ts}\"",
4141
"test": "NODE_ENV=test run-s test:*",
42+
"test:unit": "NODE_ENV=test mocha 'src/**/*.spec.ts' --ignore 'src/**/*.integration.spec.ts'",
43+
"test:integration": "NODE_ENV=test mocha 'src/**/*.integration.spec.ts'",
4244
"test:browser": "karma start karma.conf.cjs",
4345
"watch:build": "tsc -p tsconfig.json -w",
4446
"watch:test": "mocha --watch",
@@ -60,7 +62,6 @@
6062
"@types/sinon": "^17.0.3",
6163
"@wagmi/cli": "^2.7.0",
6264
"@waku/build-utils": "^1.0.0",
63-
"@waku/interfaces": "0.0.34",
6465
"@waku/message-encryption": "^0.0.37",
6566
"deep-equal-in-any-order": "^2.0.6",
6667
"fast-check": "^3.23.2",
@@ -83,6 +84,7 @@
8384
"@waku/core": "^0.0.40",
8485
"@waku/utils": "^0.0.27",
8586
"@waku/zerokit-rln-wasm": "^0.2.1",
87+
"@waku/zerokit-rln-wasm-utils": "^0.1.0",
8688
"chai": "^5.1.2",
8789
"chai-as-promised": "^8.0.1",
8890
"chai-spies": "^1.1.0",
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { expect } from "chai";
2+
import { type Address, createPublicClient, http } from "viem";
3+
import { lineaSepolia } from "viem/chains";
4+
5+
import { Keystore } from "../keystore/index.js";
6+
import { RLNInstance } from "../rln.js";
7+
import { BytesUtils } from "../utils/index.js";
8+
import {
9+
calculateRateCommitment,
10+
extractPathDirectionsFromProof,
11+
MERKLE_TREE_DEPTH,
12+
reconstructMerkleRoot
13+
} from "../utils/merkle.js";
14+
import { TEST_KEYSTORE_DATA } from "../utils/test_keystore.js";
15+
16+
import { RLN_CONTRACT } from "./constants.js";
17+
import { RLNBaseContract } from "./rln_base_contract.js";
18+
19+
describe("RLN Proof Integration Tests", function () {
20+
this.timeout(30000);
21+
22+
let rpcUrl: string;
23+
24+
before(async function () {
25+
this.timeout(10000); // Allow time for WASM initialization
26+
27+
// Initialize WASM module before running tests
28+
await RLNInstance.create();
29+
30+
rpcUrl = process.env.RPC_URL || "https://rpc.sepolia.linea.build";
31+
32+
if (!rpcUrl) {
33+
console.log(
34+
"Skipping integration tests - RPC_URL environment variable not set"
35+
);
36+
console.log(
37+
"To run these tests, set RPC_URL to a Linea Sepolia RPC endpoint"
38+
);
39+
this.skip();
40+
}
41+
});
42+
43+
it("get merkle proof from contract, construct rln proof, verify rln proof", async function () {
44+
// Load the test keystore from constant (browser-compatible)
45+
const keystore = Keystore.fromString(TEST_KEYSTORE_DATA.keystoreJson);
46+
if (!keystore) {
47+
throw new Error("Failed to load test keystore");
48+
}
49+
50+
// Use the known credential hash and password from the test data
51+
const credentialHash = TEST_KEYSTORE_DATA.credentialHash;
52+
const password = TEST_KEYSTORE_DATA.password;
53+
console.log(`Using credential hash: ${credentialHash}`);
54+
const credential = await keystore.readCredential(credentialHash, password);
55+
if (!credential) {
56+
throw new Error("Failed to unlock credential with provided password");
57+
}
58+
59+
// Extract the ID commitment from the credential
60+
const idCommitment = credential.identity.IDCommitmentBigInt;
61+
console.log(`ID Commitment from keystore: ${idCommitment.toString()}`);
62+
63+
const publicClient = createPublicClient({
64+
chain: lineaSepolia,
65+
transport: http(rpcUrl)
66+
});
67+
68+
const dummyWalletClient = createPublicClient({
69+
chain: lineaSepolia,
70+
transport: http(rpcUrl)
71+
}) as any;
72+
73+
const contract = await RLNBaseContract.create({
74+
address: RLN_CONTRACT.address as Address,
75+
publicClient,
76+
walletClient: dummyWalletClient
77+
});
78+
79+
// First, get membership info to find the index
80+
const membershipInfo = await contract.getMembershipInfo(idCommitment);
81+
82+
if (!membershipInfo) {
83+
console.log(
84+
`ID commitment ${idCommitment.toString()} not found in membership set`
85+
);
86+
this.skip();
87+
return;
88+
}
89+
90+
console.log(`Found membership at index: ${membershipInfo.index}`);
91+
console.log(`Membership state: ${membershipInfo.state}`);
92+
93+
// Get the merkle proof for this member's index
94+
const merkleProof = await contract.getMerkleProof(membershipInfo.index);
95+
96+
expect(merkleProof).to.be.an("array");
97+
expect(merkleProof).to.have.lengthOf(MERKLE_TREE_DEPTH); // RLN uses fixed depth merkle tree
98+
99+
console.log(`Merkle proof for ID commitment ${idCommitment.toString()}:`);
100+
console.log(`Index: ${membershipInfo.index}`);
101+
console.log(`Proof elements (${merkleProof.length}):`);
102+
merkleProof.forEach((element, i) => {
103+
console.log(
104+
` [${i}]: ${element.toString()} (0x${element.toString(16)})`
105+
);
106+
});
107+
108+
// Verify all proof elements are valid bigints
109+
merkleProof.forEach((element, i) => {
110+
expect(element).to.be.a(
111+
"bigint",
112+
`Proof element ${i} should be a bigint`
113+
);
114+
expect(element).to.not.equal(0n, `Proof element ${i} should not be zero`);
115+
});
116+
});
117+
118+
it.only("should generate a valid RLN proof", async function () {
119+
const publicClient = createPublicClient({
120+
chain: lineaSepolia,
121+
transport: http(rpcUrl)
122+
});
123+
124+
const dummyWalletClient = createPublicClient({
125+
chain: lineaSepolia,
126+
transport: http(rpcUrl)
127+
}) as any;
128+
129+
const contract = await RLNBaseContract.create({
130+
address: RLN_CONTRACT.address as Address,
131+
publicClient,
132+
walletClient: dummyWalletClient
133+
});
134+
// get credential from keystore
135+
const keystore = Keystore.fromString(TEST_KEYSTORE_DATA.keystoreJson);
136+
if (!keystore) {
137+
throw new Error("Failed to load test keystore");
138+
}
139+
const credentialHash = TEST_KEYSTORE_DATA.credentialHash;
140+
const password = TEST_KEYSTORE_DATA.password;
141+
const credential = await keystore.readCredential(credentialHash, password);
142+
if (!credential) {
143+
throw new Error("Failed to unlock credential with provided password");
144+
}
145+
const idCommitment = credential.identity.IDCommitmentBigInt;
146+
const membershipInfo = await contract.getMembershipInfo(idCommitment);
147+
if (!membershipInfo) {
148+
throw new Error("Failed to get membership info");
149+
}
150+
const rateLimit = BigInt(membershipInfo.rateLimit);
151+
152+
const merkleProof = await contract.getMerkleProof(membershipInfo.index);
153+
const merkleRoot = await contract.getMerkleRoot();
154+
const rateCommitment = calculateRateCommitment(idCommitment, rateLimit);
155+
156+
// Get the array of indexes that correspond to each proof element
157+
const proofElementIndexes = extractPathDirectionsFromProof(
158+
merkleProof,
159+
rateCommitment,
160+
merkleRoot
161+
);
162+
if (!proofElementIndexes) {
163+
throw new Error("Failed to extract proof element indexes");
164+
}
165+
166+
// Verify the array has the correct length
167+
expect(proofElementIndexes).to.have.lengthOf(MERKLE_TREE_DEPTH);
168+
169+
// Verify that we can reconstruct the root using these indexes
170+
const reconstructedRoot = reconstructMerkleRoot(
171+
merkleProof as bigint[],
172+
BigInt(membershipInfo.index),
173+
rateCommitment
174+
);
175+
176+
expect(reconstructedRoot).to.equal(
177+
merkleRoot,
178+
"Reconstructed root should match contract root"
179+
);
180+
181+
const testMessage = new TextEncoder().encode("test");
182+
const rlnInstance = await RLNInstance.create();
183+
184+
const proof = await rlnInstance.zerokit.generateRLNProof(
185+
testMessage,
186+
membershipInfo.index,
187+
new Date(),
188+
credential.identity.IDSecretHash,
189+
merkleProof.map((proof) => BytesUtils.fromBigInt(proof, 32, "little")),
190+
proofElementIndexes.map((index) =>
191+
BytesUtils.writeUIntLE(new Uint8Array(1), index, 0, 1)
192+
),
193+
Number(rateLimit),
194+
0
195+
);
196+
197+
const isValid = rlnInstance.zerokit.verifyRLNProof(
198+
BytesUtils.writeUIntLE(new Uint8Array(8), testMessage.length, 0, 8),
199+
testMessage,
200+
proof,
201+
[BytesUtils.fromBigInt(merkleRoot, 32, "little")]
202+
);
203+
expect(isValid).to.be.true;
204+
});
205+
});

0 commit comments

Comments
 (0)