mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-05 23:33:08 +00:00
feat: implement proof generation and verification
This commit is contained in:
parent
9b5b47f55c
commit
feb8b87d7a
7
package-lock.json
generated
7
package-lock.json
generated
@ -7154,6 +7154,11 @@
|
||||
"integrity": "sha512-2Xp7e92y4qZpsiTPGBSVr4gVJ9mJTLaudlo0DQxNpxJUBtoJKpxdH5xDCQDiorbkWZC2j9EId+ohhxHO/xC1QQ==",
|
||||
"license": "MIT or Apache2"
|
||||
},
|
||||
"node_modules/@waku/zerokit-rln-wasm-utils": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm-utils/-/zerokit-rln-wasm-utils-0.1.0.tgz",
|
||||
"integrity": "sha512-3ccyg9+CtRXFJfWaxI/kx8Aec5B2S9YUmZAVhPRdN1EG6iQYG2hgvAurx8ZF9/zOppdrhzzyvCgDPg5kRUlOfQ=="
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
||||
@ -35489,6 +35494,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",
|
||||
@ -35510,7 +35516,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",
|
||||
"deep-equal-in-any-order": "^2.0.6",
|
||||
"fast-check": "^3.23.2",
|
||||
|
||||
6
packages/rln/.mocha.reporters.json
Normal file
6
packages/rln/.mocha.reporters.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"reporterEnabled": "spec, allure-mocha",
|
||||
"allureMochaReporter": {
|
||||
"outputDir": "allure-results"
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,9 @@ if (process.env.CI) {
|
||||
config.reporterOptions = {
|
||||
configFile: '.mocha.reporters.json'
|
||||
};
|
||||
// Exclude integration tests in CI (they require RPC access)
|
||||
console.log("Excluding integration tests in CI environment");
|
||||
config.ignore = 'src/**/*.integration.spec.ts';
|
||||
} else {
|
||||
console.log("Running tests serially. To enable parallel execution update mocha config");
|
||||
}
|
||||
|
||||
@ -38,9 +38,19 @@ 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
|
||||
}
|
||||
],
|
||||
|
||||
exclude: process.env.CI ? ["src/**/*.integration.spec.ts"] : [],
|
||||
|
||||
preprocessors: {
|
||||
"src/**/*.spec.ts": ["webpack"]
|
||||
},
|
||||
@ -82,6 +92,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":
|
||||
|
||||
@ -39,6 +39,8 @@
|
||||
"check:lint": "eslint \"src/!(resources)/**/*.{ts,js}\" *.js",
|
||||
"check:spelling": "cspell \"{README.md,src/**/*.ts}\"",
|
||||
"test": "NODE_ENV=test run-s test:*",
|
||||
"test:unit": "NODE_ENV=test mocha 'src/**/*.spec.ts' --ignore 'src/**/*.integration.spec.ts'",
|
||||
"test:integration": "NODE_ENV=test mocha 'src/**/*.integration.spec.ts'",
|
||||
"test:browser": "karma start karma.conf.cjs",
|
||||
"watch:build": "tsc -p tsconfig.json -w",
|
||||
"watch:test": "mocha --watch",
|
||||
@ -60,7 +62,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",
|
||||
"deep-equal-in-any-order": "^2.0.6",
|
||||
"fast-check": "^3.23.2",
|
||||
@ -83,6 +84,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",
|
||||
|
||||
205
packages/rln/src/contract/proof.integration.spec.ts
Normal file
205
packages/rln/src/contract/proof.integration.spec.ts
Normal file
@ -0,0 +1,205 @@
|
||||
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;
|
||||
console.log(`Using credential hash: ${credentialHash}`);
|
||||
const credential = await keystore.readCredential(credentialHash, password);
|
||||
if (!credential) {
|
||||
throw new Error("Failed to unlock credential with provided password");
|
||||
}
|
||||
|
||||
// Extract the ID commitment from the credential
|
||||
const idCommitment = credential.identity.IDCommitmentBigInt;
|
||||
console.log(`ID Commitment from keystore: ${idCommitment.toString()}`);
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
// First, get membership info to find the index
|
||||
const membershipInfo = await contract.getMembershipInfo(idCommitment);
|
||||
|
||||
if (!membershipInfo) {
|
||||
console.log(
|
||||
`ID commitment ${idCommitment.toString()} not found in membership set`
|
||||
);
|
||||
this.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found membership at index: ${membershipInfo.index}`);
|
||||
console.log(`Membership state: ${membershipInfo.state}`);
|
||||
|
||||
// Get the merkle proof for this member's index
|
||||
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
|
||||
|
||||
console.log(`Merkle proof for ID commitment ${idCommitment.toString()}:`);
|
||||
console.log(`Index: ${membershipInfo.index}`);
|
||||
console.log(`Proof elements (${merkleProof.length}):`);
|
||||
merkleProof.forEach((element, i) => {
|
||||
console.log(
|
||||
` [${i}]: ${element.toString()} (0x${element.toString(16)})`
|
||||
);
|
||||
});
|
||||
|
||||
// Verify all proof elements are valid bigints
|
||||
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.only("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
|
||||
});
|
||||
// get credential from 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 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);
|
||||
|
||||
// Get the array of indexes that correspond to each proof element
|
||||
const proofElementIndexes = extractPathDirectionsFromProof(
|
||||
merkleProof,
|
||||
rateCommitment,
|
||||
merkleRoot
|
||||
);
|
||||
if (!proofElementIndexes) {
|
||||
throw new Error("Failed to extract proof element indexes");
|
||||
}
|
||||
|
||||
// Verify the array has the correct length
|
||||
expect(proofElementIndexes).to.have.lengthOf(MERKLE_TREE_DEPTH);
|
||||
|
||||
// Verify that we can reconstruct the root using these indexes
|
||||
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;
|
||||
});
|
||||
});
|
||||
@ -3,18 +3,10 @@ import {
|
||||
type Address,
|
||||
decodeEventLog,
|
||||
getContract,
|
||||
<<<<<<< HEAD
|
||||
type GetContractReturnType,
|
||||
type Hash,
|
||||
type PublicClient,
|
||||
type WalletClient
|
||||
=======
|
||||
GetContractEventsReturnType,
|
||||
GetContractReturnType,
|
||||
type Hash,
|
||||
PublicClient,
|
||||
WalletClient
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
} from "viem";
|
||||
|
||||
import { IdentityCredential } from "../identity.js";
|
||||
@ -27,11 +19,6 @@ import {
|
||||
RLN_CONTRACT
|
||||
} from "./constants.js";
|
||||
import {
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
FetchMembersOptions,
|
||||
Member,
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
MembershipInfo,
|
||||
MembershipState,
|
||||
RLNContractOptions
|
||||
@ -40,36 +27,20 @@ import { iPriceCalculatorAbi, wakuRlnV2Abi } from "./wagmi/generated.js";
|
||||
|
||||
const log = new Logger("rln:contract:base");
|
||||
|
||||
type MembershipEvents = GetContractEventsReturnType<
|
||||
typeof wakuRlnV2Abi,
|
||||
"MembershipRegistered" | "MembershipErased" | "MembershipExpired"
|
||||
>;
|
||||
export class RLNBaseContract {
|
||||
public contract: GetContractReturnType<
|
||||
typeof wakuRlnV2Abi,
|
||||
PublicClient | WalletClient
|
||||
>;
|
||||
<<<<<<< HEAD
|
||||
public rpcClient: RpcClient;
|
||||
=======
|
||||
public publicClient: PublicClient;
|
||||
public walletClient: WalletClient;
|
||||
private deployBlock: undefined | number;
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
private rateLimit: number;
|
||||
private minRateLimit?: number;
|
||||
private maxRateLimit?: number;
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
protected _members: Map<number, Member> = new Map();
|
||||
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
/**
|
||||
* Private constructor for RLNBaseContract. Use static create() instead.
|
||||
*/
|
||||
protected constructor(options: RLNContractOptions) {
|
||||
<<<<<<< HEAD
|
||||
const { address, rpcClient, rateLimit = DEFAULT_RATE_LIMIT } = options;
|
||||
|
||||
log.info("Initializing RLNBaseContract", { address, rateLimit });
|
||||
@ -81,34 +52,6 @@ export class RLNBaseContract {
|
||||
client: this.rpcClient
|
||||
});
|
||||
this.rateLimit = rateLimit;
|
||||
=======
|
||||
const {
|
||||
address,
|
||||
publicClient,
|
||||
walletClient,
|
||||
rateLimit = DEFAULT_RATE_LIMIT
|
||||
} = options;
|
||||
|
||||
log.info("Initializing RLNBaseContract", { address, rateLimit });
|
||||
|
||||
this.publicClient = publicClient;
|
||||
this.walletClient = walletClient;
|
||||
this.contract = getContract({
|
||||
address,
|
||||
abi: wakuRlnV2Abi,
|
||||
client: { wallet: walletClient, public: publicClient }
|
||||
});
|
||||
this.rateLimit = rateLimit;
|
||||
|
||||
// Initialize members and subscriptions
|
||||
this.fetchMembers()
|
||||
.then(() => {
|
||||
this.subscribeToMembers();
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("Failed to initialize members", { error });
|
||||
});
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,11 +62,6 @@ export class RLNBaseContract {
|
||||
): Promise<RLNBaseContract> {
|
||||
const instance = new RLNBaseContract(options);
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
instance.deployBlock = await instance.contract.read.deployedBlockNumber();
|
||||
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
const [min, max] = await Promise.all([
|
||||
instance.contract.read.minMembershipRateLimit(),
|
||||
instance.contract.read.maxMembershipRateLimit()
|
||||
@ -211,7 +149,6 @@ export class RLNBaseContract {
|
||||
*/
|
||||
public async getMerkleRoot(): Promise<bigint> {
|
||||
return this.contract.read.root();
|
||||
<<<<<<< HEAD
|
||||
}
|
||||
|
||||
/**
|
||||
@ -222,156 +159,6 @@ export class RLNBaseContract {
|
||||
*/
|
||||
public async getMerkleProof(index: number): Promise<readonly bigint[]> {
|
||||
return await this.contract.read.getMerkleProof([index]);
|
||||
=======
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Merkle proof for a member at a given index
|
||||
* @param index The index of the member in the membership set
|
||||
* @returns Promise<bigint[]> Array of 20 Merkle proof elements
|
||||
*
|
||||
*/
|
||||
public async getMerkleProof(index: number): Promise<readonly bigint[]> {
|
||||
return await this.contract.read.getMerkleProof([index]);
|
||||
}
|
||||
|
||||
public get members(): Member[] {
|
||||
const sortedMembers = Array.from(this._members.values()).sort(
|
||||
(left, right) => Number(left.index) - Number(right.index)
|
||||
);
|
||||
return sortedMembers;
|
||||
}
|
||||
|
||||
public async fetchMembers(options: FetchMembersOptions = {}): Promise<void> {
|
||||
const fromBlock = options.fromBlock
|
||||
? BigInt(options.fromBlock!)
|
||||
: BigInt(this.deployBlock!);
|
||||
const registeredMemberEvents =
|
||||
await this.contract.getEvents.MembershipRegistered({
|
||||
fromBlock,
|
||||
toBlock: fromBlock + BigInt(options.fetchRange!)
|
||||
});
|
||||
const removedMemberEvents = await this.contract.getEvents.MembershipErased({
|
||||
fromBlock,
|
||||
toBlock: fromBlock + BigInt(options.fetchRange!)
|
||||
});
|
||||
const expiredMemberEvents = await this.contract.getEvents.MembershipExpired(
|
||||
{
|
||||
fromBlock,
|
||||
toBlock: fromBlock + BigInt(options.fetchRange!)
|
||||
}
|
||||
);
|
||||
|
||||
const events = [
|
||||
...registeredMemberEvents,
|
||||
...removedMemberEvents,
|
||||
...expiredMemberEvents
|
||||
];
|
||||
this.processEvents(events);
|
||||
}
|
||||
|
||||
public processEvents(events: MembershipEvents): void {
|
||||
const toRemoveTable = new Map<number, number[]>();
|
||||
const toInsertTable = new Map<number, MembershipEvents>();
|
||||
|
||||
events.forEach((evt) => {
|
||||
if (!evt.args) {
|
||||
return;
|
||||
}
|
||||
const blockNumber = Number(evt.blockNumber);
|
||||
if (
|
||||
evt.eventName === "MembershipErased" ||
|
||||
evt.eventName === "MembershipExpired"
|
||||
) {
|
||||
const index = evt.args.index;
|
||||
|
||||
if (!index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toRemoveVal = toRemoveTable.get(blockNumber);
|
||||
if (toRemoveVal != undefined) {
|
||||
toRemoveVal.push(index);
|
||||
toRemoveTable.set(blockNumber, toRemoveVal);
|
||||
} else {
|
||||
toRemoveTable.set(blockNumber, [index]);
|
||||
}
|
||||
} else if (evt.eventName === "MembershipRegistered") {
|
||||
let eventsPerBlock = toInsertTable.get(blockNumber);
|
||||
if (eventsPerBlock == undefined) {
|
||||
eventsPerBlock = [];
|
||||
}
|
||||
|
||||
eventsPerBlock.push(evt);
|
||||
toInsertTable.set(blockNumber, eventsPerBlock);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static splitToChunks(
|
||||
from: number,
|
||||
to: number,
|
||||
step: number
|
||||
): Array<[number, number]> {
|
||||
const chunks: Array<[number, number]> = [];
|
||||
|
||||
let left = from;
|
||||
while (left < to) {
|
||||
const right = left + step < to ? left + step : to;
|
||||
|
||||
chunks.push([left, right] as [number, number]);
|
||||
|
||||
left = right;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
public static *takeN<T>(array: T[], size: number): Iterable<T[]> {
|
||||
let start = 0;
|
||||
|
||||
while (start < array.length) {
|
||||
const portion = array.slice(start, start + size);
|
||||
|
||||
yield portion;
|
||||
|
||||
start += size;
|
||||
}
|
||||
}
|
||||
|
||||
public static async ignoreErrors<T>(
|
||||
promise: Promise<T>,
|
||||
defaultValue: T
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await promise;
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
log.info(`Ignoring an error during query: ${err.message}`);
|
||||
} else {
|
||||
log.info(`Ignoring an unknown error during query`);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public subscribeToMembers(): void {
|
||||
this.contract.watchEvent.MembershipRegistered({
|
||||
onLogs: (logs) => {
|
||||
this.processEvents(logs);
|
||||
}
|
||||
});
|
||||
this.contract.watchEvent.MembershipExpired({
|
||||
onLogs: (logs) => {
|
||||
this.processEvents(logs);
|
||||
}
|
||||
});
|
||||
this.contract.watchEvent.MembershipErased({
|
||||
onLogs: (logs) => {
|
||||
this.processEvents(logs);
|
||||
}
|
||||
});
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
}
|
||||
|
||||
public async getMembershipInfo(
|
||||
@ -382,11 +169,7 @@ export class RLNBaseContract {
|
||||
idCommitmentBigInt
|
||||
]);
|
||||
|
||||
<<<<<<< HEAD
|
||||
const currentBlock = await this.rpcClient.getBlockNumber();
|
||||
=======
|
||||
const currentBlock = await this.publicClient.getBlockNumber();
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
|
||||
const [
|
||||
depositAmount,
|
||||
@ -431,24 +214,15 @@ export class RLNBaseContract {
|
||||
}
|
||||
|
||||
public async extendMembership(idCommitmentBigInt: bigint): Promise<Hash> {
|
||||
<<<<<<< HEAD
|
||||
if (!this.rpcClient.account) {
|
||||
=======
|
||||
if (!this.walletClient.account) {
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
throw new Error(
|
||||
"Failed to extendMembership: no account set in wallet client"
|
||||
);
|
||||
}
|
||||
try {
|
||||
await this.contract.simulate.extendMemberships([[idCommitmentBigInt]], {
|
||||
<<<<<<< HEAD
|
||||
chain: this.rpcClient.chain,
|
||||
account: this.rpcClient.account.address
|
||||
=======
|
||||
chain: this.walletClient.chain,
|
||||
account: this.walletClient.account!.address
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error("Simulating extending membership failed: " + err);
|
||||
@ -456,21 +230,12 @@ export class RLNBaseContract {
|
||||
const hash = await this.contract.write.extendMemberships(
|
||||
[[idCommitmentBigInt]],
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
account: this.rpcClient.account,
|
||||
chain: this.rpcClient.chain
|
||||
}
|
||||
);
|
||||
|
||||
await this.rpcClient.waitForTransactionReceipt({ hash });
|
||||
=======
|
||||
account: this.walletClient.account!,
|
||||
chain: this.walletClient.chain
|
||||
}
|
||||
);
|
||||
|
||||
await this.publicClient.waitForTransactionReceipt({ hash });
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
return hash;
|
||||
}
|
||||
|
||||
@ -484,11 +249,7 @@ export class RLNBaseContract {
|
||||
) {
|
||||
throw new Error("Membership is not expired or in grace period");
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
if (!this.rpcClient.account) {
|
||||
=======
|
||||
if (!this.walletClient.account) {
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
throw new Error(
|
||||
"Failed to eraseMembership: no account set in wallet client"
|
||||
);
|
||||
@ -498,13 +259,8 @@ export class RLNBaseContract {
|
||||
await this.contract.simulate.eraseMemberships(
|
||||
[[idCommitmentBigInt], eraseFromMembershipSet],
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
chain: this.rpcClient.chain,
|
||||
account: this.rpcClient.account.address
|
||||
=======
|
||||
chain: this.walletClient.chain,
|
||||
account: this.walletClient.account!.address
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
@ -514,19 +270,11 @@ export class RLNBaseContract {
|
||||
const hash = await this.contract.write.eraseMemberships(
|
||||
[[idCommitmentBigInt], eraseFromMembershipSet],
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
chain: this.rpcClient.chain,
|
||||
account: this.rpcClient.account
|
||||
}
|
||||
);
|
||||
await this.rpcClient.waitForTransactionReceipt({ hash });
|
||||
=======
|
||||
chain: this.walletClient.chain,
|
||||
account: this.walletClient.account!
|
||||
}
|
||||
);
|
||||
await this.publicClient.waitForTransactionReceipt({ hash });
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
return hash;
|
||||
}
|
||||
|
||||
@ -542,11 +290,7 @@ export class RLNBaseContract {
|
||||
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}`
|
||||
);
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
if (!this.rpcClient.account) {
|
||||
=======
|
||||
if (!this.walletClient.account) {
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
throw new Error(
|
||||
"Failed to registerMembership: no account set in wallet client"
|
||||
);
|
||||
@ -555,13 +299,8 @@ export class RLNBaseContract {
|
||||
await this.contract.simulate.register(
|
||||
[idCommitmentBigInt, rateLimit, []],
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
chain: this.rpcClient.chain,
|
||||
account: this.rpcClient.account.address
|
||||
=======
|
||||
chain: this.walletClient.chain,
|
||||
account: this.walletClient.account!.address
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
@ -571,24 +310,15 @@ export class RLNBaseContract {
|
||||
const hash = await this.contract.write.register(
|
||||
[idCommitmentBigInt, rateLimit, []],
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
chain: this.rpcClient.chain,
|
||||
account: this.rpcClient.account
|
||||
}
|
||||
);
|
||||
await this.rpcClient.waitForTransactionReceipt({ hash });
|
||||
=======
|
||||
chain: this.walletClient.chain,
|
||||
account: this.walletClient.account!
|
||||
}
|
||||
);
|
||||
await this.publicClient.waitForTransactionReceipt({ hash });
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
<<<<<<< HEAD
|
||||
* Withdraw deposited tokens after membership is erased.
|
||||
* The smart contract validates that the sender is the holder of the membership,
|
||||
* and will only send tokens to that address.
|
||||
@ -596,45 +326,24 @@ export class RLNBaseContract {
|
||||
*/
|
||||
public async withdraw(token: string): Promise<Hash> {
|
||||
if (!this.rpcClient.account) {
|
||||
=======
|
||||
* Withdraw deposited tokens after membership is erased
|
||||
* @param token - Token address to withdraw
|
||||
* NOTE: Funds are sent to msg.sender (the walletClient's address)
|
||||
*/
|
||||
public async withdraw(token: string): Promise<Hash> {
|
||||
if (!this.walletClient.account) {
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
throw new Error("Failed to withdraw: no account set in wallet client");
|
||||
}
|
||||
|
||||
try {
|
||||
await this.contract.simulate.withdraw([token as Address], {
|
||||
<<<<<<< HEAD
|
||||
chain: this.rpcClient.chain,
|
||||
account: this.rpcClient.account.address
|
||||
=======
|
||||
chain: this.walletClient.chain,
|
||||
account: this.walletClient.account!.address
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error("Error simulating withdraw: " + err);
|
||||
}
|
||||
|
||||
const hash = await this.contract.write.withdraw([token as Address], {
|
||||
<<<<<<< HEAD
|
||||
chain: this.rpcClient.chain,
|
||||
account: this.rpcClient.account
|
||||
});
|
||||
|
||||
await this.rpcClient.waitForTransactionReceipt({ hash });
|
||||
=======
|
||||
chain: this.walletClient.chain,
|
||||
account: this.walletClient.account!
|
||||
});
|
||||
|
||||
await this.publicClient.waitForTransactionReceipt({ hash });
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
return hash;
|
||||
}
|
||||
public async registerWithIdentity(
|
||||
@ -672,20 +381,14 @@ export class RLNBaseContract {
|
||||
await this.contract.simulate.register(
|
||||
[identity.IDCommitmentBigInt, this.rateLimit, []],
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
chain: this.rpcClient.chain,
|
||||
account: this.rpcClient.account.address
|
||||
=======
|
||||
chain: this.walletClient.chain,
|
||||
account: this.walletClient.account!.address
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
}
|
||||
);
|
||||
|
||||
const hash: Hash = await this.contract.write.register(
|
||||
[identity.IDCommitmentBigInt, this.rateLimit, []],
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
chain: this.rpcClient.chain,
|
||||
account: this.rpcClient.account
|
||||
}
|
||||
@ -694,17 +397,6 @@ export class RLNBaseContract {
|
||||
const txRegisterReceipt = await this.rpcClient.waitForTransactionReceipt({
|
||||
hash
|
||||
});
|
||||
=======
|
||||
chain: this.walletClient.chain,
|
||||
account: this.walletClient.account!
|
||||
}
|
||||
);
|
||||
|
||||
const txRegisterReceipt =
|
||||
await this.publicClient.waitForTransactionReceipt({
|
||||
hash
|
||||
});
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
|
||||
if (txRegisterReceipt.status === "reverted") {
|
||||
throw new Error("Transaction failed on-chain");
|
||||
@ -735,7 +427,6 @@ export class RLNBaseContract {
|
||||
const decoded = decodeEventLog({
|
||||
abi: wakuRlnV2Abi,
|
||||
data: memberRegisteredLog.data,
|
||||
<<<<<<< HEAD
|
||||
topics: memberRegisteredLog.topics,
|
||||
eventName: "MembershipRegistered"
|
||||
});
|
||||
@ -743,35 +434,15 @@ export class RLNBaseContract {
|
||||
log.info(
|
||||
`Successfully registered membership with index ${decoded.args.index} ` +
|
||||
`and rate limit ${decoded.args.membershipRateLimit}`
|
||||
=======
|
||||
topics: memberRegisteredLog.topics
|
||||
});
|
||||
|
||||
const decodedArgs = decoded.args as {
|
||||
idCommitment: bigint;
|
||||
membershipRateLimit: number;
|
||||
index: number;
|
||||
};
|
||||
|
||||
log.info(
|
||||
`Successfully registered membership with index ${decodedArgs.index} ` +
|
||||
`and rate limit ${decodedArgs.membershipRateLimit}`
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
);
|
||||
|
||||
return {
|
||||
identity,
|
||||
membership: {
|
||||
address: this.contract.address,
|
||||
<<<<<<< HEAD
|
||||
treeIndex: decoded.args.index,
|
||||
chainId: String(RLN_CONTRACT.chainId),
|
||||
rateLimit: Number(decoded.args.membershipRateLimit)
|
||||
=======
|
||||
treeIndex: decodedArgs.index,
|
||||
chainId: String(RLN_CONTRACT.chainId),
|
||||
rateLimit: decodedArgs.membershipRateLimit
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
@ -820,10 +491,7 @@ export class RLNBaseContract {
|
||||
}
|
||||
|
||||
private async getMemberIndex(idCommitmentBigInt: bigint): Promise<number> {
|
||||
<<<<<<< HEAD
|
||||
// Current version of the contract has the index at position 5 in the membership struct
|
||||
=======
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
return (await this.contract.read.memberships([idCommitmentBigInt]))[5];
|
||||
}
|
||||
|
||||
@ -878,11 +546,7 @@ export class RLNBaseContract {
|
||||
price: bigint | null;
|
||||
}> {
|
||||
const address = await this.contract.read.priceCalculator();
|
||||
<<<<<<< HEAD
|
||||
const [token, price] = await this.rpcClient.readContract({
|
||||
=======
|
||||
const [token, price] = await this.publicClient.readContract({
|
||||
>>>>>>> a88dd8cdbd (feat: migrate rln from ethers to viem)
|
||||
address,
|
||||
abi: iPriceCalculatorAbi,
|
||||
functionName: "calculate",
|
||||
|
||||
@ -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";
|
||||
@ -16,6 +17,7 @@ export class RLNInstance extends RLNCredentialsManager {
|
||||
*/
|
||||
public static async create(): Promise<RLNInstance> {
|
||||
try {
|
||||
await initUtils();
|
||||
await init();
|
||||
zerokitRLN.initPanicHook();
|
||||
|
||||
|
||||
@ -49,6 +49,43 @@ export class BytesUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a BigInt to a Uint8Array with configurable output endianness
|
||||
* @param value - The BigInt to convert
|
||||
* @param byteLength - The desired byte length of the output (optional, auto-calculated if not provided)
|
||||
* @param outputEndianness - Endianness of the output bytes ('big' or 'little')
|
||||
* @returns Uint8Array representation of the BigInt
|
||||
*/
|
||||
public static fromBigInt(
|
||||
value: bigint,
|
||||
byteLength: number,
|
||||
outputEndianness: "big" | "little" = "little"
|
||||
): Uint8Array {
|
||||
if (value < 0n) {
|
||||
throw new Error("Cannot convert negative BigInt to bytes");
|
||||
}
|
||||
|
||||
if (value === 0n) {
|
||||
return new Uint8Array(byteLength);
|
||||
}
|
||||
|
||||
const result = new Uint8Array(byteLength);
|
||||
let workingValue = value;
|
||||
|
||||
// Extract bytes in big-endian order
|
||||
for (let i = byteLength - 1; i >= 0; i--) {
|
||||
result[i] = Number(workingValue & 0xffn);
|
||||
workingValue = workingValue >> 8n;
|
||||
}
|
||||
|
||||
// If we need little-endian output, reverse the array
|
||||
if (outputEndianness === "little") {
|
||||
result.reverse();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an unsigned integer to a buffer in little-endian format
|
||||
*/
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Logger } from "@waku/utils";
|
||||
|
||||
import { BytesUtils } from "./bytes.js";
|
||||
|
||||
const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds
|
||||
|
||||
const log = new Logger("rln:epoch");
|
||||
@ -15,11 +17,7 @@ export function dateToEpoch(
|
||||
}
|
||||
|
||||
export function epochIntToBytes(epoch: number): Uint8Array {
|
||||
const bytes = new Uint8Array(32);
|
||||
const db = new DataView(bytes.buffer);
|
||||
db.setUint32(0, epoch, true);
|
||||
log.info("encoded epoch", epoch, bytes);
|
||||
return bytes;
|
||||
return BytesUtils.writeUIntLE(new Uint8Array(32), epoch, 0, 32);
|
||||
}
|
||||
|
||||
export function epochBytesToInt(bytes: Uint8Array): number {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
||||
import { hash, poseidonHash as poseidon } from "@waku/zerokit-rln-wasm-utils";
|
||||
|
||||
import { BytesUtils } from "./bytes.js";
|
||||
|
||||
@ -10,16 +10,9 @@ export function poseidonHash(...input: Array<Uint8Array>): Uint8Array {
|
||||
8
|
||||
);
|
||||
const lenPrefixedData = BytesUtils.concatenate(inputLen, ...input);
|
||||
return zerokitRLN.poseidonHash(lenPrefixedData);
|
||||
return poseidon(lenPrefixedData, true);
|
||||
}
|
||||
|
||||
export function sha256(input: Uint8Array): Uint8Array {
|
||||
const inputLen = BytesUtils.writeUIntLE(
|
||||
new Uint8Array(8),
|
||||
input.length,
|
||||
0,
|
||||
8
|
||||
);
|
||||
const lenPrefixedData = BytesUtils.concatenate(inputLen, input);
|
||||
return zerokitRLN.hash(lenPrefixedData);
|
||||
return hash(input, true);
|
||||
}
|
||||
|
||||
179
packages/rln/src/utils/merkle.ts
Normal file
179
packages/rln/src/utils/merkle.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import { BytesUtils } from "./bytes.js";
|
||||
import { poseidonHash } from "./hash.js";
|
||||
|
||||
/**
|
||||
* The fixed depth of the Merkle tree used in the RLN contract
|
||||
* This is a constant that will never change for the on-chain implementation
|
||||
*/
|
||||
export const MERKLE_TREE_DEPTH = 20;
|
||||
|
||||
/**
|
||||
* Reconstructs a Merkle tree root from a proof and leaf information
|
||||
*
|
||||
* @param proof - Array of MERKLE_TREE_DEPTH bigint elements representing the Merkle proof
|
||||
* @param leafIndex - The index of the leaf in the tree (used to determine left/right positioning)
|
||||
* @param leafValue - The value of the leaf (typically the rate commitment)
|
||||
* @returns The reconstructed root as a bigint
|
||||
*/
|
||||
export function reconstructMerkleRoot(
|
||||
proof: readonly bigint[],
|
||||
leafIndex: bigint,
|
||||
leafValue: bigint
|
||||
): bigint {
|
||||
if (proof.length !== MERKLE_TREE_DEPTH) {
|
||||
throw new Error(
|
||||
`Expected proof of length ${MERKLE_TREE_DEPTH}, got ${proof.length}`
|
||||
);
|
||||
}
|
||||
|
||||
let currentValue = leafValue;
|
||||
|
||||
// Process each level of the tree (0 to MERKLE_TREE_DEPTH-1)
|
||||
for (let level = 0; level < MERKLE_TREE_DEPTH; level++) {
|
||||
// Check if bit `level` is set in the leaf index
|
||||
const bit = (leafIndex >> BigInt(level)) & 1n;
|
||||
|
||||
// Convert bigints to Uint8Array for hashing
|
||||
const currentBytes = bigIntToBytes32(currentValue);
|
||||
const proofBytes = bigIntToBytes32(proof[level]);
|
||||
|
||||
let hashResult: Uint8Array;
|
||||
|
||||
if (bit === 0n) {
|
||||
// Current node is a left child: hash(current, proof[level])
|
||||
hashResult = poseidonHash(currentBytes, proofBytes);
|
||||
} else {
|
||||
// Current node is a right child: hash(proof[level], current)
|
||||
hashResult = poseidonHash(proofBytes, currentBytes);
|
||||
}
|
||||
|
||||
// Convert hash result back to bigint for next iteration
|
||||
currentValue = BytesUtils.toBigInt(hashResult, "little");
|
||||
}
|
||||
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts index information from a Merkle proof by attempting to reconstruct
|
||||
* the root with different possible indices and comparing against the expected root
|
||||
*
|
||||
* @param proof - Array of MERKLE_TREE_DEPTH bigint elements representing the Merkle proof
|
||||
* @param leafValue - The value of the leaf (typically the rate commitment)
|
||||
* @param expectedRoot - The expected root to match against
|
||||
* @param maxIndex - Maximum index to try (default: 2^MERKLE_TREE_DEPTH - 1)
|
||||
* @returns The index that produces the expected root, or null if not found
|
||||
*/
|
||||
function extractIndexFromProof(
|
||||
proof: readonly bigint[],
|
||||
leafValue: bigint,
|
||||
expectedRoot: bigint,
|
||||
maxIndex: bigint = (1n << BigInt(MERKLE_TREE_DEPTH)) - 1n
|
||||
): bigint | null {
|
||||
// Try different indices to see which one produces the expected root
|
||||
for (let index = 0n; index <= maxIndex; index++) {
|
||||
try {
|
||||
const reconstructedRoot = reconstructMerkleRoot(proof, index, leafValue);
|
||||
if (reconstructedRoot === expectedRoot) {
|
||||
return index;
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue trying other indices if reconstruction fails
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the rate commitment from an ID commitment and rate limit
|
||||
* This matches the contract's calculation: PoseidonT3.hash([idCommitment, rateLimit])
|
||||
*
|
||||
* @param idCommitment - The identity commitment as a bigint
|
||||
* @param rateLimit - The rate limit as a bigint
|
||||
* @returns The rate commitment as a bigint
|
||||
*/
|
||||
export function calculateRateCommitment(
|
||||
idCommitment: bigint,
|
||||
rateLimit: bigint
|
||||
): bigint {
|
||||
const idBytes = bigIntToBytes32(idCommitment);
|
||||
const rateLimitBytes = bigIntToBytes32(rateLimit);
|
||||
|
||||
const hashResult = poseidonHash(idBytes, rateLimitBytes);
|
||||
return BytesUtils.toBigInt(hashResult, "little");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a bigint to a 32-byte Uint8Array in little-endian format
|
||||
*
|
||||
* @param value - The bigint value to convert
|
||||
* @returns 32-byte Uint8Array representation
|
||||
*/
|
||||
function bigIntToBytes32(value: bigint): Uint8Array {
|
||||
const bytes = new Uint8Array(32);
|
||||
let temp = value;
|
||||
|
||||
for (let i = 0; i < 32; i++) {
|
||||
bytes[i] = Number(temp & 0xffn);
|
||||
temp >>= 8n;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the path direction bits from a Merkle proof by finding the leaf index
|
||||
* that produces the expected root, then converting that index to path directions
|
||||
*
|
||||
* @param proof - Array of MERKLE_TREE_DEPTH bigint elements representing the Merkle proof
|
||||
* @param leafValue - The value of the leaf (typically the rate commitment)
|
||||
* @param expectedRoot - The expected root to match against
|
||||
* @param maxIndex - Maximum index to try (default: 2^MERKLE_TREE_DEPTH - 1)
|
||||
* @returns Array of MERKLE_TREE_DEPTH numbers (0 or 1) representing path directions, or null if no valid path found
|
||||
* - 0 means the node is a left child (hash order: current, sibling)
|
||||
* - 1 means the node is a right child (hash order: sibling, current)
|
||||
*/
|
||||
export function extractPathDirectionsFromProof(
|
||||
proof: readonly bigint[],
|
||||
leafValue: bigint,
|
||||
expectedRoot: bigint,
|
||||
maxIndex: bigint = (1n << BigInt(MERKLE_TREE_DEPTH)) - 1n
|
||||
): number[] | null {
|
||||
// First, find the leaf index that produces the expected root
|
||||
const leafIndex = extractIndexFromProof(
|
||||
proof,
|
||||
leafValue,
|
||||
expectedRoot,
|
||||
maxIndex
|
||||
);
|
||||
|
||||
if (leafIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert the leaf index to path directions
|
||||
return getPathDirectionsFromIndex(leafIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a leaf index to an array of path direction bits
|
||||
*
|
||||
* @param leafIndex - The index of the leaf in the tree
|
||||
* @returns Array of MERKLE_TREE_DEPTH numbers (0 or 1) representing path directions
|
||||
* - 0 means the node is a left child (hash order: current, sibling)
|
||||
* - 1 means the node is a right child (hash order: sibling, current)
|
||||
*/
|
||||
function getPathDirectionsFromIndex(leafIndex: bigint): number[] {
|
||||
const pathDirections: number[] = [];
|
||||
|
||||
// For each level (0 to MERKLE_TREE_DEPTH-1), extract the bit that determines left/right
|
||||
for (let level = 0; level < MERKLE_TREE_DEPTH; level++) {
|
||||
// Check if bit `level` is set in the leaf index
|
||||
const bit = (leafIndex >> BigInt(level)) & 1n;
|
||||
pathDirections.push(Number(bit));
|
||||
}
|
||||
|
||||
return pathDirections;
|
||||
}
|
||||
7
packages/rln/src/utils/test_keystore.ts
Normal file
7
packages/rln/src/utils/test_keystore.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const TEST_KEYSTORE_DATA = {
|
||||
keystoreJson:
|
||||
'{"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"
|
||||
};
|
||||
@ -1,14 +1,21 @@
|
||||
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
||||
import { generateSeededExtendedMembershipKey } from "@waku/zerokit-rln-wasm-utils";
|
||||
|
||||
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
|
||||
import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./contract/constants.js";
|
||||
import { IdentityCredential } from "./identity.js";
|
||||
import { WitnessCalculator } from "./resources/witness_calculator";
|
||||
import { BytesUtils } from "./utils/bytes.js";
|
||||
import { dateToEpoch, epochIntToBytes } from "./utils/epoch.js";
|
||||
import { poseidonHash, sha256 } from "./utils/hash.js";
|
||||
|
||||
export class Zerokit {
|
||||
public constructor(
|
||||
private readonly zkRLN: number,
|
||||
private readonly witnessCalculator: WitnessCalculator,
|
||||
private readonly _rateLimit: number = DEFAULT_RATE_LIMIT
|
||||
private readonly _rateLimit: number = DEFAULT_RATE_LIMIT,
|
||||
private readonly rlnIdentifier: Uint8Array = new TextEncoder().encode(
|
||||
"rln/waku-rln-relay/v2.0.0"
|
||||
)
|
||||
) {}
|
||||
|
||||
public get getZkRLN(): number {
|
||||
@ -26,10 +33,117 @@ export class Zerokit {
|
||||
public generateSeededIdentityCredential(seed: string): IdentityCredential {
|
||||
const stringEncoder = new TextEncoder();
|
||||
const seedBytes = stringEncoder.encode(seed);
|
||||
const memKeys = zerokitRLN.generateSeededExtendedMembershipKey(
|
||||
this.zkRLN,
|
||||
seedBytes
|
||||
);
|
||||
const memKeys = generateSeededExtendedMembershipKey(seedBytes, true);
|
||||
return IdentityCredential.fromBytes(memKeys);
|
||||
}
|
||||
|
||||
public async serializeWitness(
|
||||
idSecretHash: Uint8Array,
|
||||
pathElements: Uint8Array[],
|
||||
identityPathIndex: Uint8Array[],
|
||||
x: Uint8Array,
|
||||
epoch: Uint8Array,
|
||||
rateLimit: number,
|
||||
messageId: number // number of message sent by the user in this epoch
|
||||
): Promise<Uint8Array> {
|
||||
const externalNullifier = poseidonHash(
|
||||
sha256(epoch),
|
||||
sha256(this.rlnIdentifier)
|
||||
);
|
||||
const pathElementsBytes = new Uint8Array(8 + pathElements.length * 32);
|
||||
BytesUtils.writeUIntLE(pathElementsBytes, pathElements.length, 0, 8);
|
||||
for (let i = 0; i < pathElements.length; i++) {
|
||||
// We assume that the path elements are already in little-endian format
|
||||
pathElementsBytes.set(pathElements[i], 8 + i * 32);
|
||||
}
|
||||
const identityPathIndexBytes = new Uint8Array(
|
||||
8 + identityPathIndex.length * 1
|
||||
);
|
||||
BytesUtils.writeUIntLE(
|
||||
identityPathIndexBytes,
|
||||
identityPathIndex.length,
|
||||
0,
|
||||
8
|
||||
);
|
||||
for (let i = 0; i < identityPathIndex.length; i++) {
|
||||
// We assume that each identity path index is already in little-endian format
|
||||
identityPathIndexBytes.set(identityPathIndex[i], 8 + i * 1);
|
||||
}
|
||||
return BytesUtils.concatenate(
|
||||
idSecretHash,
|
||||
BytesUtils.writeUIntLE(new Uint8Array(32), rateLimit, 0, 32),
|
||||
BytesUtils.writeUIntLE(new Uint8Array(32), messageId, 0, 32),
|
||||
pathElementsBytes,
|
||||
identityPathIndexBytes,
|
||||
x,
|
||||
externalNullifier
|
||||
);
|
||||
}
|
||||
|
||||
public async generateRLNProof(
|
||||
msg: Uint8Array,
|
||||
index: number, // index of the leaf in the merkle tree
|
||||
epoch: Uint8Array | Date | undefined,
|
||||
idSecretHash: Uint8Array,
|
||||
pathElements: Uint8Array[],
|
||||
identityPathIndex: Uint8Array[],
|
||||
rateLimit: number,
|
||||
messageId: number // number of message sent by the user in this epoch
|
||||
): Promise<Uint8Array> {
|
||||
if (epoch === undefined) {
|
||||
epoch = epochIntToBytes(dateToEpoch(new Date()));
|
||||
} else if (epoch instanceof Date) {
|
||||
epoch = epochIntToBytes(dateToEpoch(epoch));
|
||||
}
|
||||
|
||||
if (epoch.length !== 32) throw new Error("invalid epoch");
|
||||
if (idSecretHash.length !== 32) throw new Error("invalid id secret hash");
|
||||
if (index < 0) throw new Error("index must be >= 0");
|
||||
if (
|
||||
rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
|
||||
rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
|
||||
) {
|
||||
throw new Error(
|
||||
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}`
|
||||
);
|
||||
}
|
||||
|
||||
const x = sha256(msg);
|
||||
|
||||
const serializedWitness = await this.serializeWitness(
|
||||
idSecretHash,
|
||||
pathElements,
|
||||
identityPathIndex,
|
||||
x,
|
||||
epoch,
|
||||
rateLimit,
|
||||
messageId
|
||||
);
|
||||
const witnessJson: Record<string, unknown> = zerokitRLN.rlnWitnessToJson(
|
||||
this.zkRLN,
|
||||
serializedWitness
|
||||
) as Record<string, unknown>;
|
||||
const calculatedWitness: bigint[] =
|
||||
await this.witnessCalculator.calculateWitness(witnessJson);
|
||||
return zerokitRLN.generateRLNProofWithWitness(
|
||||
this.zkRLN,
|
||||
calculatedWitness,
|
||||
serializedWitness
|
||||
);
|
||||
}
|
||||
|
||||
public verifyRLNProof(
|
||||
signalLength: Uint8Array,
|
||||
signal: Uint8Array,
|
||||
proof: Uint8Array,
|
||||
roots: Uint8Array[]
|
||||
): boolean {
|
||||
if (signalLength.length !== 8)
|
||||
throw new Error("signalLength must be 8 bytes");
|
||||
return zerokitRLN.verifyWithRoots(
|
||||
this.zkRLN,
|
||||
BytesUtils.concatenate(proof, signalLength, signal),
|
||||
BytesUtils.concatenate(...roots)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user