feat: upgrade zerokit

This commit is contained in:
Arseniy Klempner 2025-12-19 14:32:43 -08:00
parent 94a1eb4dd6
commit bdbe6181e2
No known key found for this signature in database
GPG Key ID: 51653F18863BD24B
15 changed files with 116 additions and 184 deletions

17
package-lock.json generated
View File

@ -7138,15 +7138,10 @@
"link": true
},
"node_modules/@waku/zerokit-rln-wasm": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.2.1.tgz",
"integrity": "sha512-2Xp7e92y4qZpsiTPGBSVr4gVJ9mJTLaudlo0DQxNpxJUBtoJKpxdH5xDCQDiorbkWZC2j9EId+ohhxHO/xC1QQ==",
"license": "MIT or Apache2"
},
"node_modules/@waku/zerokit-rln-wasm-utils": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm-utils/-/zerokit-rln-wasm-utils-0.1.0.tgz",
"integrity": "sha512-3ccyg9+CtRXFJfWaxI/kx8Aec5B2S9YUmZAVhPRdN1EG6iQYG2hgvAurx8ZF9/zOppdrhzzyvCgDPg5kRUlOfQ=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-1.0.0.tgz",
"integrity": "sha512-kRAeUePAY3++i5XXniCx+tqDH+3rdfPKED/lFRrbQ8ZiNWpu059fKxtPQqqvd8jNZQUOWDc7HRTpq2TVbWd8yQ==",
"license": "MIT OR Apache-2.0"
},
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
@ -34727,8 +34722,7 @@
"@wagmi/core": "^2.22.1",
"@waku/core": "^0.0.40",
"@waku/utils": "^0.0.27",
"@waku/zerokit-rln-wasm": "^0.2.1",
"@waku/zerokit-rln-wasm-utils": "^0.1.0",
"@waku/zerokit-rln-wasm": "^1.0.0",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0",
@ -34752,6 +34746,7 @@
"@waku/build-utils": "^1.0.0",
"@waku/message-encryption": "^0.0.37",
"@waku/sdk": "^0.0.36",
"@waku/tests": "*",
"deep-equal-in-any-order": "^2.0.6",
"fast-check": "^3.23.2",
"rollup-plugin-copy": "^3.5.0"

View File

