mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-07 16:23:09 +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";
|
} from "@waku/interfaces";
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
|
|
||||||
import type { IdentityCredential } from "./identity.js";
|
import { RLNCredentialsManager } from "./credentials_manager.js";
|
||||||
import { Proof } from "./proof.js";
|
import { Proof } from "./proof.js";
|
||||||
import { RLNInstance } from "./rln.js";
|
import { RLNInstance } from "./rln.js";
|
||||||
import { BytesUtils } from "./utils/bytes.js";
|
import { BytesUtils } from "./utils/bytes.js";
|
||||||
@ -16,18 +16,12 @@ import { dateToNanosecondBytes } from "./utils/epoch.js";
|
|||||||
const log = new Logger("waku:rln:encoder");
|
const log = new Logger("waku:rln:encoder");
|
||||||
|
|
||||||
export class RLNEncoder implements IEncoder {
|
export class RLNEncoder implements IEncoder {
|
||||||
private readonly idSecretHash: Uint8Array;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly encoder: IEncoder,
|
private readonly encoder: IEncoder,
|
||||||
private readonly rlnInstance: RLNInstance,
|
private readonly rlnInstance: RLNInstance,
|
||||||
private readonly rateLimit: number,
|
private readonly rateLimit: number,
|
||||||
public pathElements: Uint8Array[],
|
private readonly credentialsManager: RLNCredentialsManager
|
||||||
public identityPathIndex: Uint8Array[],
|
) {}
|
||||||
identityCredential: IdentityCredential
|
|
||||||
) {
|
|
||||||
this.idSecretHash = identityCredential.IDSecretHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
private toRlnSignal(message: IMessage): Uint8Array {
|
private toRlnSignal(message: IMessage): Uint8Array {
|
||||||
if (!message.timestamp)
|
if (!message.timestamp)
|
||||||
@ -44,11 +38,7 @@ export class RLNEncoder implements IEncoder {
|
|||||||
|
|
||||||
public async toWire(message: IMessage): Promise<Uint8Array | undefined> {
|
public async toWire(message: IMessage): Promise<Uint8Array | undefined> {
|
||||||
if (!message.rateLimitProof) {
|
if (!message.rateLimitProof) {
|
||||||
message.rateLimitProof = await this.generateProof(
|
message.rateLimitProof = await this.generateProof(message);
|
||||||
message,
|
|
||||||
this.pathElements,
|
|
||||||
this.identityPathIndex
|
|
||||||
);
|
|
||||||
log.info("Proof generated", message.rateLimitProof);
|
log.info("Proof generated", message.rateLimitProof);
|
||||||
}
|
}
|
||||||
return this.encoder.toWire(message);
|
return this.encoder.toWire(message);
|
||||||
@ -62,11 +52,7 @@ export class RLNEncoder implements IEncoder {
|
|||||||
|
|
||||||
protoMessage.contentTopic = this.contentTopic;
|
protoMessage.contentTopic = this.contentTopic;
|
||||||
if (!message.rateLimitProof) {
|
if (!message.rateLimitProof) {
|
||||||
protoMessage.rateLimitProof = await this.generateProof(
|
protoMessage.rateLimitProof = await this.generateProof(message);
|
||||||
message,
|
|
||||||
this.pathElements,
|
|
||||||
this.identityPathIndex
|
|
||||||
);
|
|
||||||
log.info("Proof generated", protoMessage.rateLimitProof);
|
log.info("Proof generated", protoMessage.rateLimitProof);
|
||||||
} else {
|
} else {
|
||||||
protoMessage.rateLimitProof = message.rateLimitProof;
|
protoMessage.rateLimitProof = message.rateLimitProof;
|
||||||
@ -74,21 +60,26 @@ export class RLNEncoder implements IEncoder {
|
|||||||
return protoMessage;
|
return protoMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateProof(
|
private async generateProof(message: IMessage): Promise<IRateLimitProof> {
|
||||||
message: IMessage,
|
|
||||||
pathElements: Uint8Array[],
|
|
||||||
identityPathIndex: Uint8Array[]
|
|
||||||
): Promise<IRateLimitProof> {
|
|
||||||
if (!message.timestamp)
|
if (!message.timestamp)
|
||||||
throw new Error("RLNEncoder: message must have a timestamp set");
|
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 signal = this.toRlnSignal(message);
|
||||||
const { proof, epoch, rlnIdentifier } =
|
const { proof, epoch, rlnIdentifier } =
|
||||||
await this.rlnInstance.zerokit.generateRLNProof(
|
await this.rlnInstance.zerokit.generateRLNProof(
|
||||||
signal,
|
signal,
|
||||||
message.timestamp,
|
message.timestamp,
|
||||||
this.idSecretHash,
|
this.credentialsManager.credentials.identity.IDSecretHash,
|
||||||
pathElements,
|
this.credentialsManager.pathElements,
|
||||||
identityPathIndex,
|
this.credentialsManager.identityPathIndex,
|
||||||
this.rateLimit,
|
this.rateLimit,
|
||||||
0 // TODO: need to track messages sent per epoch
|
0 // TODO: need to track messages sent per epoch
|
||||||
);
|
);
|
||||||
@ -116,9 +107,7 @@ export class RLNEncoder implements IEncoder {
|
|||||||
type RLNEncoderOptions = {
|
type RLNEncoderOptions = {
|
||||||
encoder: IEncoder;
|
encoder: IEncoder;
|
||||||
rlnInstance: RLNInstance;
|
rlnInstance: RLNInstance;
|
||||||
credential: IdentityCredential;
|
credentialsManager: RLNCredentialsManager;
|
||||||
pathElements: Uint8Array[];
|
|
||||||
identityPathIndex: Uint8Array[];
|
|
||||||
rateLimit: number;
|
rateLimit: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,8 +116,6 @@ export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => {
|
|||||||
options.encoder,
|
options.encoder,
|
||||||
options.rlnInstance,
|
options.rlnInstance,
|
||||||
options.rateLimit,
|
options.rateLimit,
|
||||||
options.pathElements,
|
options.credentialsManager
|
||||||
options.identityPathIndex,
|
|
||||||
options.credential
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./constants.js";
|
export * from "./constants.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
export { RLNBaseContract } from "./rln_base_contract.js";
|
||||||
|
|||||||
@ -579,4 +579,47 @@ export class RLNBaseContract {
|
|||||||
}
|
}
|
||||||
return { token, price };
|
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";
|
} from "./keystore/index.js";
|
||||||
import { KeystoreEntity, Password } from "./keystore/types.js";
|
import { KeystoreEntity, Password } from "./keystore/types.js";
|
||||||
import { RegisterMembershipOptions, StartRLNOptions } from "./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";
|
import { Zerokit } from "./zerokit.js";
|
||||||
|
|
||||||
const log = new Logger("rln:credentials");
|
const log = new Logger("rln:credentials");
|
||||||
@ -28,9 +33,14 @@ export class RLNCredentialsManager {
|
|||||||
|
|
||||||
protected keystore = Keystore.create();
|
protected keystore = Keystore.create();
|
||||||
public credentials: undefined | DecryptedCredentials;
|
public credentials: undefined | DecryptedCredentials;
|
||||||
|
public pathElements: undefined | Uint8Array[];
|
||||||
|
public identityPathIndex: undefined | Uint8Array[];
|
||||||
|
|
||||||
public zerokit: Zerokit;
|
public zerokit: Zerokit;
|
||||||
|
|
||||||
|
private unwatchRootStored?: () => void;
|
||||||
|
private rootPollingInterval?: number = 5000;
|
||||||
|
|
||||||
public constructor(zerokit: Zerokit) {
|
public constructor(zerokit: Zerokit) {
|
||||||
log.info("RLNCredentialsManager initialized");
|
log.info("RLNCredentialsManager initialized");
|
||||||
this.zerokit = zerokit;
|
this.zerokit = zerokit;
|
||||||
@ -73,6 +83,11 @@ export class RLNCredentialsManager {
|
|||||||
rateLimit: rateLimit ?? this.zerokit.rateLimit
|
rateLimit: rateLimit ?? this.zerokit.rateLimit
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.credentials) {
|
||||||
|
await this.updateMerkleProof();
|
||||||
|
await this.startWatchingRootStored();
|
||||||
|
}
|
||||||
|
|
||||||
log.info("RLNCredentialsManager successfully started");
|
log.info("RLNCredentialsManager successfully started");
|
||||||
this.started = true;
|
this.started = true;
|
||||||
} catch (error) {
|
} 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 { multiaddr } from "@multiformats/multiaddr";
|
||||||
import { createLightNode, Protocols } from "@waku/sdk";
|
import { createLightNode, Protocols } from "@waku/sdk";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
import Sinon from "sinon";
|
||||||
|
|
||||||
import { createRLNEncoder } from "./codec.js";
|
import { createRLNEncoder } from "./codec.js";
|
||||||
|
import { RLNCredentialsManager } from "./credentials_manager.js";
|
||||||
import { Keystore } from "./keystore/index.js";
|
import { Keystore } from "./keystore/index.js";
|
||||||
import { RLNInstance } from "./rln.js";
|
import { RLNInstance } from "./rln.js";
|
||||||
import { BytesUtils } from "./utils/index.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)
|
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
|
// Create base encoder
|
||||||
const contentTopic = "/rln/1/test/proto";
|
const contentTopic = "/rln/1/test/proto";
|
||||||
const baseEncoder = waku.createEncoder({
|
const baseEncoder = waku.createEncoder({
|
||||||
@ -92,9 +113,8 @@ describe("RLN Proof Integration Tests", function () {
|
|||||||
const rlnEncoder = createRLNEncoder({
|
const rlnEncoder = createRLNEncoder({
|
||||||
encoder: baseEncoder,
|
encoder: baseEncoder,
|
||||||
rlnInstance,
|
rlnInstance,
|
||||||
credential: credential.identity,
|
credentialsManager:
|
||||||
pathElements,
|
mockCredentialsManager as unknown as RLNCredentialsManager,
|
||||||
identityPathIndex,
|
|
||||||
rateLimit
|
rateLimit
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -9,3 +9,9 @@ export {
|
|||||||
dateToEpochBytes,
|
dateToEpochBytes,
|
||||||
dateToNanosecondBytes
|
dateToNanosecondBytes
|
||||||
} from "./epoch.js";
|
} from "./epoch.js";
|
||||||
|
export {
|
||||||
|
getPathDirectionsFromIndex,
|
||||||
|
calculateRateCommitment,
|
||||||
|
reconstructMerkleRoot,
|
||||||
|
MERKLE_TREE_DEPTH
|
||||||
|
} from "./merkle.js";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user