feat(rln): implement RLN encoder

This commit is contained in:
Arseniy Klempner 2025-12-12 16:10:25 -08:00
parent a20fac086e
commit cbf99f27b5
No known key found for this signature in database
GPG Key ID: 51653F18863BD24B
10 changed files with 644 additions and 27 deletions

1
package-lock.json generated
View File

@ -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"

View File

@ -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"

142
packages/rln/src/codec.ts Normal file
View File

@ -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<Uint8Array | undefined> {
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<IProtoMessage | undefined> {
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<IRateLimitProof> {
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
);
};

View File

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

81
packages/rln/src/proof.ts Normal file
View File

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

View File

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

View File

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

View File

@ -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";

View File

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

View File

@ -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<Uint8Array> {
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<string, unknown>;
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(