fix: store merkle proof/root as constant, remove use of RPC in proof gen/verification test

This commit is contained in:
Arseniy Klempner 2025-11-13 14:07:49 -08:00
parent 30ca244c32
commit f7d7673e6f
No known key found for this signature in database
GPG Key ID: 51653F18863BD24B
7 changed files with 527 additions and 1392 deletions

1557
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
import { expect, use } from "chai";
import chaiAsPromised from "chai-as-promised";
import sinon from "sinon";
import { PublicClient } from "viem";
import { RLNBaseContract } from "./rln_base_contract.js";
@ -9,29 +8,17 @@ use(chaiAsPromised);
function createMockRLNBaseContract(
mockContract: any,
<<<<<<< HEAD
mockRpcClient: any
): RLNBaseContract {
const dummy = Object.create(RLNBaseContract.prototype);
dummy.contract = mockContract;
dummy.rpcClient = mockRpcClient;
=======
mockPublicClient: PublicClient
): RLNBaseContract {
const dummy = Object.create(RLNBaseContract.prototype);
dummy.contract = mockContract;
dummy.publicClient = mockPublicClient;
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
return dummy as RLNBaseContract;
}
describe("RLNBaseContract.getPriceForRateLimit (unit)", function () {
let mockContract: any;
<<<<<<< HEAD
let mockRpcClient: any;
=======
let mockPublicClient: any;
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
let priceCalculatorReadStub: sinon.SinonStub;
let readContractStub: sinon.SinonStub;
@ -45,11 +32,7 @@ describe("RLNBaseContract.getPriceForRateLimit (unit)", function () {
}
};
<<<<<<< HEAD
mockRpcClient = {
=======
mockPublicClient = {
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
readContract: readContractStub
};
});
@ -66,11 +49,7 @@ describe("RLNBaseContract.getPriceForRateLimit (unit)", function () {
priceCalculatorReadStub.resolves(priceCalculatorAddress);
readContractStub.resolves([fakeToken, fakePrice]);
<<<<<<< HEAD
const rlnBase = createMockRLNBaseContract(mockContract, mockRpcClient);
=======
const rlnBase = createMockRLNBaseContract(mockContract, mockPublicClient);
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
const result = await rlnBase.getPriceForRateLimit(20);
expect(result.token).to.equal(fakeToken);
@ -92,11 +71,7 @@ describe("RLNBaseContract.getPriceForRateLimit (unit)", function () {
priceCalculatorReadStub.resolves(priceCalculatorAddress);
readContractStub.rejects(new Error("fail"));
<<<<<<< HEAD
const rlnBase = createMockRLNBaseContract(mockContract, mockRpcClient);
=======
const rlnBase = createMockRLNBaseContract(mockContract, mockPublicClient);
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
await expect(rlnBase.getPriceForRateLimit(20)).to.be.rejectedWith("fail");
expect(priceCalculatorReadStub.calledOnce).to.be.true;
@ -109,11 +84,7 @@ describe("RLNBaseContract.getPriceForRateLimit (unit)", function () {
priceCalculatorReadStub.resolves(priceCalculatorAddress);
readContractStub.resolves([null, null]);
<<<<<<< HEAD
const rlnBase = createMockRLNBaseContract(mockContract, mockRpcClient);
=======
const rlnBase = createMockRLNBaseContract(mockContract, mockPublicClient);
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
const result = await rlnBase.getPriceForRateLimit(20);
expect(result.token).to.be.null;

View File

@ -1,187 +0,0 @@
import { expect } from "chai";
import { type Address, createPublicClient, http } from "viem";
import { lineaSepolia } from "viem/chains";
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";
import { RLN_CONTRACT } from "./constants.js";
import { RLNBaseContract } from "./rln_base_contract.js";
describe("RLN Proof Integration Tests", function () {
this.timeout(30000);
let rpcUrl: string;
before(async function () {
this.timeout(10000); // Allow time for WASM initialization
// Initialize WASM module before running tests
await RLNInstance.create();
rpcUrl = process.env.RPC_URL || "https://rpc.sepolia.linea.build";
if (!rpcUrl) {
console.log(
"Skipping integration tests - RPC_URL environment variable not set"
);
console.log(
"To run these tests, set RPC_URL to a Linea Sepolia RPC endpoint"
);
this.skip();
}
});
it("get merkle proof from contract, construct rln proof, verify rln proof", async function () {
// Load the test keystore from constant (browser-compatible)
const keystore = Keystore.fromString(TEST_KEYSTORE_DATA.keystoreJson);
if (!keystore) {
throw new Error("Failed to load test keystore");
}
// Use the known credential hash and password from the test data
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 publicClient = createPublicClient({
chain: lineaSepolia,
transport: http(rpcUrl)
});
const dummyWalletClient = createPublicClient({
chain: lineaSepolia,
transport: http(rpcUrl)
}) as any;
const contract = await RLNBaseContract.create({
address: RLN_CONTRACT.address as Address,
publicClient,
walletClient: dummyWalletClient
});
const membershipInfo = await contract.getMembershipInfo(idCommitment);
if (!membershipInfo) {
throw new Error(
`ID commitment ${idCommitment.toString()} not found in membership set`
);
}
const merkleProof = await contract.getMerkleProof(membershipInfo.index);
expect(merkleProof).to.be.an("array");
expect(merkleProof).to.have.lengthOf(MERKLE_TREE_DEPTH); // RLN uses fixed depth merkle tree
merkleProof.forEach((element, i) => {
console.log(
` [${i}]: ${element.toString()} (0x${element.toString(16)})`
);
});
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 publicClient = createPublicClient({
chain: lineaSepolia,
transport: http(rpcUrl)
});
const dummyWalletClient = createPublicClient({
chain: lineaSepolia,
transport: http(rpcUrl)
}) as any;
const contract = await RLNBaseContract.create({
address: RLN_CONTRACT.address as Address,
publicClient,
walletClient: dummyWalletClient
});
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 membershipInfo = await contract.getMembershipInfo(idCommitment);
if (!membershipInfo) {
throw new Error("Failed to get membership info");
}
const rateLimit = BigInt(membershipInfo.rateLimit);
const merkleProof = await contract.getMerkleProof(membershipInfo.index);
const merkleRoot = await contract.getMerkleRoot();
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 as bigint[],
BigInt(membershipInfo.index),
rateCommitment
);
expect(reconstructedRoot).to.equal(
merkleRoot,
"Reconstructed root should match contract root"
);
const testMessage = new TextEncoder().encode("test");
const rlnInstance = await RLNInstance.create();
const proof = await rlnInstance.zerokit.generateRLNProof(
testMessage,
membershipInfo.index,
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,10 +1,6 @@
<<<<<<< HEAD
import { Address } from "viem";
import { RpcClient } from "../utils/index.js";
=======
import { Address, PublicClient, WalletClient } from "viem";
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
export type Member = {
idCommitment: string;
@ -12,12 +8,7 @@ export type Member = {
};
export interface RLNContractOptions {
<<<<<<< HEAD
rpcClient: RpcClient;
=======
publicClient: PublicClient;
walletClient: WalletClient;
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
address: Address;
rateLimit?: number;
}

View File

@ -4,11 +4,7 @@ import { createRLN } from "./create.js";
import { IdentityCredential } from "./identity.js";
import { Keystore } from "./keystore/index.js";
import { RLNInstance } from "./rln.js";
<<<<<<< HEAD
import { createViemClientFromWindow } from "./utils/index.js";
=======
import { createViemClientsFromWindow } from "./utils/index.js";
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
export {
RLNBaseContract,
@ -17,11 +13,7 @@ export {
RLNInstance,
IdentityCredential,
RLN_CONTRACT,
<<<<<<< HEAD
createViemClientFromWindow
=======
createViemClientsFromWindow as extractMetaMaskSigner
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
};
export {

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

@ -3,5 +3,31 @@ export const TEST_KEYSTORE_DATA = {
'{"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"
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"
};