mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-04 14:53:08 +00:00
feat: store and update merkle proof/index in credentials manager
This commit is contained in:
parent
804144daba
commit
94a1eb4dd6
@ -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
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./constants.js";
|
||||
export * from "./types.js";
|
||||
export { RLNBaseContract } from "./rln_base_contract.js";
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
|
||||
@ -9,3 +9,9 @@ export {
|
||||
dateToEpochBytes,
|
||||
dateToNanosecondBytes
|
||||
} from "./epoch.js";
|
||||
export {
|
||||
getPathDirectionsFromIndex,
|
||||
calculateRateCommitment,
|
||||
reconstructMerkleRoot,
|
||||
MERKLE_TREE_DEPTH
|
||||
} from "./merkle.js";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user