From 94a1eb4dd608fe6780c70eb8635cf23395ff7b5e Mon Sep 17 00:00:00 2001 From: Arseniy Klempner Date: Thu, 18 Dec 2025 17:53:44 -0800 Subject: [PATCH] feat: store and update merkle proof/index in credentials manager --- packages/rln/src/codec.ts | 53 ++++------- packages/rln/src/contract/index.ts | 1 + .../rln/src/contract/rln_base_contract.ts | 43 +++++++++ packages/rln/src/credentials_manager.ts | 93 ++++++++++++++++++- packages/rln/src/encoder.node.spec.ts | 26 +++++- packages/rln/src/utils/index.ts | 6 ++ 6 files changed, 185 insertions(+), 37 deletions(-) diff --git a/packages/rln/src/codec.ts b/packages/rln/src/codec.ts index 3bfdbc69f4..824736c6e1 100644 --- a/packages/rln/src/codec.ts +++ b/packages/rln/src/codec.ts @@ -7,7 +7,7 @@ import type { } from "@waku/interfaces"; import { Logger } from "@waku/utils"; -import type { IdentityCredential } from "./identity.js"; +import { RLNCredentialsManager } from "./credentials_manager.js"; import { Proof } from "./proof.js"; import { RLNInstance } from "./rln.js"; import { BytesUtils } from "./utils/bytes.js"; @@ -16,18 +16,12 @@ import { dateToNanosecondBytes } from "./utils/epoch.js"; const log = new Logger("waku:rln:encoder"); export class RLNEncoder implements IEncoder { - private readonly idSecretHash: Uint8Array; - public constructor( private readonly encoder: IEncoder, private readonly rlnInstance: RLNInstance, private readonly rateLimit: number, - public pathElements: Uint8Array[], - public identityPathIndex: Uint8Array[], - identityCredential: IdentityCredential - ) { - this.idSecretHash = identityCredential.IDSecretHash; - } + private readonly credentialsManager: RLNCredentialsManager + ) {} private toRlnSignal(message: IMessage): Uint8Array { if (!message.timestamp) @@ -44,11 +38,7 @@ export class RLNEncoder implements IEncoder { public async toWire(message: IMessage): Promise { if (!message.rateLimitProof) { - message.rateLimitProof = await this.generateProof( - message, - this.pathElements, - this.identityPathIndex - ); + message.rateLimitProof = await this.generateProof(message); log.info("Proof generated", message.rateLimitProof); } return this.encoder.toWire(message); @@ -62,11 +52,7 @@ export class RLNEncoder implements IEncoder { protoMessage.contentTopic = this.contentTopic; if (!message.rateLimitProof) { - protoMessage.rateLimitProof = await this.generateProof( - message, - this.pathElements, - this.identityPathIndex - ); + protoMessage.rateLimitProof = await this.generateProof(message); log.info("Proof generated", protoMessage.rateLimitProof); } else { protoMessage.rateLimitProof = message.rateLimitProof; @@ -74,21 +60,26 @@ export class RLNEncoder implements IEncoder { return protoMessage; } - private async generateProof( - message: IMessage, - pathElements: Uint8Array[], - identityPathIndex: Uint8Array[] - ): Promise { + private async generateProof(message: IMessage): Promise { if (!message.timestamp) throw new Error("RLNEncoder: message must have a timestamp set"); + if (!this.credentialsManager.credentials) { + throw new Error("RLNEncoder: credentials not set"); + } + if ( + !this.credentialsManager.pathElements || + !this.credentialsManager.identityPathIndex + ) { + throw new Error("RLNEncoder: merkle proof not set"); + } const signal = this.toRlnSignal(message); const { proof, epoch, rlnIdentifier } = await this.rlnInstance.zerokit.generateRLNProof( signal, message.timestamp, - this.idSecretHash, - pathElements, - identityPathIndex, + this.credentialsManager.credentials.identity.IDSecretHash, + this.credentialsManager.pathElements, + this.credentialsManager.identityPathIndex, this.rateLimit, 0 // TODO: need to track messages sent per epoch ); @@ -116,9 +107,7 @@ export class RLNEncoder implements IEncoder { type RLNEncoderOptions = { encoder: IEncoder; rlnInstance: RLNInstance; - credential: IdentityCredential; - pathElements: Uint8Array[]; - identityPathIndex: Uint8Array[]; + credentialsManager: RLNCredentialsManager; rateLimit: number; }; @@ -127,8 +116,6 @@ export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => { options.encoder, options.rlnInstance, options.rateLimit, - options.pathElements, - options.identityPathIndex, - options.credential + options.credentialsManager ); }; diff --git a/packages/rln/src/contract/index.ts b/packages/rln/src/contract/index.ts index 5d4d612733..a2a22d5786 100644 --- a/packages/rln/src/contract/index.ts +++ b/packages/rln/src/contract/index.ts @@ -1,2 +1,3 @@ export * from "./constants.js"; export * from "./types.js"; +export { RLNBaseContract } from "./rln_base_contract.js"; diff --git a/packages/rln/src/contract/rln_base_contract.ts b/packages/rln/src/contract/rln_base_contract.ts index 7952e5e67b..e866d1c029 100644 --- a/packages/rln/src/contract/rln_base_contract.ts +++ b/packages/rln/src/contract/rln_base_contract.ts @@ -579,4 +579,47 @@ export class RLNBaseContract { } return { token, price }; } + + /** + * Watches for RootStored events emitted by the contract + * @param onLogs Callback function invoked when new RootStored events are detected + * @param options Optional configuration for the watcher + * @returns A function that can be invoked to stop watching for events + * + * @example + * ```typescript + * const unwatch = contract.watchRootStoredEvent({ + * onLogs: (logs) => { + * logs.forEach(log => { + * console.log('New root:', log.args.newRoot); + * console.log('Block number:', log.blockNumber); + * }); + * } + * }); + * + * // Later, to stop watching: + * unwatch(); + * ``` + */ + public async watchRootStoredEvent( + callback: () => void, + pollingInterval?: number + ): Promise<() => void> { + log.info("Starting to watch RootStored events", { + address: this.contract.address, + pollingInterval + }); + + const fromBlock = await this.rpcClient.getBlockNumber(); + + return this.contract.watchEvent.RootStored({ + onLogs: (_) => { + callback(); + }, + onError: (error) => log.error("Error watching RootStored events:", error), + pollingInterval, + fromBlock, + batch: false + }); + } } diff --git a/packages/rln/src/credentials_manager.ts b/packages/rln/src/credentials_manager.ts index 25a61898f2..7e03dd3dfa 100644 --- a/packages/rln/src/credentials_manager.ts +++ b/packages/rln/src/credentials_manager.ts @@ -10,7 +10,12 @@ import type { } from "./keystore/index.js"; import { KeystoreEntity, Password } from "./keystore/types.js"; import { RegisterMembershipOptions, StartRLNOptions } from "./types.js"; -import { createViemClientFromWindow, RpcClient } from "./utils/index.js"; +import { + BytesUtils, + createViemClientFromWindow, + getPathDirectionsFromIndex, + RpcClient +} from "./utils/index.js"; import { Zerokit } from "./zerokit.js"; const log = new Logger("rln:credentials"); @@ -28,9 +33,14 @@ export class RLNCredentialsManager { protected keystore = Keystore.create(); public credentials: undefined | DecryptedCredentials; + public pathElements: undefined | Uint8Array[]; + public identityPathIndex: undefined | Uint8Array[]; public zerokit: Zerokit; + private unwatchRootStored?: () => void; + private rootPollingInterval?: number = 5000; + public constructor(zerokit: Zerokit) { log.info("RLNCredentialsManager initialized"); this.zerokit = zerokit; @@ -73,6 +83,11 @@ export class RLNCredentialsManager { rateLimit: rateLimit ?? this.zerokit.rateLimit }); + if (this.credentials) { + await this.updateMerkleProof(); + await this.startWatchingRootStored(); + } + log.info("RLNCredentialsManager successfully started"); this.started = true; } catch (error) { @@ -225,4 +240,80 @@ export class RLNCredentialsManager { ); } } + + /** + * Updates the Merkle proof for the current credentials + * Fetches the latest proof from the contract and updates pathElements and identityPathIndex + */ + private async updateMerkleProof(): Promise { + if (!this.contract || !this.credentials) { + log.warn("Cannot update merkle proof: contract or credentials not set"); + return; + } + + try { + const treeIndex = this.credentials.membership.treeIndex; + log.info(`Updating merkle proof for tree index: ${treeIndex}`); + + // Get the merkle proof from the contract + const proof = await this.contract.getMerkleProof(treeIndex); + + // Convert bigint[] to Uint8Array[] for pathElements + this.pathElements = proof.map((element) => + BytesUtils.bytes32FromBigInt(element, "little") + ); + + // Get path directions from the tree index + const pathDirections = getPathDirectionsFromIndex(BigInt(treeIndex)); + + // Convert path directions to Uint8Array[] for identityPathIndex + this.identityPathIndex = pathDirections.map((direction: number) => + Uint8Array.from([direction]) + ); + + log.info("Successfully updated merkle proof", { + pathElementsCount: this.pathElements.length, + pathIndexCount: this.identityPathIndex!.length + }); + } catch (error) { + log.error("Failed to update merkle proof:", error); + throw error; + } + } + + /** + * Starts watching for RootStored events and updates merkle proof when detected + */ + private async startWatchingRootStored(): Promise { + if (!this.contract) { + log.warn("Cannot watch for RootStored events: contract not set"); + return; + } + + // Stop any existing watcher + this.stopWatchingRootStored(); + + log.info("Starting to watch for RootStored events"); + + this.unwatchRootStored = await this.contract.watchRootStoredEvent(() => { + // Update the merkle proof when root changes (fire-and-forget) + this.updateMerkleProof().catch((error) => { + log.error( + "Failed to update merkle proof after RootStored event:", + error + ); + }); + }, this.rootPollingInterval); + } + + /** + * Stops watching for RootStored events + */ + private stopWatchingRootStored(): void { + if (this.unwatchRootStored) { + log.info("Stopping RootStored event watcher"); + this.unwatchRootStored(); + this.unwatchRootStored = undefined; + } + } } diff --git a/packages/rln/src/encoder.node.spec.ts b/packages/rln/src/encoder.node.spec.ts index c54ece9acd..f8e5b33512 100644 --- a/packages/rln/src/encoder.node.spec.ts +++ b/packages/rln/src/encoder.node.spec.ts @@ -1,8 +1,10 @@ import { multiaddr } from "@multiformats/multiaddr"; import { createLightNode, Protocols } from "@waku/sdk"; import { expect } from "chai"; +import Sinon from "sinon"; import { createRLNEncoder } from "./codec.js"; +import { RLNCredentialsManager } from "./credentials_manager.js"; import { Keystore } from "./keystore/index.js"; import { RLNInstance } from "./rln.js"; import { BytesUtils } from "./utils/index.js"; @@ -82,6 +84,25 @@ describe("RLN Proof Integration Tests", function () { BytesUtils.writeUIntLE(new Uint8Array(1), index, 0, 1) ); + // Create mock credentials manager + const mockCredentialsManager = Sinon.createStubInstance( + RLNCredentialsManager + ); + + // Set up the mock to return test values + Object.defineProperty(mockCredentialsManager, "credentials", { + get: () => credential, + configurable: true + }); + Object.defineProperty(mockCredentialsManager, "pathElements", { + get: () => pathElements, + configurable: true + }); + Object.defineProperty(mockCredentialsManager, "identityPathIndex", { + get: () => identityPathIndex, + configurable: true + }); + // Create base encoder const contentTopic = "/rln/1/test/proto"; const baseEncoder = waku.createEncoder({ @@ -92,9 +113,8 @@ describe("RLN Proof Integration Tests", function () { const rlnEncoder = createRLNEncoder({ encoder: baseEncoder, rlnInstance, - credential: credential.identity, - pathElements, - identityPathIndex, + credentialsManager: + mockCredentialsManager as unknown as RLNCredentialsManager, rateLimit }); diff --git a/packages/rln/src/utils/index.ts b/packages/rln/src/utils/index.ts index 808cdc5290..cf6f3e7bf1 100644 --- a/packages/rln/src/utils/index.ts +++ b/packages/rln/src/utils/index.ts @@ -9,3 +9,9 @@ export { dateToEpochBytes, dateToNanosecondBytes } from "./epoch.js"; +export { + getPathDirectionsFromIndex, + calculateRateCommitment, + reconstructMerkleRoot, + MERKLE_TREE_DEPTH +} from "./merkle.js";