diff --git a/package-lock.json b/package-lock.json index 57bd6f48d8..b34148a402 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34751,6 +34751,7 @@ "@wagmi/cli": "^2.7.0", "@waku/build-utils": "^1.0.0", "@waku/message-encryption": "^0.0.37", + "@waku/sdk": "^0.0.36", "deep-equal-in-any-order": "^2.0.6", "fast-check": "^3.23.2", "rollup-plugin-copy": "^3.5.0" diff --git a/packages/rln/package.json b/packages/rln/package.json index 6b65d79ec2..a3813e6aa4 100644 --- a/packages/rln/package.json +++ b/packages/rln/package.json @@ -61,6 +61,7 @@ "@wagmi/cli": "^2.7.0", "@waku/build-utils": "^1.0.0", "@waku/message-encryption": "^0.0.37", + "@waku/sdk": "^0.0.36", "deep-equal-in-any-order": "^2.0.6", "fast-check": "^3.23.2", "rollup-plugin-copy": "^3.5.0" diff --git a/packages/rln/src/codec.ts b/packages/rln/src/codec.ts new file mode 100644 index 0000000000..f0b2f10c07 --- /dev/null +++ b/packages/rln/src/codec.ts @@ -0,0 +1,142 @@ +import type { + IEncoder, + IMessage, + IProtoMessage, + IRateLimitProof, + IRoutingInfo +} from "@waku/interfaces"; +import { Logger } from "@waku/utils"; + +import type { IdentityCredential } from "./identity.js"; +import { Proof } from "./proof.js"; +import { RLNInstance } from "./rln.js"; +import { BytesUtils } from "./utils/bytes.js"; +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 index: number, + public pathElements: Uint8Array[], + public identityPathIndex: Uint8Array[], + identityCredential: IdentityCredential + ) { + if (index < 0) throw new Error("Invalid membership index"); + this.idSecretHash = identityCredential.IDSecretHash; + } + + private toRlnSignal(message: IMessage): Uint8Array { + if (!message.timestamp) + throw new Error("RLNEncoder: message must have a timestamp set"); + const contentTopicBytes = new TextEncoder().encode(this.contentTopic); + const timestampBytes = dateToNanosecondBytes(message.timestamp); + + return BytesUtils.concatenate( + message.payload, + contentTopicBytes, + timestampBytes + ); + } + + public async toWire(message: IMessage): Promise { + if (!message.rateLimitProof) { + message.rateLimitProof = await this.generateProof( + message, + this.index, + this.pathElements, + this.identityPathIndex + ); + log.info("Proof generated", message.rateLimitProof); + } + return this.encoder.toWire(message); + } + + public async toProtoObj( + message: IMessage + ): Promise { + const protoMessage = await this.encoder.toProtoObj(message); + if (!protoMessage) return; + + protoMessage.contentTopic = this.contentTopic; + if (!message.rateLimitProof) { + protoMessage.rateLimitProof = await this.generateProof( + message, + this.index, + this.pathElements, + this.identityPathIndex + ); + log.info("Proof generated", protoMessage.rateLimitProof); + } else { + protoMessage.rateLimitProof = message.rateLimitProof; + } + return protoMessage; + } + + private async generateProof( + message: IMessage, + leafIndex: number, + pathElements: Uint8Array[], + identityPathIndex: Uint8Array[] + ): Promise { + if (!message.timestamp) + throw new Error("RLNEncoder: message must have a timestamp set"); + const signal = this.toRlnSignal(message); + const { proof, epoch, rlnIdentifier } = + await this.rlnInstance.zerokit.generateRLNProof( + signal, + leafIndex, + message.timestamp, + this.idSecretHash, + pathElements, + identityPathIndex, + this.rateLimit, + 0 // TODO: need to track messages sent per epoch + ); + + return new Proof(proof, epoch, rlnIdentifier); + } + + public get pubsubTopic(): string { + return this.encoder.pubsubTopic; + } + + public get routingInfo(): IRoutingInfo { + return this.encoder.routingInfo; + } + + public get contentTopic(): string { + return this.encoder.contentTopic; + } + + public get ephemeral(): boolean { + return this.encoder.ephemeral; + } +} + +type RLNEncoderOptions = { + encoder: IEncoder; + rlnInstance: RLNInstance; + index: number; + credential: IdentityCredential; + pathElements: Uint8Array[]; + identityPathIndex: Uint8Array[]; + rateLimit: number; +}; + +export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => { + return new RLNEncoder( + options.encoder, + options.rlnInstance, + options.rateLimit, + options.index, + options.pathElements, + options.identityPathIndex, + options.credential + ); +}; diff --git a/packages/rln/src/proof.spec.ts b/packages/rln/src/proof.spec.ts index 6a1499db86..a00b85f1cc 100644 --- a/packages/rln/src/proof.spec.ts +++ b/packages/rln/src/proof.spec.ts @@ -1,7 +1,12 @@ +import { multiaddr } from "@multiformats/multiaddr"; +import { createLightNode, IMessage, Protocols } from "@waku/sdk"; import { expect } from "chai"; +import { createRLNEncoder } from "./codec.js"; import { Keystore } from "./keystore/index.js"; +import { Proof, proofToBytes } from "./proof.js"; import { RLNInstance } from "./rln.js"; +// import { epochBytesToInt } from "./utils/epoch.js"; import { BytesUtils } from "./utils/index.js"; import { calculateRateCommitment, @@ -11,7 +16,7 @@ import { } from "./utils/merkle.js"; import { TEST_KEYSTORE_DATA } from "./utils/test_keystore.js"; -describe("RLN Proof Integration Tests", function () { +describe.only("RLN Proof Integration Tests", function () { this.timeout(30000); it("validate stored merkle proof data", function () { @@ -91,4 +96,304 @@ describe("RLN Proof Integration Tests", function () { ); expect(isValid).to.be.true; }); + + const nwakuNode3 = multiaddr( + "/ip4/192.168.0.216/tcp/8002/ws/p2p/16Uiu2HAm4YTSbqhsa6xHfuqvo11T1oX4JgD5fMuDujsd1qojkfPi" + ); + + it("should parse proof bytes into Proof class", async function () { + const rlnInstance = await RLNInstance.create(); + + // Load credential from test keystore + const keystore = Keystore.fromString(TEST_KEYSTORE_DATA.keystoreJson); + if (!keystore) { + throw new Error("Failed to load test keystore"); + } + const credential = await keystore.readCredential( + TEST_KEYSTORE_DATA.credentialHash, + TEST_KEYSTORE_DATA.password + ); + if (!credential) { + throw new Error("Failed to unlock credential with provided password"); + } + + const idCommitment = credential.identity.IDCommitmentBigInt; + const merkleProof = TEST_KEYSTORE_DATA.merkleProof.map((p) => BigInt(p)); + const merkleRoot = BigInt(TEST_KEYSTORE_DATA.merkleRoot); + const membershipIndex = BigInt(TEST_KEYSTORE_DATA.membershipIndex); + const rateLimit = BigInt(TEST_KEYSTORE_DATA.rateLimit); + + const rateCommitment = calculateRateCommitment(idCommitment, rateLimit); + const proofElementIndexes = extractPathDirectionsFromProof( + merkleProof, + rateCommitment, + merkleRoot + ); + if (!proofElementIndexes) { + throw new Error("Failed to extract proof element indexes"); + } + + const testMessage = new TextEncoder().encode("test"); + + // Generate the proof + const { proof, epoch, rlnIdentifier } = + await rlnInstance.zerokit.generateRLNProof( + testMessage, + Number(membershipIndex), + new Date(), + credential.identity.IDSecretHash, + merkleProof.map((proof) => BytesUtils.fromBigInt(proof, 32, "little")), + proofElementIndexes.map((index) => + BytesUtils.writeUIntLE(new Uint8Array(1), index, 0, 1) + ), + Number(rateLimit), + 0 + ); + + // Parse proof bytes into Proof class + const parsedProof = new Proof(proof, epoch, rlnIdentifier); + + // Verify all fields have correct lengths according to Nim format: + // proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> + expect(parsedProof.proof).to.have.lengthOf(128); + expect(parsedProof.merkleRoot).to.have.lengthOf(32); + expect(parsedProof.externalNullifier).to.have.lengthOf(32); + expect(parsedProof.shareX).to.have.lengthOf(32); + expect(parsedProof.shareY).to.have.lengthOf(32); + expect(parsedProof.nullifier).to.have.lengthOf(32); + + // Verify merkle root matches expected + const parsedMerkleRoot = BytesUtils.toBigInt(parsedProof.merkleRoot); + expect(parsedMerkleRoot).to.equal( + merkleRoot, + "Parsed merkle root should match expected" + ); + + // Verify round-trip: proofToBytes should reconstruct original bytes + const reconstructedBytes = proofToBytes(parsedProof); + expect(reconstructedBytes).to.deep.equal( + proof, + "Reconstructed bytes should match original" + ); + + // Verify extractMetadata works + const metadata = parsedProof.extractMetadata(); + expect(metadata.nullifier).to.deep.equal(parsedProof.nullifier); + expect(metadata.shareX).to.deep.equal(parsedProof.shareX); + expect(metadata.shareY).to.deep.equal(parsedProof.shareY); + expect(metadata.externalNullifier).to.deep.equal( + parsedProof.externalNullifier + ); + }); + + it.only("sends a message with a proof", async function () { + const waku = await createLightNode({ + networkConfig: { + clusterId: 0, + numShardsInCluster: 1 + }, + defaultBootstrap: false, + libp2p: { + filterMultiaddrs: false + } + }); + + // Create RLN instance + const rlnInstance = await RLNInstance.create(); + + // Load credential from test keystore + const keystore = Keystore.fromString(TEST_KEYSTORE_DATA.keystoreJson); + if (!keystore) { + throw new Error("Failed to load test keystore"); + } + const credential = await keystore.readCredential( + TEST_KEYSTORE_DATA.credentialHash, + TEST_KEYSTORE_DATA.password + ); + if (!credential) { + throw new Error("Failed to unlock credential with provided password"); + } + + // Prepare merkle proof data + const idCommitment = credential.identity.IDCommitmentBigInt; + const merkleProof = TEST_KEYSTORE_DATA.merkleProof.map((p) => BigInt(p)); + const merkleRoot = BigInt(TEST_KEYSTORE_DATA.merkleRoot); + const membershipIndex = Number(TEST_KEYSTORE_DATA.membershipIndex); + const rateLimit = Number(TEST_KEYSTORE_DATA.rateLimit); + + const rateCommitment = calculateRateCommitment( + idCommitment, + BigInt(rateLimit) + ); + const proofElementIndexes = extractPathDirectionsFromProof( + merkleProof, + rateCommitment, + merkleRoot + ); + if (!proofElementIndexes) { + throw new Error("Failed to extract proof element indexes"); + } + + // Convert merkle proof to bytes format + const pathElements = merkleProof.map((proof) => + BytesUtils.fromBigInt(proof, 32, "little") + ); + const identityPathIndex = proofElementIndexes.map((index) => + BytesUtils.writeUIntLE(new Uint8Array(1), index, 0, 1) + ); + + // Create base encoder + const contentTopic = "/rln/1/test/proto"; + // const pubsubTopic = "/waku/2/rs/1/0"; + const baseEncoder = waku.createEncoder({ + contentTopic + }); + + // Create RLN encoder + const rlnEncoder = createRLNEncoder({ + encoder: baseEncoder, + rlnInstance, + index: membershipIndex, + credential: credential.identity, + pathElements, + identityPathIndex, + rateLimit + }); + + await waku.dial(nwakuNode3, [Protocols.LightPush]); + + await waku.waitForPeers([Protocols.LightPush]); + + // Create message + const messageTimestamp = new Date(); + const message = { + payload: new TextEncoder().encode("Hello RLN!"), + timestamp: messageTimestamp + }; + + // Send message with proof + const result = await waku.lightPush.send(rlnEncoder, message); + console.log("LightPush result:", result); + + if (result.failures) { + console.log(result.failures.map((f) => f.error)); + } + + expect(result.successes.length).to.be.greaterThan(0); + }); + + it("send many messages, track which succeed or fail", async function () { + this.timeout(50000); + + const waku = await createLightNode({ + networkConfig: { + clusterId: 0, + numShardsInCluster: 1 + }, + defaultBootstrap: false, + libp2p: { + filterMultiaddrs: false + } + }); + + console.log("node created"); + // Create RLN instance + const rlnInstance = await RLNInstance.create(); + + // Load credential from test keystore + const keystore = Keystore.fromString(TEST_KEYSTORE_DATA.keystoreJson); + if (!keystore) { + throw new Error("Failed to load test keystore"); + } + const credential = await keystore.readCredential( + TEST_KEYSTORE_DATA.credentialHash, + TEST_KEYSTORE_DATA.password + ); + if (!credential) { + throw new Error("Failed to unlock credential with provided password"); + } + + // Prepare merkle proof data + const idCommitment = credential.identity.IDCommitmentBigInt; + const merkleProof = TEST_KEYSTORE_DATA.merkleProof.map((p) => BigInt(p)); + const merkleRoot = BigInt(TEST_KEYSTORE_DATA.merkleRoot); + const membershipIndex = Number(TEST_KEYSTORE_DATA.membershipIndex); + const rateLimit = Number(TEST_KEYSTORE_DATA.rateLimit); + + const rateCommitment = calculateRateCommitment( + idCommitment, + BigInt(rateLimit) + ); + const proofElementIndexes = extractPathDirectionsFromProof( + merkleProof, + rateCommitment, + merkleRoot + ); + if (!proofElementIndexes) { + throw new Error("Failed to extract proof element indexes"); + } + + // Convert merkle proof to bytes format + const pathElements = merkleProof.map((proof) => + BytesUtils.fromBigInt(proof, 32, "little") + ); + const identityPathIndex = proofElementIndexes.map((index) => + BytesUtils.writeUIntLE(new Uint8Array(1), index, 0, 1) + ); + + // Create base encoder + const contentTopic = "/rln/1/test/proto"; + // const pubsubTopic = "/waku/2/rs/1/0"; + const baseEncoder = waku.createEncoder({ + contentTopic + }); + + // Create RLN encoder + const rlnEncoder = createRLNEncoder({ + encoder: baseEncoder, + rlnInstance, + index: membershipIndex, + credential: credential.identity, + pathElements, + identityPathIndex, + rateLimit + }); + + // connect to node + await waku.dial(nwakuNode3, [Protocols.LightPush]); + console.log("node dialed"); + await waku.waitForPeers([Protocols.LightPush]); + console.log("peers waited"); + + const messagesToSend = 20; + + const results: { + success: boolean; + epoch: number; + }[] = []; + + for (let i = 0; i < messagesToSend; i++) { + // Create message + const messageTimestamp = new Date(); + const message = { + payload: new TextEncoder().encode("Hello RLN!"), + timestamp: messageTimestamp + }; + + // Send message with proof + console.log("sending message", i); + const result = await waku.lightPush.send(rlnEncoder, message, { + autoRetry: false + }); + const success = result.successes.length > 0; + console.log("success:", success); + const timestampSeconds = Math.floor(message.timestamp!.getTime() / 1000); + results.push({ + success, + epoch: timestampSeconds + }); + + await new Promise((resolve) => setTimeout(resolve, 2500)); + } + }); }); diff --git a/packages/rln/src/proof.ts b/packages/rln/src/proof.ts new file mode 100644 index 0000000000..e7876e3e01 --- /dev/null +++ b/packages/rln/src/proof.ts @@ -0,0 +1,81 @@ +import type { IRateLimitProof } from "@waku/interfaces"; + +import { BytesUtils } from "./utils/index.js"; + +// Offsets for parsing proof bytes +// Format: proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> +const proofOffset = 128; +const rootOffset = proofOffset + 32; +const externalNullifierOffset = rootOffset + 32; +const shareXOffset = externalNullifierOffset + 32; +const shareYOffset = shareXOffset + 32; +const nullifierOffset = shareYOffset + 32; + +class ProofMetadata { + public constructor( + public readonly nullifier: Uint8Array, + public readonly shareX: Uint8Array, + public readonly shareY: Uint8Array, + public readonly externalNullifier: Uint8Array + ) {} +} + +export class Proof implements IRateLimitProof { + public readonly proof: Uint8Array; + public readonly merkleRoot: Uint8Array; + public readonly externalNullifier: Uint8Array; + public readonly shareX: Uint8Array; + public readonly shareY: Uint8Array; + public readonly nullifier: Uint8Array; + public readonly epoch: Uint8Array; + public readonly rlnIdentifier: Uint8Array; + + public constructor( + proofBytes: Uint8Array, + epoch: Uint8Array, + rlnIdentifier: Uint8Array + ) { + if (proofBytes.length < nullifierOffset) { + throw new Error("invalid proof"); + } + // parse the proof as proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> + this.proof = proofBytes.subarray(0, proofOffset); + this.merkleRoot = proofBytes.subarray(proofOffset, rootOffset); + this.externalNullifier = proofBytes.subarray( + rootOffset, + externalNullifierOffset + ); + this.shareX = proofBytes.subarray(externalNullifierOffset, shareXOffset); + this.shareY = proofBytes.subarray(shareXOffset, shareYOffset); + this.nullifier = proofBytes.subarray(shareYOffset, nullifierOffset); + + if (epoch.length !== 32) { + throw new Error("invalid epoch"); + } + if (rlnIdentifier.length !== 32) { + throw new Error("invalid rlnIdentifier"); + } + this.epoch = epoch; + this.rlnIdentifier = rlnIdentifier; + } + + public extractMetadata(): ProofMetadata { + return new ProofMetadata( + this.nullifier, + this.shareX, + this.shareY, + this.externalNullifier + ); + } +} + +export function proofToBytes(p: Proof): Uint8Array { + return BytesUtils.concatenate( + p.proof, + p.merkleRoot, + p.externalNullifier, + p.shareX, + p.shareY, + p.nullifier + ); +} diff --git a/packages/rln/src/scripts/update_merkle_proof.ts b/packages/rln/src/scripts/update_merkle_proof.ts new file mode 100644 index 0000000000..c2afb49408 --- /dev/null +++ b/packages/rln/src/scripts/update_merkle_proof.ts @@ -0,0 +1,69 @@ +import { type Address, createWalletClient, http, publicActions } from "viem"; +import { lineaSepolia } from "viem/chains"; + +import { RLN_CONTRACT } from "../contract/constants.js"; +import { RLNBaseContract } from "../contract/rln_base_contract.js"; +import { TEST_KEYSTORE_DATA } from "../utils/test_keystore.js"; + +async function updateMerkleProof(): Promise { + // eslint-disable-next-line no-console + console.log("Connecting to Linea Sepolia RPC..."); + + // Create RPC client (read-only, no account needed) + const rpcClient = createWalletClient({ + chain: lineaSepolia, + transport: http("https://rpc.sepolia.linea.build") + }).extend(publicActions); + + // eslint-disable-next-line no-console + console.log("Initializing RLN contract..."); + const contract = await RLNBaseContract.create({ + address: RLN_CONTRACT.address as Address, + rpcClient + }); + + const membershipIndex = Number(TEST_KEYSTORE_DATA.membershipIndex); + // eslint-disable-next-line no-console + console.log(`Fetching merkle proof for index ${membershipIndex}...`); + + // Get current merkle root + const merkleRoot = await contract.getMerkleRoot(); + // eslint-disable-next-line no-console + console.log(`Current merkle root: ${merkleRoot}`); + + // Get merkle proof for the membership index + const merkleProof = await contract.getMerkleProof(membershipIndex); + // eslint-disable-next-line no-console + console.log(`Merkle proof (${merkleProof.length} elements):`); + merkleProof.forEach((element, i) => { + // eslint-disable-next-line no-console + console.log(` [${i}]: ${element}`); + }); + + // Format the output for updating test_keystore.ts + // eslint-disable-next-line no-console + console.log("\n=== Update test_keystore.ts with these values ===\n"); + // eslint-disable-next-line no-console + console.log("merkleProof: ["); + merkleProof.forEach((element, i) => { + const comma = i < merkleProof.length - 1 ? "," : ""; + // eslint-disable-next-line no-console + console.log(` "${element}"${comma}`); + }); + // eslint-disable-next-line no-console + console.log("],"); + // eslint-disable-next-line no-console + console.log(`merkleRoot: "${merkleRoot}",`); +} + +updateMerkleProof() + .then(() => { + // eslint-disable-next-line no-console + console.log("\nScript completed successfully!"); + process.exit(0); + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error("Error updating merkle proof:", error); + process.exit(1); + }); diff --git a/packages/rln/src/utils/epoch.ts b/packages/rln/src/utils/epoch.ts index bf89d40aa5..8772c76a20 100644 --- a/packages/rln/src/utils/epoch.ts +++ b/packages/rln/src/utils/epoch.ts @@ -1,18 +1,13 @@ -import { Logger } from "@waku/utils"; - import { BytesUtils } from "./bytes.js"; const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds -const log = new Logger("rln:epoch"); - export function dateToEpoch( timestamp: Date, epochUnitSeconds: number = DefaultEpochUnitSeconds ): number { const time = timestamp.getTime(); const epoch = Math.floor(time / 1000 / epochUnitSeconds); - log.info("generated epoch", epoch); return epoch; } @@ -23,6 +18,18 @@ export function epochIntToBytes(epoch: number): Uint8Array { export function epochBytesToInt(bytes: Uint8Array): number { const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const epoch = dv.getUint32(0, true); - log.info("decoded epoch", epoch, bytes); return epoch; } + +export function dateToEpochSeconds(timestamp: Date): number { + return Math.floor(timestamp.getTime() / 1000); +} + +export function dateToEpochBytes(timestamp: Date): Uint8Array { + return epochIntToBytes(dateToEpochSeconds(timestamp)); +} + +export function dateToNanosecondBytes(timestamp: Date): Uint8Array { + const nanoseconds = BigInt(timestamp.getTime()) * 1000000n; + return BytesUtils.fromBigInt(nanoseconds, 8, "little"); +} diff --git a/packages/rln/src/utils/index.ts b/packages/rln/src/utils/index.ts index a5cf32937e..808cdc5290 100644 --- a/packages/rln/src/utils/index.ts +++ b/packages/rln/src/utils/index.ts @@ -1,4 +1,11 @@ export { createViemClientFromWindow, RpcClient } from "./rpcClient.js"; export { BytesUtils } from "./bytes.js"; export { sha256, poseidonHash } from "./hash.js"; -export { dateToEpoch, epochIntToBytes, epochBytesToInt } from "./epoch.js"; +export { + dateToEpoch, + epochIntToBytes, + epochBytesToInt, + dateToEpochSeconds, + dateToEpochBytes, + dateToNanosecondBytes +} from "./epoch.js"; diff --git a/packages/rln/src/utils/test_keystore.ts b/packages/rln/src/utils/test_keystore.ts index 707ec8f451..e88d525fdf 100644 --- a/packages/rln/src/utils/test_keystore.ts +++ b/packages/rln/src/utils/test_keystore.ts @@ -11,7 +11,7 @@ export const TEST_KEYSTORE_DATA = { "8522396354694062508299995669286882048091268903835874022564768254605186873188", "4967828252976847302563643214799688359334626491919847999565033460501719790119", "985039452502497454598906195897243897432778848314526706136284672198477696437", - "3565679202982155915846059790230166166058846233389836779083891288518797717794", + "19922236706682864826758848301828373105737204541535252785791101041288809679484", "1241870589869015758600129850815671823696180350556207862318506998039540071293", "21551820661461729022865262380882070649935529853313286572328683688269863701601", "16870197621778677478951480138572599814910741341994641594346262317677658226992", @@ -27,7 +27,7 @@ export const TEST_KEYSTORE_DATA = { "10941962436777715901943463195175331263348098796018438960955633645115732864202" ], merkleRoot: - "3281768056038133311055294993138164819435524453040629949691729675724822631973", + "2736078608533319394386474878088665333284588969678017122712404976506399404519", membershipIndex: "703", rateLimit: "300" }; diff --git a/packages/rln/src/zerokit.ts b/packages/rln/src/zerokit.ts index d39aebde85..328412bfe0 100644 --- a/packages/rln/src/zerokit.ts +++ b/packages/rln/src/zerokit.ts @@ -5,7 +5,7 @@ import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./contract/constants.js"; import { IdentityCredential } from "./identity.js"; import { WitnessCalculator } from "./resources/witness_calculator"; import { BytesUtils } from "./utils/bytes.js"; -import { dateToEpoch, epochIntToBytes } from "./utils/epoch.js"; +import { dateToEpochBytes } from "./utils/epoch.js"; import { poseidonHash, sha256 } from "./utils/hash.js"; import { MERKLE_TREE_DEPTH } from "./utils/merkle.js"; @@ -13,10 +13,13 @@ export class Zerokit { public constructor( private readonly zkRLN: number, private readonly witnessCalculator: WitnessCalculator, - private readonly _rateLimit: number = DEFAULT_RATE_LIMIT, - private readonly rlnIdentifier: Uint8Array = new TextEncoder().encode( - "rln/waku-rln-relay/v2.0.0" - ) + public readonly rateLimit: number = DEFAULT_RATE_LIMIT, + public readonly rlnIdentifier: Uint8Array = (() => { + const encoded = new TextEncoder().encode("rln/waku-rln-relay/v2.0.0"); + const padded = new Uint8Array(32); + padded.set(encoded); + return padded; + })() ) {} public get getZkRLN(): number { @@ -27,10 +30,6 @@ export class Zerokit { return this.witnessCalculator; } - public get rateLimit(): number { - return this._rateLimit; - } - public generateSeededIdentityCredential(seed: string): IdentityCredential { const stringEncoder = new TextEncoder(); const seedBytes = stringEncoder.encode(seed); @@ -84,18 +83,18 @@ export class Zerokit { public async generateRLNProof( msg: Uint8Array, - epoch: Uint8Array | Date | undefined, + timestamp: Date, idSecretHash: Uint8Array, pathElements: Uint8Array[], identityPathIndex: Uint8Array[], rateLimit: number, messageId: number // number of message sent by the user in this epoch - ): Promise { - if (epoch === undefined) { - epoch = epochIntToBytes(dateToEpoch(new Date())); - } else if (epoch instanceof Date) { - epoch = epochIntToBytes(dateToEpoch(epoch)); - } + ): Promise<{ + proof: Uint8Array; + epoch: Uint8Array; + rlnIdentifier: Uint8Array; + }> { + const epoch = dateToEpochBytes(timestamp); if (epoch.length !== 32) throw new Error(`Epoch must be 32 bytes, got ${epoch.length}`); @@ -137,11 +136,16 @@ export class Zerokit { ) as Record; const calculatedWitness: bigint[] = await this.witnessCalculator.calculateWitness(witnessJson); - return zerokitRLN.generateRLNProofWithWitness( + const proof = zerokitRLN.generateRLNProofWithWitness( this.zkRLN, calculatedWitness, serializedWitness ); + return { + proof, + epoch, + rlnIdentifier: this.rlnIdentifier + }; } public verifyRLNProof(