@ -26,7 +26,7 @@ module.exports = function (config) {
nocache: true
},
{
pattern: "src/resources/**/*.zkey",
pattern: "src/resources/**/*.arkzkey",
included: false,
served: true,
watched: false,
@ -39,14 +39,6 @@ module.exports = function (config) {
watched: false,
type: "wasm",
nocache: true
},
{
pattern: "../../node_modules/@waku/zerokit-rln-wasm-utils/*.wasm",
included: false,
served: true,
watched: false,
type: "wasm",
nocache: true
}
],
@ -68,7 +60,7 @@ module.exports = function (config) {
mime: {
"application/wasm": ["wasm"],
"application/octet-stream": ["zkey"]
"application/octet-stream": ["arkzkey"]
},
customHeaders: [
@ -78,7 +70,7 @@ module.exports = function (config) {
value: "application/wasm"
},
{
match: ".*\\.zkey$",
match: ".*\\.arkzkey$",
name: "Content-Type",
value: "application/octet-stream"
}
@ -91,16 +83,10 @@ module.exports = function (config) {
__dirname,
"../../node_modules/@waku/zerokit-rln-wasm/rln_wasm_bg.wasm"
),
"/base/rln_wasm_utils_bg.wasm":
"/absolute" +
path.resolve(
__dirname,
"../../node_modules/@waku/zerokit-rln-wasm-utils/rln_wasm_utils_bg.wasm"
),
"/base/rln.wasm":
"/absolute" + path.resolve(__dirname, "src/resources/rln.wasm"),
"/base/rln_final.zkey":
"/absolute" + path.resolve(__dirname, "src/resources/rln_final.zkey")
"/base/rln_final.arkzkey":
"/absolute" + path.resolve(__dirname, "src/resources/rln_final.arkzkey")
},
webpack: {
@ -131,7 +117,7 @@ module.exports = function (config) {
}
},
{
test: /\.zkey$/,
test: /\.arkzkey$/,
type: "asset/resource",
generator: {
filename: "[name][ext]"

View File

@ -33,7 +33,7 @@ module.exports = function (config) {
nocache: true
},
{
pattern: "src/resources/**/*.zkey",
pattern: "src/resources/**/*.arkzkey",
included: false,
served: true,
watched: false,
@ -47,14 +47,6 @@ module.exports = function (config) {
type: "wasm",
nocache: true
},
{
pattern: "../../node_modules/@waku/zerokit-rln-wasm-utils/*.wasm",
included: false,
served: true,
watched: false,
type: "wasm",
nocache: true
},
{
// Fleet info is written by the integration test runner
pattern: "fleet-info.json",
@ -83,7 +75,7 @@ module.exports = function (config) {
mime: {
"application/wasm": ["wasm"],
"application/octet-stream": ["zkey"]
"application/octet-stream": ["arkzkey"]
},
customHeaders: [
@ -93,7 +85,7 @@ module.exports = function (config) {
value: "application/wasm"
},
{
match: ".*\\.zkey$",
match: ".*\\.arkzkey$",
name: "Content-Type",
value: "application/octet-stream"
}
@ -106,16 +98,10 @@ module.exports = function (config) {
__dirname,
"../../node_modules/@waku/zerokit-rln-wasm/rln_wasm_bg.wasm"
),
"/base/rln_wasm_utils_bg.wasm":
"/absolute" +
path.resolve(
__dirname,
"../../node_modules/@waku/zerokit-rln-wasm-utils/rln_wasm_utils_bg.wasm"
),
"/base/rln.wasm":
"/absolute" + path.resolve(__dirname, "src/resources/rln.wasm"),
"/base/rln_final.zkey":
"/absolute" + path.resolve(__dirname, "src/resources/rln_final.zkey")
"/base/rln_final.arkzkey":
"/absolute" + path.resolve(__dirname, "src/resources/rln_final.arkzkey")
},
webpack: {
@ -146,7 +132,7 @@ module.exports = function (config) {
}
},
{
test: /\.zkey$/,
test: /\.arkzkey$/,
type: "asset/resource",
generator: {
filename: "[name][ext]"

View File

@ -85,8 +85,7 @@
"@wagmi/core": "^2.22.1",
"@waku/core": "^0.0.40",
"@waku/utils": "^0.0.27",
"@waku/zerokit-rln-wasm": "^0.2.1",
"@waku/zerokit-rln-wasm-utils": "^0.1.0",
"@waku/zerokit-rln-wasm": "^1.0.0",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0",

View File

@ -84,7 +84,7 @@ export class RLNEncoder implements IEncoder {
0 // TODO: need to track messages sent per epoch
);
return new Proof(proof, epoch, rlnIdentifier);
return new Proof(proof.toBytesLE(), epoch, rlnIdentifier);
}
public get pubsubTopic(): string {

View File

@ -3,6 +3,7 @@ import { publicActions } from "viem";
import { RLN_CONTRACT } from "./contract/constants.js";
import { RLNBaseContract } from "./contract/rln_base_contract.js";
import { IdentityCredential } from "./identity.js";
import { Keystore } from "./keystore/index.js";
import type {
DecryptedCredentials,
@ -111,9 +112,10 @@ export class RLNCredentialsManager {
if ("signature" in options) {
log.info("Using Zerokit to generate identity");
identity = this.zerokit.generateSeededIdentityCredential(
const extendedIdentity = this.zerokit.generateSeededIdentityCredential(
options.signature
);
identity = IdentityCredential.fromBytes(extendedIdentity.toBytesLE());
}
if (!identity) {

View File

@ -133,7 +133,7 @@ describe("RLN Proof Unit Tests", function () {
);
// Parse proof bytes into Proof class
const parsedProof = new Proof(proof, epoch, rlnIdentifier);
const parsedProof = new Proof(proof.toBytesLE(), 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>
@ -154,7 +154,7 @@ describe("RLN Proof Unit Tests", function () {
// Verify round-trip: proofToBytes should reconstruct original bytes
const reconstructedBytes = proofToBytes(parsedProof);
expect(reconstructedBytes).to.deep.equal(
proof,
proof.toBytesLE(),
"Reconstructed bytes should match original"
);

Binary file not shown.

View File

@ -1,6 +1,5 @@
import { Logger } from "@waku/utils";
import init, * as zerokitRLN from "@waku/zerokit-rln-wasm";
import initUtils from "@waku/zerokit-rln-wasm-utils";
import init, { WasmRLN } from "@waku/zerokit-rln-wasm";
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
import { RLNCredentialsManager } from "./credentials_manager.js";
@ -17,14 +16,12 @@ export class RLNInstance extends RLNCredentialsManager {
*/
public static async create(): Promise<RLNInstance> {
try {
await initUtils();
await init();
zerokitRLN.initPanicHook();
const witnessCalculator = await RLNInstance.loadWitnessCalculator();
const zkey = await RLNInstance.loadZkey();
const zkRLN = zerokitRLN.newRLN(zkey);
const zkRLN = new WasmRLN(zkey);
const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT);
return new RLNInstance(zerokit);
@ -63,7 +60,7 @@ export class RLNInstance extends RLNCredentialsManager {
public static async loadZkey(): Promise<Uint8Array> {
try {
const url = new URL("./resources/rln_final.zkey", import.meta.url);
const url = new URL("./resources/rln_final.arkzkey", import.meta.url);
const response = await fetch(url);
if (!response.ok) {

View File

@ -1,18 +0,0 @@
import { hash, poseidonHash as poseidon } from "@waku/zerokit-rln-wasm-utils";
import { BytesUtils } from "./bytes.js";
export function poseidonHash(...input: Array<Uint8Array>): Uint8Array {
const inputLen = BytesUtils.writeUIntLE(
new Uint8Array(8),
input.length,
0,
8
);
const lenPrefixedData = BytesUtils.concatenate(inputLen, ...input);
return poseidon(lenPrefixedData, true);
}
export function sha256(input: Uint8Array): Uint8Array {
return hash(input, true);
}

View File

@ -1,6 +1,5 @@
export { createViemClientFromWindow, RpcClient } from "./rpcClient.js";
export { BytesUtils } from "./bytes.js";
export { sha256, poseidonHash } from "./hash.js";
export {
dateToEpoch,
epochIntToBytes,

View File

@ -1,5 +1,6 @@
import { Hasher, WasmFr } from "@waku/zerokit-rln-wasm";
import { BytesUtils } from "./bytes.js";
import { poseidonHash } from "./hash.js";
/**
* The fixed depth of the Merkle tree used in the RLN contract
@ -26,23 +27,27 @@ export function reconstructMerkleRoot(
);
}
let currentValue = BytesUtils.bytes32FromBigInt(leafValue);
let currentValue = WasmFr.fromBytesLE(
BytesUtils.bytes32FromBigInt(leafValue)
);
for (let level = 0; level < MERKLE_TREE_DEPTH; level++) {
const bit = (leafIndex >> BigInt(level)) & 1n;
const proofBytes = BytesUtils.bytes32FromBigInt(proof[level]);
const proofFr = WasmFr.fromBytesLE(
BytesUtils.bytes32FromBigInt(proof[level])
);
if (bit === 0n) {
// Current node is a left child: hash(current, proof[level])
currentValue = poseidonHash(currentValue, proofBytes);
currentValue = Hasher.poseidonHashPair(currentValue, proofFr);
} else {
// Current node is a right child: hash(proof[level], current)
currentValue = poseidonHash(proofBytes, currentValue);
currentValue = Hasher.poseidonHashPair(proofFr, currentValue);
}
}
return BytesUtils.toBigInt(currentValue, "little");
return BytesUtils.toBigInt(currentValue.toBytesLE(), "little");
}
/**
@ -60,8 +65,11 @@ export function calculateRateCommitment(
const idBytes = BytesUtils.bytes32FromBigInt(idCommitment);
const rateLimitBytes = BytesUtils.bytes32FromBigInt(rateLimit);
const hashResult = poseidonHash(idBytes, rateLimitBytes);
return BytesUtils.toBigInt(hashResult, "little");
const hashResult = Hasher.poseidonHashPair(
WasmFr.fromBytesLE(idBytes),
WasmFr.fromBytesLE(rateLimitBytes)
);
return BytesUtils.toBigInt(hashResult.toBytesLE(), "little");
}
/**

View File

@ -10,17 +10,29 @@ describe("@waku/rln", () => {
const memKeys1 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
const memKeys2 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
memKeys1.IDCommitment.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDCommitment[index]);
});
memKeys1.IDNullifier.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDNullifier[index]);
});
memKeys1.IDSecretHash.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDSecretHash[index]);
});
memKeys1.IDTrapdoor.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDTrapdoor[index]);
});
memKeys1
.getCommitment()
.toBytesLE()
.forEach((element, index) => {
expect(element).to.equal(memKeys2.getCommitment().toBytesLE()[index]);
});
memKeys1
.getNullifier()
.toBytesLE()
.forEach((element, index) => {
expect(element).to.equal(memKeys2.getNullifier().toBytesLE()[index]);
});
memKeys1
.getSecretHash()
.toBytesLE()
.forEach((element, index) => {
expect(element).to.equal(memKeys2.getSecretHash().toBytesLE()[index]);
});
memKeys1
.getTrapdoor()
.toBytesLE()
.forEach((element, index) => {
expect(element).to.equal(memKeys2.getTrapdoor().toBytesLE()[index]);
});
});
});

View File

@ -1,17 +1,21 @@
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
import { generateSeededExtendedMembershipKey } from "@waku/zerokit-rln-wasm-utils";
import {
ExtendedIdentity,
Hasher,
VecWasmFr,
WasmFr,
WasmRLN,
WasmRLNProof,
WasmRLNWitnessInput
} from "@waku/zerokit-rln-wasm";
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 { dateToEpochBytes } from "./utils/epoch.js";
import { poseidonHash, sha256 } from "./utils/hash.js";
import { MERKLE_TREE_DEPTH } from "./utils/merkle.js";
export class Zerokit {
public constructor(
private readonly zkRLN: number,
private readonly zkRLN: WasmRLN,
private readonly witnessCalculator: WitnessCalculator,
public readonly rateLimit: number = DEFAULT_RATE_LIMIT,
public readonly rlnIdentifier: Uint8Array = (() => {
@ -22,63 +26,14 @@ export class Zerokit {
})()
) {}
public get getZkRLN(): number {
return this.zkRLN;
}
public get getWitnessCalculator(): WitnessCalculator {
return this.witnessCalculator;
}
public generateSeededIdentityCredential(seed: string): IdentityCredential {
public generateSeededIdentityCredential(seed: string): ExtendedIdentity {
const stringEncoder = new TextEncoder();
const seedBytes = stringEncoder.encode(seed);
const memKeys = generateSeededExtendedMembershipKey(seedBytes, true);
return IdentityCredential.fromBytes(memKeys);
}
private async serializeWitness(
idSecretHash: Uint8Array,
pathElements: Uint8Array[],
identityPathIndex: Uint8Array[],
msg: Uint8Array,
epoch: Uint8Array,
rateLimit: number,
messageId: number // number of message sent by the user in this epoch
): Promise<Uint8Array> {
const externalNullifier = poseidonHash(
sha256(epoch),
sha256(this.rlnIdentifier)
);
const pathElementsBytes = new Uint8Array(8 + pathElements.length * 32);
BytesUtils.writeUIntLE(pathElementsBytes, pathElements.length, 0, 8);
for (let i = 0; i < pathElements.length; i++) {
// We assume that the path elements are already in little-endian format
pathElementsBytes.set(pathElements[i], 8 + i * 32);
}
const identityPathIndexBytes = new Uint8Array(
8 + identityPathIndex.length * 1
);
BytesUtils.writeUIntLE(
identityPathIndexBytes,
identityPathIndex.length,
0,
8
);
for (let i = 0; i < identityPathIndex.length; i++) {
// We assume that each identity path index is already in little-endian format
identityPathIndexBytes.set(identityPathIndex[i], 8 + i * 1);
}
const x = sha256(msg);
return BytesUtils.concatenate(
idSecretHash,
BytesUtils.writeUIntLE(new Uint8Array(32), rateLimit, 0, 32),
BytesUtils.writeUIntLE(new Uint8Array(32), messageId, 0, 32),
pathElementsBytes,
identityPathIndexBytes,
x,
externalNullifier
);
return ExtendedIdentity.generateSeeded(seedBytes);
}
public async generateRLNProof(
@ -90,7 +45,7 @@ export class Zerokit {
rateLimit: number,
messageId: number // number of message sent by the user in this epoch
): Promise<{
proof: Uint8Array;
proof: WasmRLNProof;
epoch: Uint8Array;
rlnIdentifier: Uint8Array;
}> {
@ -120,26 +75,37 @@ export class Zerokit {
`messageId must be an integer between 0 and ${rateLimit - 1}, got ${messageId}`
);
}
const serializedWitness = await this.serializeWitness(
idSecretHash,
pathElements,
identityPathIndex,
msg,
epoch,
rateLimit,
messageId
const pathElementsVec = new VecWasmFr();
for (const element of pathElements) {
pathElementsVec.push(WasmFr.fromBytesLE(element));
}
const identityPathIndexBytes = new Uint8Array(identityPathIndex.length);
for (let i = 0; i < identityPathIndex.length; i++) {
// We assume that each identity path index is already in little-endian format
identityPathIndexBytes.set(identityPathIndex[i], i);
}
const x = Hasher.hashToFieldLE(msg);
const externalNullifier = Hasher.poseidonHashPair(
Hasher.hashToFieldLE(epoch),
Hasher.hashToFieldLE(this.rlnIdentifier)
);
const witnessJson: Record<string, unknown> = zerokitRLN.rlnWitnessToJson(
this.zkRLN,
serializedWitness
) as Record<string, unknown>;
const witness = new WasmRLNWitnessInput(
WasmFr.fromBytesLE(idSecretHash),
WasmFr.fromUint(rateLimit),
WasmFr.fromUint(messageId),
pathElementsVec,
identityPathIndexBytes,
x,
externalNullifier
);
const calculatedWitness: bigint[] =
await this.witnessCalculator.calculateWitness(witnessJson);
const proof = zerokitRLN.generateRLNProofWithWitness(
this.zkRLN,
await this.witnessCalculator.calculateWitness(
witness.toBigIntJson() as Record<string, unknown>
);
const proof = this.zkRLN.generateRLNProofWithWitness(
calculatedWitness,
serializedWitness
witness
);
return {
proof,
@ -151,21 +117,21 @@ export class Zerokit {
public verifyRLNProof(
signalLength: Uint8Array,
signal: Uint8Array,
proof: Uint8Array,
proof: WasmRLNProof,
roots: Uint8Array[]
): boolean {
if (signalLength.length !== 8)
throw new Error("signalLength must be 8 bytes");
if (proof.length !== 288) throw new Error("proof must be 288 bytes");
if (roots.length == 0) throw new Error("roots array is empty");
if (roots.find((root) => root.length !== 32)) {
throw new Error("All roots must be 32 bytes");
}
return zerokitRLN.verifyWithRoots(
this.zkRLN,
BytesUtils.concatenate(proof, signalLength, signal),
BytesUtils.concatenate(...roots)
);
const rootsVec = new VecWasmFr();
for (const root of roots) {
rootsVec.push(WasmFr.fromBytesLE(root));
}
const x = Hasher.hashToFieldLE(signal);
return this.zkRLN.verifyWithRoots(proof, rootsVec, x);
}
}