mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-02 13:53:12 +00:00
feat(rln): implement RLN encoder
This commit is contained in:
parent
a20fac086e
commit
cbf99f27b5
1
package-lock.json
generated
1
package-lock.json
generated
@ -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"
|
||||
|
||||
@ -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
142
packages/rln/src/codec.ts
Normal 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
|
||||
);
|
||||
};
|
||||
@ -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
81
packages/rln/src/proof.ts
Normal 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
|
||||
);
|
||||
}
|
||||
69
packages/rln/src/scripts/update_merkle_proof.ts
Normal file
69
packages/rln/src/scripts/update_merkle_proof.ts
Normal 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);
|
||||
});
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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"
|
||||
};
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user