Files
zerokit/rln-wasm/examples/index.js
Vinh Trịnh 77a8d28965 feat: unify RLN types, refactor public APIs, add full (de)serialization, align FFI/WASM/APIs, simplify errors, update docs/examples, and clean up zerokit (#355)
# Changes

- Unified the `RLN` struct and core protocol types across public, FFI,
and WASM so everything works consistently.
- Fully refactored `protocol.rs` and `public.rs` to clean up the API
surface and make the flow easier to work with.
- Added (de)serialization for `RLN_Proof` and `RLN_ProofValues`, and
matched all C, Nim, WASM, and Node.js examples.
- Aligned FFI and WASM behavior, added missing APIs, and standardized
how witness are created and passed around.
- Reworked the error types, added clearer verification messages, and
simplified the overall error structure.
- Updated variable names, README, Rust docs, and examples across the
repo, updated outdated RLN RFC link.
- Refactored `rln-cli` to use the new public API, removed
serialize-based cli example, and dropped the `eyre` crate.
- Bumped dependencies, fixed CI, fixed `+atomic` flags for latest
nightly Rust and added `Clippy.toml` for better fmt.
- Added a `prelude.rs` file for easier use, cleaned up public access for
types and types import across zerokit modules.
- Separated keygen, proof handling, slashing logic, and witness into
protocol folder.
2025-12-09 19:03:04 +07:00

308 lines
9.8 KiB
JavaScript

import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
function debugUint8Array(uint8Array) {
return Array.from(uint8Array, (byte) =>
byte.toString(16).padStart(2, "0")
).join(", ");
}
async function calculateWitness(circomPath, inputs, witnessCalculatorFile) {
const wasmFile = readFileSync(circomPath);
const wasmFileBuffer = wasmFile.slice(
wasmFile.byteOffset,
wasmFile.byteOffset + wasmFile.byteLength
);
const witnessCalculator = await witnessCalculatorFile(wasmFileBuffer);
const calculatedWitness = await witnessCalculator.calculateWitness(
inputs,
false
);
return calculatedWitness;
}
async function main() {
const rlnWasm = await import("../pkg/rln_wasm.js");
const wasmPath = join(__dirname, "../pkg/rln_wasm_bg.wasm");
const wasmBytes = readFileSync(wasmPath);
rlnWasm.initSync({ module: wasmBytes });
const zkeyPath = join(
__dirname,
"../../rln/resources/tree_depth_20/rln_final.arkzkey"
);
const circomPath = join(
__dirname,
"../../rln/resources/tree_depth_20/rln.wasm"
);
const witnessCalculatorPath = join(
__dirname,
"../resources/witness_calculator.js"
);
const { builder: witnessCalculatorFile } = await import(
witnessCalculatorPath
);
console.log("Creating RLN instance");
const zkeyData = readFileSync(zkeyPath);
const rlnInstance = new rlnWasm.WasmRLN(new Uint8Array(zkeyData));
console.log("RLN instance created successfully");
console.log("\nGenerating identity keys");
const identity = rlnWasm.Identity.generate();
const identitySecret = identity.getSecretHash();
const idCommitment = identity.getCommitment();
console.log("Identity generated");
console.log(" - identity_secret = " + identitySecret.debug());
console.log(" - id_commitment = " + idCommitment.debug());
console.log("\nCreating message limit");
const userMessageLimit = rlnWasm.WasmFr.fromUint(1);
console.log(" - user_message_limit = " + userMessageLimit.debug());
console.log("\nComputing rate commitment");
const rateCommitment = rlnWasm.Hasher.poseidonHashPair(
idCommitment,
userMessageLimit
);
console.log(" - rate_commitment = " + rateCommitment.debug());
console.log("\nWasmFr serialization: WasmFr <-> bytes");
const serRateCommitment = rateCommitment.toBytesLE();
console.log(
" - serialized rate_commitment = [" +
debugUint8Array(serRateCommitment) +
"]"
);
const deserRateCommitment = rlnWasm.WasmFr.fromBytesLE(serRateCommitment);
console.log(
" - deserialized rate_commitment = " + deserRateCommitment.debug()
);
console.log("\nBuilding Merkle path for stateless mode");
const treeDepth = 20;
const defaultLeaf = rlnWasm.WasmFr.zero();
const defaultHashes = [];
defaultHashes[0] = rlnWasm.Hasher.poseidonHashPair(defaultLeaf, defaultLeaf);
for (let i = 1; i < treeDepth - 1; i++) {
defaultHashes[i] = rlnWasm.Hasher.poseidonHashPair(
defaultHashes[i - 1],
defaultHashes[i - 1]
);
}
const pathElements = new rlnWasm.VecWasmFr();
pathElements.push(defaultLeaf);
for (let i = 1; i < treeDepth; i++) {
pathElements.push(defaultHashes[i - 1]);
}
const identityPathIndex = new Uint8Array(treeDepth);
console.log("\nVecWasmFr serialization: VecWasmFr <-> bytes");
const serPathElements = pathElements.toBytesLE();
console.log(
" - serialized path_elements = [" + debugUint8Array(serPathElements) + "]"
);
const deserPathElements = rlnWasm.VecWasmFr.fromBytesLE(serPathElements);
console.log(" - deserialized path_elements = ", deserPathElements.debug());
console.log("\nUint8Array serialization: Uint8Array <-> bytes");
const serPathIndex = rlnWasm.Uint8ArrayUtils.toBytesLE(identityPathIndex);
console.log(
" - serialized path_index = [" + debugUint8Array(serPathIndex) + "]"
);
const deserPathIndex = rlnWasm.Uint8ArrayUtils.fromBytesLE(serPathIndex);
console.log(" - deserialized path_index =", deserPathIndex);
console.log("\nComputing Merkle root for stateless mode");
console.log(" - computing root for index 0 with rate_commitment");
let computedRoot = rlnWasm.Hasher.poseidonHashPair(
rateCommitment,
defaultLeaf
);
for (let i = 1; i < treeDepth; i++) {
computedRoot = rlnWasm.Hasher.poseidonHashPair(
computedRoot,
defaultHashes[i - 1]
);
}
console.log(" - computed_root = " + computedRoot.debug());
console.log("\nHashing signal");
const signal = new Uint8Array([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0,
]);
const x = rlnWasm.Hasher.hashToFieldLE(signal);
console.log(" - x = " + x.debug());
console.log("\nHashing epoch");
const epochStr = "test-epoch";
const epoch = rlnWasm.Hasher.hashToFieldLE(
new TextEncoder().encode(epochStr)
);
console.log(" - epoch = " + epoch.debug());
console.log("\nHashing RLN identifier");
const rlnIdStr = "test-rln-identifier";
const rlnIdentifier = rlnWasm.Hasher.hashToFieldLE(
new TextEncoder().encode(rlnIdStr)
);
console.log(" - rln_identifier = " + rlnIdentifier.debug());
console.log("\nComputing Poseidon hash for external nullifier");
const externalNullifier = rlnWasm.Hasher.poseidonHashPair(
epoch,
rlnIdentifier
);
console.log(" - external_nullifier = " + externalNullifier.debug());
console.log("\nCreating message_id");
const messageId = rlnWasm.WasmFr.fromUint(0);
console.log(" - message_id = " + messageId.debug());
console.log("\nCreating RLN Witness");
const witness = new rlnWasm.WasmRLNWitnessInput(
identitySecret,
userMessageLimit,
messageId,
pathElements,
identityPathIndex,
x,
externalNullifier
);
console.log("RLN Witness created successfully");
console.log("\nCalculating witness");
const witnessJson = witness.toBigIntJson();
const calculatedWitness = await calculateWitness(
circomPath,
witnessJson,
witnessCalculatorFile
);
console.log("Witness calculated successfully");
console.log("\nGenerating RLN Proof");
const rln_proof = rlnInstance.generateRLNProofWithWitness(
calculatedWitness,
witness
);
console.log("Proof generated successfully");
console.log("\nGetting proof values");
const proofValues = rln_proof.getValues();
console.log(" - y = " + proofValues.y.debug());
console.log(" - nullifier = " + proofValues.nullifier.debug());
console.log(" - root = " + proofValues.root.debug());
console.log(" - x = " + proofValues.x.debug());
console.log(
" - external_nullifier = " + proofValues.externalNullifier.debug()
);
console.log("\nRLNProof serialization: RLNProof <-> bytes");
const serProof = rln_proof.toBytesLE();
console.log(" - serialized proof = [" + debugUint8Array(serProof) + " ]");
const deserProof = rlnWasm.WasmRLNProof.fromBytesLE(serProof);
console.log(" - proof deserialized successfully");
console.log("\nRLNProofValues serialization: RLNProofValues <-> bytes");
const serProofValues = proofValues.toBytesLE();
console.log(
" - serialized proof_values = [" + debugUint8Array(serProofValues) + " ]"
);
const deserProofValues2 =
rlnWasm.WasmRLNProofValues.fromBytesLE(serProofValues);
console.log(" - proof_values deserialized successfully");
console.log(
" - deserialized external_nullifier = " +
deserProofValues2.externalNullifier.debug()
);
console.log("\nVerifying Proof");
const roots = new rlnWasm.VecWasmFr();
roots.push(computedRoot);
const isValid = rlnInstance.verifyWithRoots(rln_proof, roots, x);
if (isValid) {
console.log("Proof verified successfully");
} else {
console.log("Proof verification failed");
return;
}
console.log(
"\nSimulating double-signaling attack (same epoch, different message)"
);
console.log("\nHashing second signal");
const signal2 = new Uint8Array([
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]);
const x2 = rlnWasm.Hasher.hashToFieldLE(signal2);
console.log(" - x2 = " + x2.debug());
console.log("\nCreating second message with the same id");
const messageId2 = rlnWasm.WasmFr.fromUint(0);
console.log(" - message_id2 = " + messageId2.debug());
console.log("\nCreating second RLN Witness");
const witness2 = new rlnWasm.WasmRLNWitnessInput(
identitySecret,
userMessageLimit,
messageId2,
pathElements,
identityPathIndex,
x2,
externalNullifier
);
console.log("Second RLN Witness created successfully");
console.log("\nCalculating second witness");
const witnessJson2 = witness2.toBigIntJson();
const calculatedWitness2 = await calculateWitness(
circomPath,
witnessJson2,
witnessCalculatorFile
);
console.log("Second witness calculated successfully");
console.log("\nGenerating second RLN Proof");
const rln_proof2 = rlnInstance.generateRLNProofWithWitness(
calculatedWitness2,
witness2
);
console.log("Second proof generated successfully");
console.log("\nVerifying second proof");
const isValid2 = rlnInstance.verifyWithRoots(rln_proof2, roots, x2);
if (isValid2) {
console.log("Second proof verified successfully");
console.log("\nRecovering identity secret");
const proofValues1 = rln_proof.getValues();
const proofValues2 = rln_proof2.getValues();
const recoveredSecret = rlnWasm.WasmRLNProofValues.recoverIdSecret(
proofValues1,
proofValues2
);
console.log(" - recovered_secret = " + recoveredSecret.debug());
console.log(" - original_secret = " + identitySecret.debug());
console.log("Slashing successful: Identity is recovered!");
} else {
console.log("Second proof verification failed");
}
}
main().catch(console.error);