From 317d835c3a8204c38d5ae42ab7978c4251fffb5f Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Wed, 2 Apr 2025 20:39:03 +0530 Subject: [PATCH 1/4] chore: rename RLNLight to CredentialsManager, add logs --- .../{rln_light.ts => credentials_manager.ts} | 110 ++++++++++-------- packages/rln/src/index.ts | 4 +- packages/rln/src/rln.ts | 26 +---- packages/rln/src/types.ts | 31 +++++ 4 files changed, 97 insertions(+), 74 deletions(-) rename packages/rln/src/{rln_light.ts => credentials_manager.ts} (68%) create mode 100644 packages/rln/src/types.ts diff --git a/packages/rln/src/rln_light.ts b/packages/rln/src/credentials_manager.ts similarity index 68% rename from packages/rln/src/rln_light.ts rename to packages/rln/src/credentials_manager.ts index 1c2fde41be..c0100e7486 100644 --- a/packages/rln/src/rln_light.ts +++ b/packages/rln/src/credentials_manager.ts @@ -12,51 +12,20 @@ import type { EncryptedCredentials } from "./keystore/index.js"; import { KeystoreEntity, Password } from "./keystore/types.js"; +import { RegisterMembershipOptions, StartRLNOptions } from "./types.js"; import { buildBigIntFromUint8Array, extractMetaMaskSigner } from "./utils/index.js"; -const log = new Logger("waku:rln"); +const log = new Logger("waku:credentials"); /** - * Create an instance of RLN - * @returns RLNInstance + * Manages credentials for RLN + * This is a lightweight implementation of the RLN contract that doesn't require Zerokit + * It is used to register membership and generate identity credentials */ -export async function create(): Promise { - try { - return new RLNLightInstance(); - } catch (error) { - log.error("Failed to initialize RLN:", error); - throw error; - } -} - -type StartRLNOptions = { - /** - * If not set - will extract MetaMask account and get signer from it. - */ - signer?: ethers.Signer; - /** - * If not set - will use default SEPOLIA_CONTRACT address. - */ - address?: string; - /** - * Credentials to use for generating proofs and connecting to the contract and network. - * If provided used for validating the network chainId and connecting to registry contract. - */ - credentials?: EncryptedCredentials | DecryptedCredentials; - /** - * Rate limit for the member. - */ - rateLimit?: number; -}; - -type RegisterMembershipOptions = - | { signature: string } - | { identity: IdentityCredential }; - -export class RLNLightInstance { +export class RLNCredentialsManager { private started = false; private starting = false; @@ -66,7 +35,9 @@ export class RLNLightInstance { private keystore = Keystore.create(); private _credentials: undefined | DecryptedCredentials; - public constructor() {} + public constructor() { + log.info("RLNCredentialsManager initialized"); + } public get contract(): undefined | RLNLightContract { return this._contract; @@ -78,21 +49,33 @@ export class RLNLightInstance { public async start(options: StartRLNOptions = {}): Promise { if (this.started || this.starting) { + log.info("RLNCredentialsManager already started or starting"); return; } + log.info("Starting RLNCredentialsManager"); this.starting = true; try { const { credentials, keystore } = - await RLNLightInstance.decryptCredentialsIfNeeded(options.credentials); + await RLNCredentialsManager.decryptCredentialsIfNeeded( + options.credentials + ); + + if (credentials) { + log.info("Credentials successfully decrypted"); + } + const { signer, address, rateLimit } = await this.determineStartOptions( options, credentials ); + log.info(`Using contract address: ${address}`); + if (keystore) { this.keystore = keystore; + log.info("Using provided keystore"); } this._credentials = credentials; @@ -102,7 +85,12 @@ export class RLNLightInstance { signer: signer!, rateLimit: rateLimit }); + + log.info("RLNCredentialsManager successfully started"); this.started = true; + } catch (error) { + log.error("Failed to start RLNCredentialsManager", error); + throw error; } finally { this.starting = false; } @@ -124,12 +112,17 @@ export class RLNLightInstance { if (address === LINEA_CONTRACT.address) { chainId = LINEA_CONTRACT.chainId; + log.info(`Using Linea contract with chainId: ${chainId}`); } const signer = options.signer || (await extractMetaMaskSigner()); const currentChainId = await signer.getChainId(); + log.info(`Current chain ID: ${currentChainId}`); if (chainId && chainId !== currentChainId) { + log.error( + `Chain ID mismatch: contract=${chainId}, current=${currentChainId}` + ); throw Error( `Failed to start RLN contract, chain ID of contract is different from current one: contract-${chainId}, current network-${currentChainId}` ); @@ -145,28 +138,38 @@ export class RLNLightInstance { credentials?: EncryptedCredentials | DecryptedCredentials ): Promise<{ credentials?: DecryptedCredentials; keystore?: Keystore }> { if (!credentials) { + log.info("No credentials provided"); return {}; } if ("identity" in credentials) { + log.info("Using already decrypted credentials"); return { credentials }; } + log.info("Attempting to decrypt credentials"); const keystore = Keystore.fromString(credentials.keystore); if (!keystore) { + log.warn("Failed to create keystore from string"); return {}; } - const decryptedCredentials = await keystore.readCredential( - credentials.id, - credentials.password - ); + try { + const decryptedCredentials = await keystore.readCredential( + credentials.id, + credentials.password + ); + log.info(`Successfully decrypted credentials with ID: ${credentials.id}`); - return { - keystore, - credentials: decryptedCredentials - }; + return { + keystore, + credentials: decryptedCredentials + }; + } catch (error) { + log.error("Failed to decrypt credentials", error); + throw error; + } } /** @@ -176,6 +179,7 @@ export class RLNLightInstance { * @returns IdentityCredential */ private generateSeededIdentityCredential(seed: string): IdentityCredential { + log.info("Generating seeded identity credential"); // Convert the seed to bytes const encoder = new TextEncoder(); const seedBytes = encoder.encode(seed); @@ -195,6 +199,7 @@ export class RLNLightInstance { // Convert IDCommitment to BigInt const idCommitmentBigInt = buildBigIntFromUint8Array(idCommitment); + log.info("Successfully generated identity credential"); return new IdentityCredential( idTrapdoor, idNullifier, @@ -208,19 +213,24 @@ export class RLNLightInstance { options: RegisterMembershipOptions ): Promise { if (!this.contract) { + log.error("RLN Contract is not initialized"); throw Error("RLN Contract is not initialized."); } + log.info("Registering membership"); let identity = "identity" in options && options.identity; if ("signature" in options) { + log.info("Generating identity from signature"); identity = this.generateSeededIdentityCredential(options.signature); } if (!identity) { + log.error("Missing signature or identity to register membership"); throw Error("Missing signature or identity to register membership."); } + log.info("Registering identity with contract"); return this.contract.registerWithIdentity(identity); } @@ -230,6 +240,12 @@ export class RLNLightInstance { * @param password: string or bytes to use to decrypt credentials from Keystore */ public async useCredentials(id: string, password: Password): Promise { + log.info(`Attempting to use credentials with ID: ${id}`); this._credentials = await this.keystore?.readCredential(id, password); + if (this._credentials) { + log.info("Successfully loaded credentials"); + } else { + log.warn("Failed to load credentials"); + } } } diff --git a/packages/rln/src/index.ts b/packages/rln/src/index.ts index aa18f8e89c..0d7e825e3d 100644 --- a/packages/rln/src/index.ts +++ b/packages/rln/src/index.ts @@ -3,16 +3,16 @@ import { RLN_ABI } from "./contract/abi.js"; import { LINEA_CONTRACT, RLNContract } from "./contract/index.js"; import { RLNLightContract } from "./contract/rln_light_contract.js"; import { createRLN } from "./create.js"; +import { RLNCredentialsManager } from "./credentials_manager.js"; import { IdentityCredential } from "./identity.js"; import { Keystore } from "./keystore/index.js"; import { Proof } from "./proof.js"; import { RLNInstance } from "./rln.js"; -import { RLNLightInstance } from "./rln_light.js"; import { MerkleRootTracker } from "./root_tracker.js"; import { extractMetaMaskSigner } from "./utils/index.js"; export { - RLNLightInstance, + RLNCredentialsManager, RLNLightContract, createRLN, Keystore, diff --git a/packages/rln/src/rln.ts b/packages/rln/src/rln.ts index ec16f79e7f..54013c7f47 100644 --- a/packages/rln/src/rln.ts +++ b/packages/rln/src/rln.ts @@ -17,7 +17,6 @@ import { } from "./codec.js"; import { DEFAULT_RATE_LIMIT } from "./contract/constants.js"; import { LINEA_CONTRACT, RLNContract } from "./contract/index.js"; -import { IdentityCredential } from "./identity.js"; import { Keystore } from "./keystore/index.js"; import type { DecryptedCredentials, @@ -27,6 +26,7 @@ import { KeystoreEntity, Password } from "./keystore/types.js"; import verificationKey from "./resources/verification_key"; import * as wc from "./resources/witness_calculator"; import { WitnessCalculator } from "./resources/witness_calculator"; +import { RegisterMembershipOptions, StartRLNOptions } from "./types.js"; import { extractMetaMaskSigner } from "./utils/index.js"; import { Zerokit } from "./zerokit.js"; @@ -102,30 +102,6 @@ export async function create(): Promise { } } -type StartRLNOptions = { - /** - * If not set - will extract MetaMask account and get signer from it. - */ - signer?: ethers.Signer; - /** - * If not set - will use default LINEA_CONTRACT address. - */ - address?: string; - /** - * Credentials to use for generating proofs and connecting to the contract and network. - * If provided used for validating the network chainId and connecting to registry contract. - */ - credentials?: EncryptedCredentials | DecryptedCredentials; - /** - * Rate limit for the member. - */ - rateLimit?: number; -}; - -type RegisterMembershipOptions = - | { signature: string } - | { identity: IdentityCredential }; - type WakuRLNEncoderOptions = WakuEncoderOptions & { credentials: EncryptedCredentials | DecryptedCredentials; }; diff --git a/packages/rln/src/types.ts b/packages/rln/src/types.ts new file mode 100644 index 0000000000..7980c8aea9 --- /dev/null +++ b/packages/rln/src/types.ts @@ -0,0 +1,31 @@ +import { ethers } from "ethers"; + +import { IdentityCredential } from "./identity.js"; +import { + DecryptedCredentials, + EncryptedCredentials +} from "./keystore/types.js"; + +export type StartRLNOptions = { + /** + * If not set - will extract MetaMask account and get signer from it. + */ + signer?: ethers.Signer; + /** + * If not set - will use default SEPOLIA_CONTRACT address. + */ + address?: string; + /** + * Credentials to use for generating proofs and connecting to the contract and network. + * If provided used for validating the network chainId and connecting to registry contract. + */ + credentials?: EncryptedCredentials | DecryptedCredentials; + /** + * Rate limit for the member. + */ + rateLimit?: number; +}; + +export type RegisterMembershipOptions = + | { signature: string } + | { identity: IdentityCredential }; From 9f8fd15ba9fbb216e24d5cec850434a50d1f721d Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Thu, 3 Apr 2025 18:21:40 +0530 Subject: [PATCH 2/4] chore: setup and use rln_base_contract --- ...light_contract.ts => rln_base_contract.ts} | 606 ++++++++-------- .../rln/src/contract/rln_contract.spec.ts | 2 +- packages/rln/src/contract/rln_contract.ts | 672 +----------------- packages/rln/src/contract/types.ts | 48 ++ packages/rln/src/credentials_manager.ts | 8 +- packages/rln/src/index.ts | 4 +- 6 files changed, 358 insertions(+), 982 deletions(-) rename packages/rln/src/contract/{rln_light_contract.ts => rln_base_contract.ts} (78%) create mode 100644 packages/rln/src/contract/types.ts diff --git a/packages/rln/src/contract/rln_light_contract.ts b/packages/rln/src/contract/rln_base_contract.ts similarity index 78% rename from packages/rln/src/contract/rln_light_contract.ts rename to packages/rln/src/contract/rln_base_contract.ts index 2205c704f9..9393476709 100644 --- a/packages/rln/src/contract/rln_light_contract.ts +++ b/packages/rln/src/contract/rln_base_contract.ts @@ -1,84 +1,47 @@ import { Logger } from "@waku/utils"; import { ethers } from "ethers"; -import type { IdentityCredential } from "../identity.js"; -import type { DecryptedCredentials } from "../keystore/index.js"; +import { IdentityCredential } from "../identity.js"; +import { DecryptedCredentials } from "../keystore/types.js"; import { RLN_ABI } from "./abi.js"; import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./constants.js"; +import { + CustomQueryOptions, + FetchMembersOptions, + Member, + MembershipInfo, + MembershipRegisteredEvent, + MembershipState, + RLNContractInitOptions +} from "./types.js"; -const log = new Logger("waku:rln:contract"); +const log = new Logger("waku:rln:contract:base"); -type Member = { - idCommitment: string; - index: ethers.BigNumber; -}; - -interface RLNContractOptions { - signer: ethers.Signer; - address: string; - rateLimit?: number; -} - -interface RLNContractInitOptions extends RLNContractOptions { - contract?: ethers.Contract; -} - -export interface MembershipRegisteredEvent { - idCommitment: string; - membershipRateLimit: ethers.BigNumber; - index: ethers.BigNumber; -} - -type FetchMembersOptions = { - fromBlock?: number; - fetchRange?: number; - fetchChunks?: number; -}; - -export interface MembershipInfo { - index: ethers.BigNumber; - idCommitment: string; - rateLimit: number; - startBlock: number; - endBlock: number; - state: MembershipState; -} - -export enum MembershipState { - Active = "Active", - GracePeriod = "GracePeriod", - Expired = "Expired", - ErasedAwaitsWithdrawal = "ErasedAwaitsWithdrawal" -} - -export class RLNLightContract { +export class RLNBaseContract { public contract: ethers.Contract; - private deployBlock: undefined | number; private rateLimit: number; - private _members: Map = new Map(); + protected _members: Map = new Map(); private _membersFilter: ethers.EventFilter; private _membershipErasedFilter: ethers.EventFilter; private _membersExpiredFilter: ethers.EventFilter; /** - * Asynchronous initializer for RLNContract. + * Constructor for RLNBaseContract. * Allows injecting a mocked contract for testing purposes. */ - public static async init( - options: RLNContractInitOptions - ): Promise { - const rlnContract = new RLNLightContract(options); + public constructor(options: RLNContractInitOptions) { + // Initialize members and subscriptions + this.fetchMembers() + .then(() => { + this.subscribeToMembers(); + }) + .catch((error) => { + log.error("Failed to initialize members", { error }); + }); - await rlnContract.fetchMembers(); - rlnContract.subscribeToMembers(); - - return rlnContract; - } - - private constructor(options: RLNContractInitOptions) { const { address, signer, @@ -86,6 +49,22 @@ export class RLNLightContract { contract } = options; + this.validateRateLimit(rateLimit); + + this.contract = contract || new ethers.Contract(address, RLN_ABI, signer); + this.rateLimit = rateLimit; + + // Initialize event filters + this._membersFilter = this.contract.filters.MembershipRegistered(); + this._membershipErasedFilter = this.contract.filters.MembershipErased(); + this._membersExpiredFilter = this.contract.filters.MembershipExpired(); + } + + /** + * Validates that the rate limit is within the allowed range + * @throws Error if the rate limit is outside the allowed range + */ + private validateRateLimit(rateLimit: number): void { if ( rateLimit < RATE_LIMIT_PARAMS.MIN_RATE || rateLimit > RATE_LIMIT_PARAMS.MAX_RATE @@ -94,16 +73,6 @@ export class RLNLightContract { `Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE} messages per epoch` ); } - - this.rateLimit = rateLimit; - - // Use the injected contract if provided; otherwise, instantiate a new one. - this.contract = contract || new ethers.Contract(address, RLN_ABI, signer); - - // Initialize event filters - this._membersFilter = this.contract.filters.MembershipRegistered(); - this._membershipErasedFilter = this.contract.filters.MembershipErased(); - this._membersExpiredFilter = this.contract.filters.MembershipExpired(); } /** @@ -151,7 +120,7 @@ export class RLNLightContract { */ public async getMaxTotalRateLimit(): Promise { const maxTotalRate = await this.contract.maxTotalRateLimit(); - return ethers.BigNumber.from(maxTotalRate).toNumber(); + return maxTotalRate.toNumber(); } /** @@ -160,7 +129,7 @@ export class RLNLightContract { */ public async getCurrentTotalRateLimit(): Promise { const currentTotal = await this.contract.currentTotalRateLimit(); - return ethers.BigNumber.from(currentTotal).toNumber(); + return currentTotal.toNumber(); } /** @@ -172,9 +141,7 @@ export class RLNLightContract { this.contract.maxTotalRateLimit(), this.contract.currentTotalRateLimit() ]); - return ethers.BigNumber.from(maxTotal) - .sub(ethers.BigNumber.from(currentTotal)) - .toNumber(); + return Number(maxTotal) - Number(currentTotal); } /** @@ -182,6 +149,7 @@ export class RLNLightContract { * @param newRateLimit The new rate limit to use */ public async setRateLimit(newRateLimit: number): Promise { + this.validateRateLimit(newRateLimit); this.rateLimit = newRateLimit; } @@ -214,21 +182,30 @@ export class RLNLightContract { } public async fetchMembers(options: FetchMembersOptions = {}): Promise { - const registeredMemberEvents = await queryFilter(this.contract, { - fromBlock: this.deployBlock, - ...options, - membersFilter: this.membersFilter - }); - const removedMemberEvents = await queryFilter(this.contract, { - fromBlock: this.deployBlock, - ...options, - membersFilter: this.membershipErasedFilter - }); - const expiredMemberEvents = await queryFilter(this.contract, { - fromBlock: this.deployBlock, - ...options, - membersFilter: this.membersExpiredFilter - }); + const registeredMemberEvents = await RLNBaseContract.queryFilter( + this.contract, + { + fromBlock: this.deployBlock, + ...options, + membersFilter: this.membersFilter + } + ); + const removedMemberEvents = await RLNBaseContract.queryFilter( + this.contract, + { + fromBlock: this.deployBlock, + ...options, + membersFilter: this.membershipErasedFilter + } + ); + const expiredMemberEvents = await RLNBaseContract.queryFilter( + this.contract, + { + fromBlock: this.deployBlock, + ...options, + membersFilter: this.membersExpiredFilter + } + ); const events = [ ...registeredMemberEvents, @@ -238,6 +215,58 @@ export class RLNLightContract { this.processEvents(events); } + public static async queryFilter( + contract: ethers.Contract, + options: CustomQueryOptions + ): Promise { + const FETCH_CHUNK = 5; + const BLOCK_RANGE = 3000; + + const { + fromBlock, + membersFilter, + fetchRange = BLOCK_RANGE, + fetchChunks = FETCH_CHUNK + } = options; + + if (fromBlock === undefined) { + return contract.queryFilter(membersFilter); + } + + if (!contract.provider) { + throw Error("No provider found on the contract."); + } + + const toBlock = await contract.provider.getBlockNumber(); + + if (toBlock - fromBlock < fetchRange) { + return contract.queryFilter(membersFilter, fromBlock, toBlock); + } + + const events: ethers.Event[][] = []; + const chunks = RLNBaseContract.splitToChunks( + fromBlock, + toBlock, + fetchRange + ); + + for (const portion of RLNBaseContract.takeN<[number, number]>( + chunks, + fetchChunks + )) { + const promises = portion.map(([left, right]) => + RLNBaseContract.ignoreErrors( + contract.queryFilter(membersFilter, left, right), + [] + ) + ); + const fetchedEvents = await Promise.all(promises); + events.push(fetchedEvents.flatMap((v) => v)); + } + + return events.flatMap((v) => v); + } + public processEvents(events: ethers.Event[]): void { const toRemoveTable = new Map(); const toInsertTable = new Map(); @@ -280,6 +309,53 @@ export class RLNLightContract { }); } + 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(array: T[], size: number): Iterable { + let start = 0; + + while (start < array.length) { + const portion = array.slice(start, start + size); + + yield portion; + + start += size; + } + } + + public static async ignoreErrors( + promise: Promise, + defaultValue: T + ): Promise { + 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.on( this.membersFilter, @@ -318,6 +394,133 @@ export class RLNLightContract { ); } + /** + * Helper method to get remaining messages in current epoch + * @param membershipId The ID of the membership to check + * @returns number of remaining messages allowed in current epoch + */ + public async getRemainingMessages(membershipId: number): Promise { + try { + const [startTime, , rateLimit] = + await this.contract.getMembershipInfo(membershipId); + + // Calculate current epoch + const currentTime = Math.floor(Date.now() / 1000); + const epochsPassed = Math.floor( + (currentTime - startTime) / RATE_LIMIT_PARAMS.EPOCH_LENGTH + ); + const currentEpochStart = + startTime + epochsPassed * RATE_LIMIT_PARAMS.EPOCH_LENGTH; + + // Get message count in current epoch using contract's function + const messageCount = await this.contract.getMessageCount( + membershipId, + currentEpochStart + ); + return Math.max( + 0, + ethers.BigNumber.from(rateLimit) + .sub(ethers.BigNumber.from(messageCount)) + .toNumber() + ); + } catch (error) { + log.error( + `Error getting remaining messages: ${(error as Error).message}` + ); + return 0; // Fail safe: assume no messages remaining on error + } + } + + public async getMembershipInfo( + idCommitment: string + ): Promise { + try { + const [startBlock, endBlock, rateLimit] = + await this.contract.getMembershipInfo(idCommitment); + const currentBlock = await this.contract.provider.getBlockNumber(); + + let state: MembershipState; + if (currentBlock < startBlock) { + state = MembershipState.Active; + } else if (currentBlock < endBlock) { + state = MembershipState.GracePeriod; + } else { + state = MembershipState.Expired; + } + + const index = await this.getMemberIndex(idCommitment); + if (!index) return undefined; + + return { + index, + idCommitment, + rateLimit: rateLimit.toNumber(), + startBlock: startBlock.toNumber(), + endBlock: endBlock.toNumber(), + state + }; + } catch (error) { + return undefined; + } + } + + public async extendMembership( + idCommitment: string + ): Promise { + return this.contract.extendMemberships([idCommitment]); + } + + public async eraseMembership( + idCommitment: string, + eraseFromMembershipSet: boolean = true + ): Promise { + return this.contract.eraseMemberships( + [idCommitment], + eraseFromMembershipSet + ); + } + + private async getMemberIndex( + idCommitment: string + ): Promise { + try { + const events = await this.contract.queryFilter( + this.contract.filters.MembershipRegistered(idCommitment) + ); + if (events.length === 0) return undefined; + + // Get the most recent registration event + const event = events[events.length - 1]; + return event.args?.index; + } catch (error) { + return undefined; + } + } + + public async registerMembership( + idCommitment: string, + rateLimit: number = DEFAULT_RATE_LIMIT + ): Promise { + 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}` + ); + } + return this.contract.register(idCommitment, rateLimit, []); + } + + public async withdraw(token: string, holder: string): Promise { + try { + const tx = await this.contract.withdraw(token, { from: holder }); + await tx.wait(); + } catch (error) { + log.error(`Error in withdraw: ${(error as Error).message}`); + } + } + public async registerWithIdentity( identity: IdentityCredential ): Promise { @@ -430,43 +633,6 @@ export class RLNLightContract { } } - /** - * Helper method to get remaining messages in current epoch - * @param membershipId The ID of the membership to check - * @returns number of remaining messages allowed in current epoch - */ - public async getRemainingMessages(membershipId: number): Promise { - try { - const [startTime, , rateLimit] = - await this.contract.getMembershipInfo(membershipId); - - // Calculate current epoch - const currentTime = Math.floor(Date.now() / 1000); - const epochsPassed = Math.floor( - (currentTime - startTime) / RATE_LIMIT_PARAMS.EPOCH_LENGTH - ); - const currentEpochStart = - startTime + epochsPassed * RATE_LIMIT_PARAMS.EPOCH_LENGTH; - - // Get message count in current epoch using contract's function - const messageCount = await this.contract.getMessageCount( - membershipId, - currentEpochStart - ); - return Math.max( - 0, - ethers.BigNumber.from(rateLimit) - .sub(ethers.BigNumber.from(messageCount)) - .toNumber() - ); - } catch (error) { - log.error( - `Error getting remaining messages: ${(error as Error).message}` - ); - return 0; // Fail safe: assume no messages remaining on error - } - } - public async registerWithPermitAndErase( identity: IdentityCredential, permit: { @@ -538,188 +704,4 @@ export class RLNLightContract { return undefined; } } - - public async withdraw(token: string, holder: string): Promise { - try { - const tx = await this.contract.withdraw(token, { from: holder }); - await tx.wait(); - } catch (error) { - log.error(`Error in withdraw: ${(error as Error).message}`); - } - } - - public async getMembershipInfo( - idCommitment: string - ): Promise { - try { - const [startBlock, endBlock, rateLimit] = - await this.contract.getMembershipInfo(idCommitment); - const currentBlock = await this.contract.provider.getBlockNumber(); - - let state: MembershipState; - if (currentBlock < startBlock) { - state = MembershipState.Active; - } else if (currentBlock < endBlock) { - state = MembershipState.GracePeriod; - } else { - state = MembershipState.Expired; - } - - const index = await this.getMemberIndex(idCommitment); - if (!index) return undefined; - - return { - index, - idCommitment, - rateLimit: rateLimit.toNumber(), - startBlock: startBlock.toNumber(), - endBlock: endBlock.toNumber(), - state - }; - } catch (error) { - return undefined; - } - } - - public async extendMembership( - idCommitment: string - ): Promise { - return this.contract.extendMemberships([idCommitment]); - } - - public async eraseMembership( - idCommitment: string, - eraseFromMembershipSet: boolean = true - ): Promise { - return this.contract.eraseMemberships( - [idCommitment], - eraseFromMembershipSet - ); - } - - public async registerMembership( - idCommitment: string, - rateLimit: number = DEFAULT_RATE_LIMIT - ): Promise { - 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}` - ); - } - return this.contract.register(idCommitment, rateLimit, []); - } - - private async getMemberIndex( - idCommitment: string - ): Promise { - try { - const events = await this.contract.queryFilter( - this.contract.filters.MembershipRegistered(idCommitment) - ); - if (events.length === 0) return undefined; - - // Get the most recent registration event - const event = events[events.length - 1]; - return event.args?.index; - } catch (error) { - return undefined; - } - } -} - -interface CustomQueryOptions extends FetchMembersOptions { - membersFilter: ethers.EventFilter; -} - -// These values should be tested on other networks -const FETCH_CHUNK = 5; -const BLOCK_RANGE = 3000; - -async function queryFilter( - contract: ethers.Contract, - options: CustomQueryOptions -): Promise { - const { - fromBlock, - membersFilter, - fetchRange = BLOCK_RANGE, - fetchChunks = FETCH_CHUNK - } = options; - - if (fromBlock === undefined) { - return contract.queryFilter(membersFilter); - } - - if (!contract.provider) { - throw Error("No provider found on the contract."); - } - - const toBlock = await contract.provider.getBlockNumber(); - - if (toBlock - fromBlock < fetchRange) { - return contract.queryFilter(membersFilter, fromBlock, toBlock); - } - - const events: ethers.Event[][] = []; - const chunks = splitToChunks(fromBlock, toBlock, fetchRange); - - for (const portion of takeN<[number, number]>(chunks, fetchChunks)) { - const promises = portion.map(([left, right]) => - ignoreErrors(contract.queryFilter(membersFilter, left, right), []) - ); - const fetchedEvents = await Promise.all(promises); - events.push(fetchedEvents.flatMap((v) => v)); - } - - return events.flatMap((v) => v); -} - -function 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; -} - -function* takeN(array: T[], size: number): Iterable { - let start = 0; - - while (start < array.length) { - const portion = array.slice(start, start + size); - - yield portion; - - start += size; - } -} - -async function ignoreErrors( - promise: Promise, - defaultValue: T -): Promise { - 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; - } } diff --git a/packages/rln/src/contract/rln_contract.spec.ts b/packages/rln/src/contract/rln_contract.spec.ts index c05902e6e5..219b1268be 100644 --- a/packages/rln/src/contract/rln_contract.spec.ts +++ b/packages/rln/src/contract/rln_contract.spec.ts @@ -40,7 +40,7 @@ describe("RLN Contract abstraction - RLN", () => { mockedRegistryContract ); - await rlnContract.fetchMembers(rlnInstance, { + await rlnContract.fetchMembers({ fromBlock: 0, fetchRange: 1000, fetchChunks: 2 diff --git a/packages/rln/src/contract/rln_contract.ts b/packages/rln/src/contract/rln_contract.ts index 0aa0d81fb8..12b733f6e1 100644 --- a/packages/rln/src/contract/rln_contract.ts +++ b/packages/rln/src/contract/rln_contract.ts @@ -2,72 +2,19 @@ import { Logger } from "@waku/utils"; import { hexToBytes } from "@waku/utils/bytes"; import { ethers } from "ethers"; -import type { IdentityCredential } from "../identity.js"; -import type { DecryptedCredentials } from "../keystore/index.js"; import type { RLNInstance } from "../rln.js"; import { MerkleRootTracker } from "../root_tracker.js"; import { zeroPadLE } from "../utils/bytes.js"; -import { RLN_ABI } from "./abi.js"; -import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./constants.js"; +import { RLNBaseContract } from "./rln_base_contract.js"; +import { RLNContractInitOptions } from "./types.js"; const log = new Logger("waku:rln:contract"); -type Member = { - idCommitment: string; - index: ethers.BigNumber; -}; - -interface RLNContractOptions { - signer: ethers.Signer; - address: string; - rateLimit?: number; -} - -interface RLNContractInitOptions extends RLNContractOptions { - contract?: ethers.Contract; -} - -export interface MembershipRegisteredEvent { - idCommitment: string; - membershipRateLimit: ethers.BigNumber; - index: ethers.BigNumber; -} - -type FetchMembersOptions = { - fromBlock?: number; - fetchRange?: number; - fetchChunks?: number; -}; - -export interface MembershipInfo { - index: ethers.BigNumber; - idCommitment: string; - rateLimit: number; - startBlock: number; - endBlock: number; - state: MembershipState; -} - -export enum MembershipState { - Active = "Active", - GracePeriod = "GracePeriod", - Expired = "Expired", - ErasedAwaitsWithdrawal = "ErasedAwaitsWithdrawal" -} - -export class RLNContract { - public contract: ethers.Contract; +export class RLNContract extends RLNBaseContract { + private instance: RLNInstance; private merkleRootTracker: MerkleRootTracker; - private deployBlock: undefined | number; - private rateLimit: number; - - private _members: Map = new Map(); - private _membersFilter: ethers.EventFilter; - private _membershipErasedFilter: ethers.EventFilter; - private _membersExpiredFilter: ethers.EventFilter; - /** * Asynchronous initializer for RLNContract. * Allows injecting a mocked contract for testing purposes. @@ -78,9 +25,6 @@ export class RLNContract { ): Promise { const rlnContract = new RLNContract(rlnInstance, options); - await rlnContract.fetchMembers(rlnInstance); - rlnContract.subscribeToMembers(rlnInstance); - return rlnContract; } @@ -88,178 +32,15 @@ export class RLNContract { rlnInstance: RLNInstance, options: RLNContractInitOptions ) { - const { - address, - signer, - rateLimit = DEFAULT_RATE_LIMIT, - contract - } = options; + super(options); - this.validateRateLimit(rateLimit); - this.rateLimit = rateLimit; + this.instance = rlnInstance; const initialRoot = rlnInstance.zerokit.getMerkleRoot(); - - // Use the injected contract if provided; otherwise, instantiate a new one. - this.contract = contract || new ethers.Contract(address, RLN_ABI, signer); this.merkleRootTracker = new MerkleRootTracker(5, initialRoot); - - // Initialize event filters - this._membersFilter = this.contract.filters.MembershipRegistered(); - this._membershipErasedFilter = this.contract.filters.MembershipErased(); - this._membersExpiredFilter = this.contract.filters.MembershipExpired(); } - /** - * Validates that the rate limit is within the allowed range - * @throws Error if the rate limit is outside the allowed range - */ - private validateRateLimit(rateLimit: number): void { - 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} messages per epoch` - ); - } - } - - /** - * Gets the current rate limit for this contract instance - */ - public getRateLimit(): number { - return this.rateLimit; - } - - /** - * Gets the contract address - */ - public get address(): string { - return this.contract.address; - } - - /** - * Gets the contract provider - */ - public get provider(): ethers.providers.Provider { - return this.contract.provider; - } - - /** - * Gets the minimum allowed rate limit from the contract - * @returns Promise The minimum rate limit in messages per epoch - */ - public async getMinRateLimit(): Promise { - const minRate = await this.contract.minMembershipRateLimit(); - return ethers.BigNumber.from(minRate).toNumber(); - } - - /** - * Gets the maximum allowed rate limit from the contract - * @returns Promise The maximum rate limit in messages per epoch - */ - public async getMaxRateLimit(): Promise { - const maxRate = await this.contract.maxMembershipRateLimit(); - return ethers.BigNumber.from(maxRate).toNumber(); - } - - /** - * Gets the maximum total rate limit across all memberships - * @returns Promise The maximum total rate limit in messages per epoch - */ - public async getMaxTotalRateLimit(): Promise { - const maxTotalRate = await this.contract.maxTotalRateLimit(); - return maxTotalRate.toNumber(); - } - - /** - * Gets the current total rate limit usage across all memberships - * @returns Promise The current total rate limit usage in messages per epoch - */ - public async getCurrentTotalRateLimit(): Promise { - const currentTotal = await this.contract.currentTotalRateLimit(); - return currentTotal.toNumber(); - } - - /** - * Gets the remaining available total rate limit that can be allocated - * @returns Promise The remaining rate limit that can be allocated - */ - public async getRemainingTotalRateLimit(): Promise { - const [maxTotal, currentTotal] = await Promise.all([ - this.contract.maxTotalRateLimit(), - this.contract.currentTotalRateLimit() - ]); - return Number(maxTotal) - Number(currentTotal); - } - - /** - * Updates the rate limit for future registrations - * @param newRateLimit The new rate limit to use - */ - public async setRateLimit(newRateLimit: number): Promise { - this.validateRateLimit(newRateLimit); - this.rateLimit = newRateLimit; - } - - public get members(): Member[] { - const sortedMembers = Array.from(this._members.values()).sort( - (left, right) => left.index.toNumber() - right.index.toNumber() - ); - return sortedMembers; - } - - private get membersFilter(): ethers.EventFilter { - if (!this._membersFilter) { - throw Error("Members filter was not initialized."); - } - return this._membersFilter; - } - - private get membershipErasedFilter(): ethers.EventFilter { - if (!this._membershipErasedFilter) { - throw Error("MembershipErased filter was not initialized."); - } - return this._membershipErasedFilter; - } - - private get membersExpiredFilter(): ethers.EventFilter { - if (!this._membersExpiredFilter) { - throw Error("MembersExpired filter was not initialized."); - } - return this._membersExpiredFilter; - } - - public async fetchMembers( - rlnInstance: RLNInstance, - options: FetchMembersOptions = {} - ): Promise { - const registeredMemberEvents = await queryFilter(this.contract, { - fromBlock: this.deployBlock, - ...options, - membersFilter: this.membersFilter - }); - const removedMemberEvents = await queryFilter(this.contract, { - fromBlock: this.deployBlock, - ...options, - membersFilter: this.membershipErasedFilter - }); - const expiredMemberEvents = await queryFilter(this.contract, { - fromBlock: this.deployBlock, - ...options, - membersFilter: this.membersExpiredFilter - }); - - const events = [ - ...registeredMemberEvents, - ...removedMemberEvents, - ...expiredMemberEvents - ]; - this.processEvents(rlnInstance, events); - } - - public processEvents(rlnInstance: RLNInstance, events: ethers.Event[]): void { + public override processEvents(events: ethers.Event[]): void { const toRemoveTable = new Map(); const toInsertTable = new Map(); @@ -306,8 +87,8 @@ export class RLNContract { } }); - this.removeMembers(rlnInstance, toRemoveTable); - this.insertMembers(rlnInstance, toInsertTable); + this.removeMembers(this.instance, toRemoveTable); + this.insertMembers(this.instance, toInsertTable); } private insertMembers( @@ -360,439 +141,4 @@ export class RLNContract { this.merkleRootTracker.backFill(blockNumber); }); } - - public subscribeToMembers(rlnInstance: RLNInstance): void { - this.contract.on( - this.membersFilter, - ( - _idCommitment: string, - _membershipRateLimit: ethers.BigNumber, - _index: ethers.BigNumber, - event: ethers.Event - ) => { - this.processEvents(rlnInstance, [event]); - } - ); - - this.contract.on( - this.membershipErasedFilter, - ( - _idCommitment: string, - _membershipRateLimit: ethers.BigNumber, - _index: ethers.BigNumber, - event: ethers.Event - ) => { - this.processEvents(rlnInstance, [event]); - } - ); - - this.contract.on( - this.membersExpiredFilter, - ( - _idCommitment: string, - _membershipRateLimit: ethers.BigNumber, - _index: ethers.BigNumber, - event: ethers.Event - ) => { - this.processEvents(rlnInstance, [event]); - } - ); - } - - public async registerWithIdentity( - identity: IdentityCredential - ): Promise { - try { - log.info( - `Registering identity with rate limit: ${this.rateLimit} messages/epoch` - ); - - // Check if the ID commitment is already registered - const existingIndex = await this.getMemberIndex( - identity.IDCommitmentBigInt.toString() - ); - if (existingIndex) { - throw new Error( - `ID commitment is already registered with index ${existingIndex}` - ); - } - - // Check if there's enough remaining rate limit - const remainingRateLimit = await this.getRemainingTotalRateLimit(); - if (remainingRateLimit < this.rateLimit) { - throw new Error( - `Not enough remaining rate limit. Requested: ${this.rateLimit}, Available: ${remainingRateLimit}` - ); - } - - const estimatedGas = await this.contract.estimateGas.register( - identity.IDCommitmentBigInt, - this.rateLimit, - [] - ); - const gasLimit = estimatedGas.add(10000); - - const txRegisterResponse: ethers.ContractTransaction = - await this.contract.register( - identity.IDCommitmentBigInt, - this.rateLimit, - [], - { gasLimit } - ); - - const txRegisterReceipt = await txRegisterResponse.wait(); - - if (txRegisterReceipt.status === 0) { - throw new Error("Transaction failed on-chain"); - } - - const memberRegistered = txRegisterReceipt.events?.find( - (event) => event.event === "MembershipRegistered" - ); - - if (!memberRegistered || !memberRegistered.args) { - log.error( - "Failed to register membership: No MembershipRegistered event found" - ); - return undefined; - } - - const decodedData: MembershipRegisteredEvent = { - idCommitment: memberRegistered.args.idCommitment, - membershipRateLimit: memberRegistered.args.membershipRateLimit, - index: memberRegistered.args.index - }; - - log.info( - `Successfully registered membership with index ${decodedData.index} ` + - `and rate limit ${decodedData.membershipRateLimit}` - ); - - const network = await this.contract.provider.getNetwork(); - const address = this.contract.address; - const membershipId = Number(decodedData.index); - - return { - identity, - membership: { - address, - treeIndex: membershipId, - chainId: network.chainId, - rateLimit: decodedData.membershipRateLimit.toNumber() - } - }; - } catch (error) { - if (error instanceof Error) { - const errorMessage = error.message; - log.error("registerWithIdentity - error message:", errorMessage); - log.error("registerWithIdentity - error stack:", error.stack); - - // Try to extract more specific error information - if (errorMessage.includes("CannotExceedMaxTotalRateLimit")) { - throw new Error( - "Registration failed: Cannot exceed maximum total rate limit" - ); - } else if (errorMessage.includes("InvalidIdCommitment")) { - throw new Error("Registration failed: Invalid ID commitment"); - } else if (errorMessage.includes("InvalidMembershipRateLimit")) { - throw new Error("Registration failed: Invalid membership rate limit"); - } else if (errorMessage.includes("execution reverted")) { - throw new Error( - "Contract execution reverted. Check contract requirements." - ); - } else { - throw new Error(`Error in registerWithIdentity: ${errorMessage}`); - } - } else { - throw new Error("Unknown error in registerWithIdentity", { - cause: error - }); - } - } - } - - /** - * Helper method to get remaining messages in current epoch - * @param membershipId The ID of the membership to check - * @returns number of remaining messages allowed in current epoch - */ - public async getRemainingMessages(membershipId: number): Promise { - try { - const [startTime, , rateLimit] = - await this.contract.getMembershipInfo(membershipId); - - // Calculate current epoch - const currentTime = Math.floor(Date.now() / 1000); - const epochsPassed = Math.floor( - (currentTime - startTime) / RATE_LIMIT_PARAMS.EPOCH_LENGTH - ); - const currentEpochStart = - startTime + epochsPassed * RATE_LIMIT_PARAMS.EPOCH_LENGTH; - - // Get message count in current epoch using contract's function - const messageCount = await this.contract.getMessageCount( - membershipId, - currentEpochStart - ); - return Math.max(0, rateLimit.sub(messageCount).toNumber()); - } catch (error) { - log.error( - `Error getting remaining messages: ${(error as Error).message}` - ); - return 0; // Fail safe: assume no messages remaining on error - } - } - - public async registerWithPermitAndErase( - identity: IdentityCredential, - permit: { - owner: string; - deadline: number; - v: number; - r: string; - s: string; - }, - idCommitmentsToErase: string[] - ): Promise { - try { - log.info( - `Registering identity with permit and rate limit: ${this.rateLimit} messages/epoch` - ); - - const txRegisterResponse: ethers.ContractTransaction = - await this.contract.registerWithPermit( - permit.owner, - permit.deadline, - permit.v, - permit.r, - permit.s, - identity.IDCommitmentBigInt, - this.rateLimit, - idCommitmentsToErase.map((id) => ethers.BigNumber.from(id)) - ); - const txRegisterReceipt = await txRegisterResponse.wait(); - - const memberRegistered = txRegisterReceipt.events?.find( - (event) => event.event === "MembershipRegistered" - ); - - if (!memberRegistered || !memberRegistered.args) { - log.error( - "Failed to register membership with permit: No MembershipRegistered event found" - ); - return undefined; - } - - const decodedData: MembershipRegisteredEvent = { - idCommitment: memberRegistered.args.idCommitment, - membershipRateLimit: memberRegistered.args.membershipRateLimit, - index: memberRegistered.args.index - }; - - log.info( - `Successfully registered membership with permit. Index: ${decodedData.index}, ` + - `Rate limit: ${decodedData.membershipRateLimit}, Erased ${idCommitmentsToErase.length} commitments` - ); - - const network = await this.contract.provider.getNetwork(); - const address = this.contract.address; - const membershipId = ethers.BigNumber.from(decodedData.index).toNumber(); - - return { - identity, - membership: { - address, - treeIndex: membershipId, - chainId: network.chainId, - rateLimit: decodedData.membershipRateLimit.toNumber() - } - }; - } catch (error) { - log.error( - `Error in registerWithPermitAndErase: ${(error as Error).message}` - ); - return undefined; - } - } - - public roots(): Uint8Array[] { - return this.merkleRootTracker.roots(); - } - - public async withdraw(token: string, holder: string): Promise { - try { - const tx = await this.contract.withdraw(token, { from: holder }); - await tx.wait(); - } catch (error) { - log.error(`Error in withdraw: ${(error as Error).message}`); - } - } - - public async getMembershipInfo( - idCommitment: string - ): Promise { - try { - const [startBlock, endBlock, rateLimit] = - await this.contract.getMembershipInfo(idCommitment); - const currentBlock = await this.contract.provider.getBlockNumber(); - - let state: MembershipState; - if (currentBlock < startBlock) { - state = MembershipState.Active; - } else if (currentBlock < endBlock) { - state = MembershipState.GracePeriod; - } else { - state = MembershipState.Expired; - } - - const index = await this.getMemberIndex(idCommitment); - if (!index) return undefined; - - return { - index, - idCommitment, - rateLimit: rateLimit.toNumber(), - startBlock: startBlock.toNumber(), - endBlock: endBlock.toNumber(), - state - }; - } catch (error) { - return undefined; - } - } - - public async extendMembership( - idCommitment: string - ): Promise { - return this.contract.extendMemberships([idCommitment]); - } - - public async eraseMembership( - idCommitment: string, - eraseFromMembershipSet: boolean = true - ): Promise { - return this.contract.eraseMemberships( - [idCommitment], - eraseFromMembershipSet - ); - } - - public async registerMembership( - idCommitment: string, - rateLimit: number = this.rateLimit - ): Promise { - this.validateRateLimit(rateLimit); - return this.contract.register(idCommitment, rateLimit, []); - } - - private async getMemberIndex( - idCommitment: string - ): Promise { - try { - const events = await this.contract.queryFilter( - this.contract.filters.MembershipRegistered(idCommitment) - ); - if (events.length === 0) return undefined; - - // Get the most recent registration event - const event = events[events.length - 1]; - return event.args?.index; - } catch (error) { - return undefined; - } - } -} - -interface CustomQueryOptions extends FetchMembersOptions { - membersFilter: ethers.EventFilter; -} - -// These values should be tested on other networks -const FETCH_CHUNK = 5; -const BLOCK_RANGE = 3000; - -async function queryFilter( - contract: ethers.Contract, - options: CustomQueryOptions -): Promise { - const { - fromBlock, - membersFilter, - fetchRange = BLOCK_RANGE, - fetchChunks = FETCH_CHUNK - } = options; - - if (fromBlock === undefined) { - return contract.queryFilter(membersFilter); - } - - if (!contract.provider) { - throw Error("No provider found on the contract."); - } - - const toBlock = await contract.provider.getBlockNumber(); - - if (toBlock - fromBlock < fetchRange) { - return contract.queryFilter(membersFilter, fromBlock, toBlock); - } - - const events: ethers.Event[][] = []; - const chunks = splitToChunks(fromBlock, toBlock, fetchRange); - - for (const portion of takeN<[number, number]>(chunks, fetchChunks)) { - const promises = portion.map(([left, right]) => - ignoreErrors(contract.queryFilter(membersFilter, left, right), []) - ); - const fetchedEvents = await Promise.all(promises); - events.push(fetchedEvents.flatMap((v) => v)); - } - - return events.flatMap((v) => v); -} - -function 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; -} - -function* takeN(array: T[], size: number): Iterable { - let start = 0; - - while (start < array.length) { - const portion = array.slice(start, start + size); - - yield portion; - - start += size; - } -} - -async function ignoreErrors( - promise: Promise, - defaultValue: T -): Promise { - 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; - } } diff --git a/packages/rln/src/contract/types.ts b/packages/rln/src/contract/types.ts new file mode 100644 index 0000000000..833e2a5c1f --- /dev/null +++ b/packages/rln/src/contract/types.ts @@ -0,0 +1,48 @@ +import { ethers } from "ethers"; + +export interface CustomQueryOptions extends FetchMembersOptions { + membersFilter: ethers.EventFilter; +} + +export type Member = { + idCommitment: string; + index: ethers.BigNumber; +}; + +export interface RLNContractOptions { + signer: ethers.Signer; + address: string; + rateLimit?: number; +} + +export interface RLNContractInitOptions extends RLNContractOptions { + contract?: ethers.Contract; +} + +export interface MembershipRegisteredEvent { + idCommitment: string; + membershipRateLimit: ethers.BigNumber; + index: ethers.BigNumber; +} + +export type FetchMembersOptions = { + fromBlock?: number; + fetchRange?: number; + fetchChunks?: number; +}; + +export interface MembershipInfo { + index: ethers.BigNumber; + idCommitment: string; + rateLimit: number; + startBlock: number; + endBlock: number; + state: MembershipState; +} + +export enum MembershipState { + Active = "Active", + GracePeriod = "GracePeriod", + Expired = "Expired", + ErasedAwaitsWithdrawal = "ErasedAwaitsWithdrawal" +} diff --git a/packages/rln/src/credentials_manager.ts b/packages/rln/src/credentials_manager.ts index c0100e7486..a4e7681c55 100644 --- a/packages/rln/src/credentials_manager.ts +++ b/packages/rln/src/credentials_manager.ts @@ -4,7 +4,7 @@ import { Logger } from "@waku/utils"; import { ethers } from "ethers"; import { LINEA_CONTRACT } from "./contract/constants.js"; -import { RLNLightContract } from "./contract/rln_light_contract.js"; +import { RLNBaseContract } from "./contract/rln_base_contract.js"; import { IdentityCredential } from "./identity.js"; import { Keystore } from "./keystore/index.js"; import type { @@ -29,7 +29,7 @@ export class RLNCredentialsManager { private started = false; private starting = false; - private _contract: undefined | RLNLightContract; + private _contract: undefined | RLNBaseContract; private _signer: undefined | ethers.Signer; private keystore = Keystore.create(); @@ -39,7 +39,7 @@ export class RLNCredentialsManager { log.info("RLNCredentialsManager initialized"); } - public get contract(): undefined | RLNLightContract { + public get contract(): undefined | RLNBaseContract { return this._contract; } @@ -80,7 +80,7 @@ export class RLNCredentialsManager { this._credentials = credentials; this._signer = signer!; - this._contract = await RLNLightContract.init({ + this._contract = new RLNBaseContract({ address: address!, signer: signer!, rateLimit: rateLimit diff --git a/packages/rln/src/index.ts b/packages/rln/src/index.ts index 0d7e825e3d..73791f7938 100644 --- a/packages/rln/src/index.ts +++ b/packages/rln/src/index.ts @@ -1,7 +1,7 @@ import { RLNDecoder, RLNEncoder } from "./codec.js"; import { RLN_ABI } from "./contract/abi.js"; import { LINEA_CONTRACT, RLNContract } from "./contract/index.js"; -import { RLNLightContract } from "./contract/rln_light_contract.js"; +import { RLNBaseContract } from "./contract/rln_base_contract.js"; import { createRLN } from "./create.js"; import { RLNCredentialsManager } from "./credentials_manager.js"; import { IdentityCredential } from "./identity.js"; @@ -13,7 +13,7 @@ import { extractMetaMaskSigner } from "./utils/index.js"; export { RLNCredentialsManager, - RLNLightContract, + RLNBaseContract, createRLN, Keystore, RLNInstance, From 7aaa84a68c0f5ad3dcd308ff91aaa1c3b57827d9 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Thu, 3 Apr 2025 19:51:55 +0530 Subject: [PATCH 3/4] chore: use CredentialsManager for rln.ts --- packages/rln/src/create.ts | 2 +- packages/rln/src/credentials_manager.ts | 79 +++++- packages/rln/src/rln.ts | 312 ++++++------------------ 3 files changed, 140 insertions(+), 253 deletions(-) diff --git a/packages/rln/src/create.ts b/packages/rln/src/create.ts index 3b32302832..24d8b660d3 100644 --- a/packages/rln/src/create.ts +++ b/packages/rln/src/create.ts @@ -5,5 +5,5 @@ export async function createRLN(): Promise { // asynchronously. This file does the single async import, so // that no one else needs to worry about it again. const rlnModule = await import("./rln.js"); - return rlnModule.create(); + return rlnModule.RLNInstance.create(); } diff --git a/packages/rln/src/credentials_manager.ts b/packages/rln/src/credentials_manager.ts index a4e7681c55..86e78c3af6 100644 --- a/packages/rln/src/credentials_manager.ts +++ b/packages/rln/src/credentials_manager.ts @@ -17,6 +17,7 @@ import { buildBigIntFromUint8Array, extractMetaMaskSigner } from "./utils/index.js"; +import { Zerokit } from "./zerokit.js"; const log = new Logger("waku:credentials"); @@ -26,27 +27,50 @@ const log = new Logger("waku:credentials"); * It is used to register membership and generate identity credentials */ export class RLNCredentialsManager { - private started = false; - private starting = false; + protected started = false; + protected starting = false; private _contract: undefined | RLNBaseContract; private _signer: undefined | ethers.Signer; - private keystore = Keystore.create(); + protected keystore = Keystore.create(); private _credentials: undefined | DecryptedCredentials; - public constructor() { + public zerokit: undefined | Zerokit; + + public constructor(zerokit?: Zerokit) { log.info("RLNCredentialsManager initialized"); + this.zerokit = zerokit; } public get contract(): undefined | RLNBaseContract { return this._contract; } + public set contract(contract: RLNBaseContract | undefined) { + this._contract = contract; + } + public get signer(): undefined | ethers.Signer { return this._signer; } + public set signer(signer: ethers.Signer | undefined) { + this._signer = signer; + } + + public get credentials(): undefined | DecryptedCredentials { + return this._credentials; + } + + public set credentials(credentials: DecryptedCredentials | undefined) { + this._credentials = credentials; + } + + public get provider(): undefined | ethers.providers.Provider { + return this.contract?.provider; + } + public async start(options: StartRLNOptions = {}): Promise { if (this.started || this.starting) { log.info("RLNCredentialsManager already started or starting"); @@ -83,7 +107,7 @@ export class RLNCredentialsManager { this._contract = new RLNBaseContract({ address: address!, signer: signer!, - rateLimit: rateLimit + rateLimit: rateLimit ?? this.zerokit?.getRateLimit }); log.info("RLNCredentialsManager successfully started"); @@ -96,11 +120,7 @@ export class RLNCredentialsManager { } } - public get credentials(): DecryptedCredentials | undefined { - return this._credentials; - } - - private async determineStartOptions( + protected async determineStartOptions( options: StartRLNOptions, credentials: KeystoreEntity | undefined ): Promise { @@ -134,7 +154,7 @@ export class RLNCredentialsManager { }; } - private static async decryptCredentialsIfNeeded( + protected static async decryptCredentialsIfNeeded( credentials?: EncryptedCredentials | DecryptedCredentials ): Promise<{ credentials?: DecryptedCredentials; keystore?: Keystore }> { if (!credentials) { @@ -222,7 +242,15 @@ export class RLNCredentialsManager { if ("signature" in options) { log.info("Generating identity from signature"); - identity = this.generateSeededIdentityCredential(options.signature); + if (this.zerokit) { + log.info("Using Zerokit to generate identity"); + identity = this.zerokit.generateSeededIdentityCredential( + options.signature + ); + } else { + log.info("Using local implementation to generate identity"); + identity = this.generateSeededIdentityCredential(options.signature); + } } if (!identity) { @@ -248,4 +276,31 @@ export class RLNCredentialsManager { log.warn("Failed to load credentials"); } } + + protected async verifyCredentialsAgainstContract( + credentials: KeystoreEntity + ): Promise { + if (!this.contract) { + throw Error( + "Failed to verify chain coordinates: no contract initialized." + ); + } + + const registryAddress = credentials.membership.address; + const currentRegistryAddress = this.contract.address; + if (registryAddress !== currentRegistryAddress) { + throw Error( + `Failed to verify chain coordinates: credentials contract address=${registryAddress} is not equal to registryContract address=${currentRegistryAddress}` + ); + } + + const chainId = credentials.membership.chainId; + const network = await this.contract.provider.getNetwork(); + const currentChainId = network.chainId; + if (chainId !== currentChainId) { + throw Error( + `Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}` + ); + } + } } diff --git a/packages/rln/src/rln.ts b/packages/rln/src/rln.ts index 54013c7f47..98878756aa 100644 --- a/packages/rln/src/rln.ts +++ b/packages/rln/src/rln.ts @@ -7,7 +7,6 @@ import type { import { Logger } from "@waku/utils"; import init from "@waku/zerokit-rln-wasm"; import * as zerokitRLN from "@waku/zerokit-rln-wasm"; -import { ethers } from "ethers"; import { createRLNDecoder, @@ -16,234 +15,51 @@ import { type RLNEncoder } from "./codec.js"; import { DEFAULT_RATE_LIMIT } from "./contract/constants.js"; -import { LINEA_CONTRACT, RLNContract } from "./contract/index.js"; -import { Keystore } from "./keystore/index.js"; +import { RLNCredentialsManager } from "./credentials_manager.js"; import type { DecryptedCredentials, EncryptedCredentials } from "./keystore/index.js"; -import { KeystoreEntity, Password } from "./keystore/types.js"; import verificationKey from "./resources/verification_key"; import * as wc from "./resources/witness_calculator"; import { WitnessCalculator } from "./resources/witness_calculator"; -import { RegisterMembershipOptions, StartRLNOptions } from "./types.js"; -import { extractMetaMaskSigner } from "./utils/index.js"; import { Zerokit } from "./zerokit.js"; const log = new Logger("waku:rln"); -async function loadWitnessCalculator(): Promise { - try { - const url = new URL("./resources/rln.wasm", import.meta.url); - const response = await fetch(url); - - if (!response.ok) { - throw new Error( - `Failed to fetch witness calculator: ${response.status} ${response.statusText}` - ); - } - - return await wc.builder( - new Uint8Array(await response.arrayBuffer()), - false - ); - } catch (error) { - log.error("Error loading witness calculator:", error); - throw new Error( - `Failed to load witness calculator: ${error instanceof Error ? error.message : String(error)}` - ); - } -} - -async function loadZkey(): Promise { - try { - const url = new URL("./resources/rln_final.zkey", import.meta.url); - const response = await fetch(url); - - if (!response.ok) { - throw new Error( - `Failed to fetch zkey: ${response.status} ${response.statusText}` - ); - } - - return new Uint8Array(await response.arrayBuffer()); - } catch (error) { - log.error("Error loading zkey:", error); - throw new Error( - `Failed to load zkey: ${error instanceof Error ? error.message : String(error)}` - ); - } -} - -/** - * Create an instance of RLN - * @returns RLNInstance - */ -export async function create(): Promise { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (init as any)?.(); - zerokitRLN.init_panic_hook(); - - const witnessCalculator = await loadWitnessCalculator(); - const zkey = await loadZkey(); - - const stringEncoder = new TextEncoder(); - const vkey = stringEncoder.encode(JSON.stringify(verificationKey)); - - const DEPTH = 20; - const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey); - const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT); - - return new RLNInstance(zerokit); - } catch (error) { - log.error("Failed to initialize RLN:", error); - throw error; - } -} - type WakuRLNEncoderOptions = WakuEncoderOptions & { credentials: EncryptedCredentials | DecryptedCredentials; }; -export class RLNInstance { - private started = false; - private starting = false; - - private _contract: undefined | RLNContract; - private _signer: undefined | ethers.Signer; - - private keystore = Keystore.create(); - private _credentials: undefined | DecryptedCredentials; - - public constructor(public zerokit: Zerokit) {} - - public get contract(): undefined | RLNContract { - return this._contract; - } - - public get signer(): undefined | ethers.Signer { - return this._signer; - } - - public async start(options: StartRLNOptions = {}): Promise { - if (this.started || this.starting) { - return; - } - - this.starting = true; - - try { - const { credentials, keystore } = - await RLNInstance.decryptCredentialsIfNeeded(options.credentials); - const { signer, address, rateLimit } = await this.determineStartOptions( - options, - credentials - ); - - if (keystore) { - this.keystore = keystore; - } - - this._credentials = credentials; - this._signer = signer!; - this._contract = await RLNContract.init(this, { - address: address!, - signer: signer!, - rateLimit: rateLimit ?? this.zerokit.getRateLimit - }); - this.started = true; - } finally { - this.starting = false; - } - } - - private async determineStartOptions( - options: StartRLNOptions, - credentials: KeystoreEntity | undefined - ): Promise { - let chainId = credentials?.membership.chainId; - const address = - credentials?.membership.address || - options.address || - LINEA_CONTRACT.address; - - if (address === LINEA_CONTRACT.address) { - chainId = LINEA_CONTRACT.chainId; - } - - const signer = options.signer || (await extractMetaMaskSigner()); - const currentChainId = await signer.getChainId(); - - if (chainId && chainId !== currentChainId) { - throw Error( - `Failed to start RLN contract, chain ID of contract is different from current one: contract-${chainId}, current network-${currentChainId}` - ); - } - - return { - signer, - address - }; - } - - private static async decryptCredentialsIfNeeded( - credentials?: EncryptedCredentials | DecryptedCredentials - ): Promise<{ credentials?: DecryptedCredentials; keystore?: Keystore }> { - if (!credentials) { - return {}; - } - - if ("identity" in credentials) { - return { credentials }; - } - - const keystore = Keystore.fromString(credentials.keystore); - - if (!keystore) { - return {}; - } - - const decryptedCredentials = await keystore.readCredential( - credentials.id, - credentials.password - ); - - return { - keystore, - credentials: decryptedCredentials - }; - } - - public async registerMembership( - options: RegisterMembershipOptions - ): Promise { - if (!this.contract) { - throw Error("RLN Contract is not initialized."); - } - - let identity = "identity" in options && options.identity; - - if ("signature" in options) { - identity = this.zerokit.generateSeededIdentityCredential( - options.signature - ); - } - - if (!identity) { - throw Error("Missing signature or identity to register membership."); - } - - return this.contract.registerWithIdentity(identity); - } - +export class RLNInstance extends RLNCredentialsManager { /** - * Changes credentials in use by relying on provided Keystore earlier in rln.start - * @param id: string, hash of credentials to select from Keystore - * @param password: string or bytes to use to decrypt credentials from Keystore + * Create an instance of RLN + * @returns RLNInstance */ - public async useCredentials(id: string, password: Password): Promise { - this._credentials = await this.keystore?.readCredential(id, password); + public static async create(): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (init as any)?.(); + zerokitRLN.init_panic_hook(); + + const witnessCalculator = await RLNInstance.loadWitnessCalculator(); + const zkey = await RLNInstance.loadZkey(); + + const stringEncoder = new TextEncoder(); + const vkey = stringEncoder.encode(JSON.stringify(verificationKey)); + + const DEPTH = 20; + const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey); + const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT); + + return new RLNInstance(zerokit); + } catch (error) { + log.error("Failed to initialize RLN:", error); + throw error; + } + } + private constructor(public zerokit: Zerokit) { + super(zerokit); } public async createEncoder( @@ -251,7 +67,7 @@ export class RLNInstance { ): Promise { const { credentials: decryptedCredentials } = await RLNInstance.decryptCredentialsIfNeeded(options.credentials); - const credentials = decryptedCredentials || this._credentials; + const credentials = decryptedCredentials || this.credentials; if (!credentials) { throw Error( @@ -269,33 +85,6 @@ export class RLNInstance { }); } - private async verifyCredentialsAgainstContract( - credentials: KeystoreEntity - ): Promise { - if (!this._contract) { - throw Error( - "Failed to verify chain coordinates: no contract initialized." - ); - } - - const registryAddress = credentials.membership.address; - const currentRegistryAddress = this._contract.address; - if (registryAddress !== currentRegistryAddress) { - throw Error( - `Failed to verify chain coordinates: credentials contract address=${registryAddress} is not equal to registryContract address=${currentRegistryAddress}` - ); - } - - const chainId = credentials.membership.chainId; - const network = await this._contract.provider.getNetwork(); - const currentChainId = network.chainId; - if (chainId !== currentChainId) { - throw Error( - `Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}` - ); - } - } - public createDecoder( contentTopic: ContentTopic ): RLNDecoder { @@ -304,4 +93,47 @@ export class RLNInstance { decoder: createDecoder(contentTopic) }); } + + public static async loadWitnessCalculator(): Promise { + try { + const url = new URL("./resources/rln.wasm", import.meta.url); + const response = await fetch(url); + + if (!response.ok) { + throw new Error( + `Failed to fetch witness calculator: ${response.status} ${response.statusText}` + ); + } + + return await wc.builder( + new Uint8Array(await response.arrayBuffer()), + false + ); + } catch (error) { + log.error("Error loading witness calculator:", error); + throw new Error( + `Failed to load witness calculator: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + public static async loadZkey(): Promise { + try { + const url = new URL("./resources/rln_final.zkey", import.meta.url); + const response = await fetch(url); + + if (!response.ok) { + throw new Error( + `Failed to fetch zkey: ${response.status} ${response.statusText}` + ); + } + + return new Uint8Array(await response.arrayBuffer()); + } catch (error) { + log.error("Error loading zkey:", error); + throw new Error( + `Failed to load zkey: ${error instanceof Error ? error.message : String(error)}` + ); + } + } } From 6997987a60a52f1382230ff7b64b25606610534f Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Thu, 3 Apr 2025 19:56:11 +0530 Subject: [PATCH 4/4] chore: public methods written above private methods --- .../rln/src/contract/rln_base_contract.ts | 106 ++++++------- packages/rln/src/credentials_manager.ts | 150 +++++++++--------- 2 files changed, 128 insertions(+), 128 deletions(-) diff --git a/packages/rln/src/contract/rln_base_contract.ts b/packages/rln/src/contract/rln_base_contract.ts index 9393476709..2c46810fbe 100644 --- a/packages/rln/src/contract/rln_base_contract.ts +++ b/packages/rln/src/contract/rln_base_contract.ts @@ -60,21 +60,6 @@ export class RLNBaseContract { this._membersExpiredFilter = this.contract.filters.MembershipExpired(); } - /** - * Validates that the rate limit is within the allowed range - * @throws Error if the rate limit is outside the allowed range - */ - private validateRateLimit(rateLimit: number): void { - 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} messages per epoch` - ); - } - } - /** * Gets the current rate limit for this contract instance */ @@ -160,27 +145,6 @@ export class RLNBaseContract { return sortedMembers; } - private get membersFilter(): ethers.EventFilter { - if (!this._membersFilter) { - throw Error("Members filter was not initialized."); - } - return this._membersFilter; - } - - private get membershipErasedFilter(): ethers.EventFilter { - if (!this._membershipErasedFilter) { - throw Error("MembershipErased filter was not initialized."); - } - return this._membershipErasedFilter; - } - - private get membersExpiredFilter(): ethers.EventFilter { - if (!this._membersExpiredFilter) { - throw Error("MembersExpired filter was not initialized."); - } - return this._membersExpiredFilter; - } - public async fetchMembers(options: FetchMembersOptions = {}): Promise { const registeredMemberEvents = await RLNBaseContract.queryFilter( this.contract, @@ -480,23 +444,6 @@ export class RLNBaseContract { ); } - private async getMemberIndex( - idCommitment: string - ): Promise { - try { - const events = await this.contract.queryFilter( - this.contract.filters.MembershipRegistered(idCommitment) - ); - if (events.length === 0) return undefined; - - // Get the most recent registration event - const event = events[events.length - 1]; - return event.args?.index; - } catch (error) { - return undefined; - } - } - public async registerMembership( idCommitment: string, rateLimit: number = DEFAULT_RATE_LIMIT @@ -704,4 +651,57 @@ export class RLNBaseContract { return undefined; } } + + /** + * Validates that the rate limit is within the allowed range + * @throws Error if the rate limit is outside the allowed range + */ + private validateRateLimit(rateLimit: number): void { + 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} messages per epoch` + ); + } + } + + private get membersFilter(): ethers.EventFilter { + if (!this._membersFilter) { + throw Error("Members filter was not initialized."); + } + return this._membersFilter; + } + + private get membershipErasedFilter(): ethers.EventFilter { + if (!this._membershipErasedFilter) { + throw Error("MembershipErased filter was not initialized."); + } + return this._membershipErasedFilter; + } + + private get membersExpiredFilter(): ethers.EventFilter { + if (!this._membersExpiredFilter) { + throw Error("MembersExpired filter was not initialized."); + } + return this._membersExpiredFilter; + } + + private async getMemberIndex( + idCommitment: string + ): Promise { + try { + const events = await this.contract.queryFilter( + this.contract.filters.MembershipRegistered(idCommitment) + ); + if (events.length === 0) return undefined; + + // Get the most recent registration event + const event = events[events.length - 1]; + return event.args?.index; + } catch (error) { + return undefined; + } + } } diff --git a/packages/rln/src/credentials_manager.ts b/packages/rln/src/credentials_manager.ts index 86e78c3af6..1cf0495d59 100644 --- a/packages/rln/src/credentials_manager.ts +++ b/packages/rln/src/credentials_manager.ts @@ -120,6 +120,54 @@ export class RLNCredentialsManager { } } + public async registerMembership( + options: RegisterMembershipOptions + ): Promise { + if (!this.contract) { + log.error("RLN Contract is not initialized"); + throw Error("RLN Contract is not initialized."); + } + + log.info("Registering membership"); + let identity = "identity" in options && options.identity; + + if ("signature" in options) { + log.info("Generating identity from signature"); + if (this.zerokit) { + log.info("Using Zerokit to generate identity"); + identity = this.zerokit.generateSeededIdentityCredential( + options.signature + ); + } else { + log.info("Using local implementation to generate identity"); + identity = this.generateSeededIdentityCredential(options.signature); + } + } + + if (!identity) { + log.error("Missing signature or identity to register membership"); + throw Error("Missing signature or identity to register membership."); + } + + log.info("Registering identity with contract"); + return this.contract.registerWithIdentity(identity); + } + + /** + * Changes credentials in use by relying on provided Keystore earlier in rln.start + * @param id: string, hash of credentials to select from Keystore + * @param password: string or bytes to use to decrypt credentials from Keystore + */ + public async useCredentials(id: string, password: Password): Promise { + log.info(`Attempting to use credentials with ID: ${id}`); + this._credentials = await this.keystore?.readCredential(id, password); + if (this._credentials) { + log.info("Successfully loaded credentials"); + } else { + log.warn("Failed to load credentials"); + } + } + protected async determineStartOptions( options: StartRLNOptions, credentials: KeystoreEntity | undefined @@ -192,6 +240,33 @@ export class RLNCredentialsManager { } } + protected async verifyCredentialsAgainstContract( + credentials: KeystoreEntity + ): Promise { + if (!this.contract) { + throw Error( + "Failed to verify chain coordinates: no contract initialized." + ); + } + + const registryAddress = credentials.membership.address; + const currentRegistryAddress = this.contract.address; + if (registryAddress !== currentRegistryAddress) { + throw Error( + `Failed to verify chain coordinates: credentials contract address=${registryAddress} is not equal to registryContract address=${currentRegistryAddress}` + ); + } + + const chainId = credentials.membership.chainId; + const network = await this.contract.provider.getNetwork(); + const currentChainId = network.chainId; + if (chainId !== currentChainId) { + throw Error( + `Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}` + ); + } + } + /** * Generates an identity credential from a seed string * This is a pure implementation that doesn't rely on Zerokit @@ -228,79 +303,4 @@ export class RLNCredentialsManager { idCommitmentBigInt ); } - - public async registerMembership( - options: RegisterMembershipOptions - ): Promise { - if (!this.contract) { - log.error("RLN Contract is not initialized"); - throw Error("RLN Contract is not initialized."); - } - - log.info("Registering membership"); - let identity = "identity" in options && options.identity; - - if ("signature" in options) { - log.info("Generating identity from signature"); - if (this.zerokit) { - log.info("Using Zerokit to generate identity"); - identity = this.zerokit.generateSeededIdentityCredential( - options.signature - ); - } else { - log.info("Using local implementation to generate identity"); - identity = this.generateSeededIdentityCredential(options.signature); - } - } - - if (!identity) { - log.error("Missing signature or identity to register membership"); - throw Error("Missing signature or identity to register membership."); - } - - log.info("Registering identity with contract"); - return this.contract.registerWithIdentity(identity); - } - - /** - * Changes credentials in use by relying on provided Keystore earlier in rln.start - * @param id: string, hash of credentials to select from Keystore - * @param password: string or bytes to use to decrypt credentials from Keystore - */ - public async useCredentials(id: string, password: Password): Promise { - log.info(`Attempting to use credentials with ID: ${id}`); - this._credentials = await this.keystore?.readCredential(id, password); - if (this._credentials) { - log.info("Successfully loaded credentials"); - } else { - log.warn("Failed to load credentials"); - } - } - - protected async verifyCredentialsAgainstContract( - credentials: KeystoreEntity - ): Promise { - if (!this.contract) { - throw Error( - "Failed to verify chain coordinates: no contract initialized." - ); - } - - const registryAddress = credentials.membership.address; - const currentRegistryAddress = this.contract.address; - if (registryAddress !== currentRegistryAddress) { - throw Error( - `Failed to verify chain coordinates: credentials contract address=${registryAddress} is not equal to registryContract address=${currentRegistryAddress}` - ); - } - - const chainId = credentials.membership.chainId; - const network = await this.contract.provider.getNetwork(); - const currentChainId = network.chainId; - if (chainId !== currentChainId) { - throw Error( - `Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}` - ); - } - } }