mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-04 06:43: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",
|
"@wagmi/cli": "^2.7.0",
|
||||||
"@waku/build-utils": "^1.0.0",
|
"@waku/build-utils": "^1.0.0",
|
||||||
"@waku/message-encryption": "^0.0.37",
|
"@waku/message-encryption": "^0.0.37",
|
||||||
|
"@waku/sdk": "^0.0.36",
|
||||||
"deep-equal-in-any-order": "^2.0.6",
|
"deep-equal-in-any-order": "^2.0.6",
|
||||||
"fast-check": "^3.23.2",
|
"fast-check": "^3.23.2",
|
||||||
"rollup-plugin-copy": "^3.5.0"
|
"rollup-plugin-copy": "^3.5.0"
|
||||||
|
|||||||
@ -61,6 +61,7 @@
|
|||||||
"@wagmi/cli": "^2.7.0",
|
"@wagmi/cli": "^2.7.0",
|
||||||
"@waku/build-utils": "^1.0.0",
|
"@waku/build-utils": "^1.0.0",
|
||||||
"@waku/message-encryption": "^0.0.37",
|
"@waku/message-encryption": "^0.0.37",
|
||||||
|
"@waku/sdk": "^0.0.36",
|
||||||
"deep-equal-in-any-order": "^2.0.6",
|
"deep-equal-in-any-order": "^2.0.6",
|
||||||
"fast-check": "^3.23.2",
|
"fast-check": "^3.23.2",
|
||||||
"rollup-plugin-copy": "^3.5.0"
|
"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 { expect } from "chai";
|
||||||
|
|
||||||
|
import { createRLNEncoder } from "./codec.js";
|
||||||
import { Keystore } from "./keystore/index.js";
|
import { Keystore } from "./keystore/index.js";
|
||||||
|
import { Proof, proofToBytes } from "./proof.js";
|
||||||
import { RLNInstance } from "./rln.js";
|
import { RLNInstance } from "./rln.js";
|
||||||
|
// import { epochBytesToInt } from "./utils/epoch.js";
|
||||||
import { BytesUtils } from "./utils/index.js";
|
import { BytesUtils } from "./utils/index.js";
|
||||||
import {
|
import {
|
||||||
calculateRateCommitment,
|
calculateRateCommitment,
|
||||||
@ -11,7 +16,7 @@ import {
|
|||||||
} from "./utils/merkle.js";
|
} from "./utils/merkle.js";
|
||||||
import { TEST_KEYSTORE_DATA } from "./utils/test_keystore.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);
|
this.timeout(30000);
|
||||||
|
|
||||||
it("validate stored merkle proof data", function () {
|
it("validate stored merkle proof data", function () {
|
||||||
@ -91,4 +96,304 @@ describe("RLN Proof Integration Tests", function () {
|
|||||||
);
|
);
|
||||||
expect(isValid).to.be.true;
|
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";
|
import { BytesUtils } from "./bytes.js";
|
||||||
|
|
||||||
const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds
|
const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds
|
||||||
|
|
||||||
const log = new Logger("rln:epoch");
|
|
||||||
|
|
||||||
export function dateToEpoch(
|
export function dateToEpoch(
|
||||||
timestamp: Date,
|
timestamp: Date,
|
||||||
epochUnitSeconds: number = DefaultEpochUnitSeconds
|
epochUnitSeconds: number = DefaultEpochUnitSeconds
|
||||||
): number {
|
): number {
|
||||||
const time = timestamp.getTime();
|
const time = timestamp.getTime();
|
||||||
const epoch = Math.floor(time / 1000 / epochUnitSeconds);
|
const epoch = Math.floor(time / 1000 / epochUnitSeconds);
|
||||||
log.info("generated epoch", epoch);
|
|
||||||
return epoch;
|
return epoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +18,18 @@ export function epochIntToBytes(epoch: number): Uint8Array {
|
|||||||
export function epochBytesToInt(bytes: Uint8Array): number {
|
export function epochBytesToInt(bytes: Uint8Array): number {
|
||||||
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||||
const epoch = dv.getUint32(0, true);
|
const epoch = dv.getUint32(0, true);
|
||||||
log.info("decoded epoch", epoch, bytes);
|
|
||||||
return epoch;
|
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 { createViemClientFromWindow, RpcClient } from "./rpcClient.js";
|
||||||
export { BytesUtils } from "./bytes.js";
|
export { BytesUtils } from "./bytes.js";
|
||||||
export { sha256, poseidonHash } from "./hash.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",
|
"8522396354694062508299995669286882048091268903835874022564768254605186873188",
|
||||||
"4967828252976847302563643214799688359334626491919847999565033460501719790119",
|
"4967828252976847302563643214799688359334626491919847999565033460501719790119",
|
||||||
"985039452502497454598906195897243897432778848314526706136284672198477696437",
|
"985039452502497454598906195897243897432778848314526706136284672198477696437",
|
||||||
"3565679202982155915846059790230166166058846233389836779083891288518797717794",
|
"19922236706682864826758848301828373105737204541535252785791101041288809679484",
|
||||||
"1241870589869015758600129850815671823696180350556207862318506998039540071293",
|
"1241870589869015758600129850815671823696180350556207862318506998039540071293",
|
||||||
"21551820661461729022865262380882070649935529853313286572328683688269863701601",
|
"21551820661461729022865262380882070649935529853313286572328683688269863701601",
|
||||||
"16870197621778677478951480138572599814910741341994641594346262317677658226992",
|
"16870197621778677478951480138572599814910741341994641594346262317677658226992",
|
||||||
@ -27,7 +27,7 @@ export const TEST_KEYSTORE_DATA = {
|
|||||||
"10941962436777715901943463195175331263348098796018438960955633645115732864202"
|
"10941962436777715901943463195175331263348098796018438960955633645115732864202"
|
||||||
],
|
],
|
||||||
merkleRoot:
|
merkleRoot:
|
||||||
"3281768056038133311055294993138164819435524453040629949691729675724822631973",
|
"2736078608533319394386474878088665333284588969678017122712404976506399404519",
|
||||||
membershipIndex: "703",
|
membershipIndex: "703",
|
||||||
rateLimit: "300"
|
rateLimit: "300"
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./contract/constants.js";
|
|||||||
import { IdentityCredential } from "./identity.js";
|
import { IdentityCredential } from "./identity.js";
|
||||||
import { WitnessCalculator } from "./resources/witness_calculator";
|
import { WitnessCalculator } from "./resources/witness_calculator";
|
||||||
import { BytesUtils } from "./utils/bytes.js";
|
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 { poseidonHash, sha256 } from "./utils/hash.js";
|
||||||
import { MERKLE_TREE_DEPTH } from "./utils/merkle.js";
|
import { MERKLE_TREE_DEPTH } from "./utils/merkle.js";
|
||||||
|
|
||||||
@ -13,10 +13,13 @@ export class Zerokit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly zkRLN: number,
|
private readonly zkRLN: number,
|
||||||
private readonly witnessCalculator: WitnessCalculator,
|
private readonly witnessCalculator: WitnessCalculator,
|
||||||
private readonly _rateLimit: number = DEFAULT_RATE_LIMIT,
|
public readonly rateLimit: number = DEFAULT_RATE_LIMIT,
|
||||||
private readonly rlnIdentifier: Uint8Array = new TextEncoder().encode(
|
public readonly rlnIdentifier: Uint8Array = (() => {
|
||||||
"rln/waku-rln-relay/v2.0.0"
|
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 {
|
public get getZkRLN(): number {
|
||||||
@ -27,10 +30,6 @@ export class Zerokit {
|
|||||||
return this.witnessCalculator;
|
return this.witnessCalculator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get rateLimit(): number {
|
|
||||||
return this._rateLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public generateSeededIdentityCredential(seed: string): IdentityCredential {
|
public generateSeededIdentityCredential(seed: string): IdentityCredential {
|
||||||
const stringEncoder = new TextEncoder();
|
const stringEncoder = new TextEncoder();
|
||||||
const seedBytes = stringEncoder.encode(seed);
|
const seedBytes = stringEncoder.encode(seed);
|
||||||
@ -84,18 +83,18 @@ export class Zerokit {
|
|||||||
|
|
||||||
public async generateRLNProof(
|
public async generateRLNProof(
|
||||||
msg: Uint8Array,
|
msg: Uint8Array,
|
||||||
epoch: Uint8Array | Date | undefined,
|
timestamp: Date,
|
||||||
idSecretHash: Uint8Array,
|
idSecretHash: Uint8Array,
|
||||||
pathElements: Uint8Array[],
|
pathElements: Uint8Array[],
|
||||||
identityPathIndex: Uint8Array[],
|
identityPathIndex: Uint8Array[],
|
||||||
rateLimit: number,
|
rateLimit: number,
|
||||||
messageId: number // number of message sent by the user in this epoch
|
messageId: number // number of message sent by the user in this epoch
|
||||||
): Promise<Uint8Array> {
|
): Promise<{
|
||||||
if (epoch === undefined) {
|
proof: Uint8Array;
|
||||||
epoch = epochIntToBytes(dateToEpoch(new Date()));
|
epoch: Uint8Array;
|
||||||
} else if (epoch instanceof Date) {
|
rlnIdentifier: Uint8Array;
|
||||||
epoch = epochIntToBytes(dateToEpoch(epoch));
|
}> {
|
||||||
}
|
const epoch = dateToEpochBytes(timestamp);
|
||||||
|
|
||||||
if (epoch.length !== 32)
|
if (epoch.length !== 32)
|
||||||
throw new Error(`Epoch must be 32 bytes, got ${epoch.length}`);
|
throw new Error(`Epoch must be 32 bytes, got ${epoch.length}`);
|
||||||
@ -137,11 +136,16 @@ export class Zerokit {
|
|||||||
) as Record<string, unknown>;
|
) as Record<string, unknown>;
|
||||||
const calculatedWitness: bigint[] =
|
const calculatedWitness: bigint[] =
|
||||||
await this.witnessCalculator.calculateWitness(witnessJson);
|
await this.witnessCalculator.calculateWitness(witnessJson);
|
||||||
return zerokitRLN.generateRLNProofWithWitness(
|
const proof = zerokitRLN.generateRLNProofWithWitness(
|
||||||
this.zkRLN,
|
this.zkRLN,
|
||||||
calculatedWitness,
|
calculatedWitness,
|
||||||
serializedWitness
|
serializedWitness
|
||||||
);
|
);
|
||||||
|
return {
|
||||||
|
proof,
|
||||||
|
epoch,
|
||||||
|
rlnIdentifier: this.rlnIdentifier
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public verifyRLNProof(
|
public verifyRLNProof(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user