feat: store and update merkle proof/index in credentials manager

This commit is contained in:
Arseniy Klempner 2025-12-18 17:53:44 -08:00
parent 804144daba
commit 94a1eb4dd6
No known key found for this signature in database
GPG Key ID: 51653F18863BD24B
6 changed files with 185 additions and 37 deletions

View File

@ -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<Uint8Array | undefined> {
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<IRateLimitProof> {
private async generateProof(message: IMessage): Promise<IRateLimitProof> {
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
);
};

View File

@ -1,2 +1,3 @@
export * from "./constants.js";
export * from "./types.js";
export { RLNBaseContract } from "./rln_base_contract.js";

View File

@ -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
});
}
}

View File

@ -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<void> {
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<void> {
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;
}
}
}

View File

@ -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
});

View File

@ -9,3 +9,9 @@ export {
dateToEpochBytes,
dateToNanosecondBytes
} from "./epoch.js";
export {
getPathDirectionsFromIndex,
calculateRateCommitment,
reconstructMerkleRoot,
MERKLE_TREE_DEPTH
} from "./merkle.js";