UNPKG

@cryptkeeperzk/rlnjs

Version:

Client library for generating and using RLN ZK proofs, is a forked repo from main Rate-Limiting-Nullifier/rlnjs to make it work with Cryptkeeper Browser Extension using `@cryptkeeperzk/snarkjs` and `@cryptkeeperzk/ffjavascript`

273 lines (236 loc) 10.5 kB
import { Group } from '@semaphore-protocol/group' import { MerkleProof } from './types' import { DEFAULT_MERKLE_TREE_DEPTH, calculateRateCommitment } from './common' import { RLNContract } from './contract-wrapper' import { ethers } from 'ethers' import poseidon from 'poseidon-lite' import { WithdrawProver } from './circuit-wrapper' export interface IRLNRegistry { isRegistered(identityCommitment: bigint): Promise<boolean> getMerkleRoot(): Promise<bigint> getMessageLimit(identityCommitment: bigint): Promise<bigint> getRateCommitment(identityCommitment: bigint): Promise<bigint> getAllRateCommitments(): Promise<bigint[]> generateMerkleProof(identityCommitment: bigint): Promise<MerkleProof> register(identityCommitment: bigint, messageLimit: bigint): Promise<void> withdraw(identitySecret: bigint): Promise<void> releaseWithdrawal(identityCommitment: bigint): Promise<void> slash(identitySecret: bigint, receiver?: string): Promise<void> } export class ContractRLNRegistry implements IRLNRegistry { private rlnContract: RLNContract // the withdrawProver allows user to generate withdraw proof, which is verified in the RLN contract private withdrawProver?: WithdrawProver private rlnIdentifier: bigint private treeDepth: number constructor(args: { rlnIdentifier: bigint, rlnContract: RLNContract, treeDepth?: number, withdrawWasmFilePath?: string | Uint8Array, withdrawFinalZkeyPath?: string | Uint8Array, }) { this.treeDepth = args.treeDepth ? args.treeDepth : DEFAULT_MERKLE_TREE_DEPTH this.rlnContract = args.rlnContract this.rlnIdentifier = args.rlnIdentifier if (args.withdrawWasmFilePath !== undefined && args.withdrawFinalZkeyPath !== undefined) { this.withdrawProver = new WithdrawProver(args.withdrawWasmFilePath, args.withdrawFinalZkeyPath) } } async getSignerAddress(): Promise<string> { return this.rlnContract.getSignerAddress() } async isRegistered(identityCommitment: bigint): Promise<boolean> { return this.rlnContract.isRegistered(identityCommitment) } async getMessageLimit(identityCommitment: bigint): Promise<bigint> { const user = await this.rlnContract.getUser(identityCommitment) if (user.userAddress === ethers.ZeroAddress) { throw new Error('Identity commitment is not registered') } return user.messageLimit } async getRateCommitment(identityCommitment: bigint): Promise<bigint> { const messageLimit = await this.getMessageLimit(identityCommitment) return calculateRateCommitment(identityCommitment, messageLimit) } private async generateLatestGroup(): Promise<Group> { const group = new Group(this.rlnIdentifier, this.treeDepth) const events = await this.rlnContract.getLogs() for (const event of events) { if (event.name === 'MemberRegistered') { const identityCommitment = BigInt(event.identityCommitment) const messageLimit = BigInt(event.messageLimit) const rateCommitment = calculateRateCommitment(identityCommitment, messageLimit) group.addMember(rateCommitment) } else if (event.name === 'MemberWithdrawn' || event.name === 'MemberSlashed') { const index = event.index group.removeMember(Number(index)) } } return group } async getAllRateCommitments(): Promise<bigint[]> { const group = await this.generateLatestGroup() return group.members.map((member) => BigInt(member)) } async getMerkleRoot(): Promise<bigint> { const group = await this.generateLatestGroup() return BigInt(group.root) } /** * Creates a Merkle Proof. * @param identityCommitment The leaf for which Merkle proof should be created. * @returns The Merkle proof. */ async generateMerkleProof(identityCommitment: bigint): Promise<MerkleProof> { const group = await this.generateLatestGroup() const user = await this.rlnContract.getUser(identityCommitment) if (user.userAddress === ethers.ZeroAddress) { throw new Error('Identity commitment is not registered') } const rateCommitment = calculateRateCommitment(identityCommitment, user.messageLimit) const index = group.indexOf(rateCommitment) if (index === -1) { // Should only happen when a user was registered before `const user = ...` and then withdraw/slashed // after `const user = ...`. throw new Error('Rate commitment is not in the merkle tree') } return group.generateMerkleProof(index) } async register(identityCommitment: bigint, messageLimit: bigint): Promise<void> { if (await this.isRegistered(identityCommitment)) { throw new Error('Identity commitment is already registered') } await this.rlnContract.register(identityCommitment, messageLimit) } async withdraw(identitySecret: bigint): Promise<void> { if (this.withdrawProver === undefined) { throw new Error('Withdraw prover is not initialized') } const identityCommitment = poseidon([identitySecret]) const user = await this.rlnContract.getUser(identityCommitment) if (user.userAddress === ethers.ZeroAddress) { throw new Error('Identity commitment is not registered') } const userAddressBigInt = BigInt(user.userAddress) const proof = await this.withdrawProver.generateProof({ identitySecret, address: userAddressBigInt, }) await this.rlnContract.withdraw(identityCommitment, proof.proof) } async releaseWithdrawal(identityCommitment: bigint): Promise<void> { if (!await this.isRegistered(identityCommitment)) { throw new Error('Identity commitment is not registered') } const withdrawal = await this.rlnContract.getWithdrawal(identityCommitment) if (withdrawal.blockNumber == BigInt(0)) { throw new Error('Withdrawal is not initiated') } await this.rlnContract.release(identityCommitment) } async slash(identitySecret: bigint, receiver?: string): Promise<void> { if (this.withdrawProver === undefined) { throw new Error('Withdraw prover is not initialized') } const identityCommitment = poseidon([identitySecret]) receiver = receiver ? receiver : await this.rlnContract.getSignerAddress() const receiverBigInt = BigInt(receiver) const proof = await this.withdrawProver.generateProof({ identitySecret, address: receiverBigInt, }) await this.rlnContract.slash(identityCommitment, receiver, proof.proof) } } export class MemoryRLNRegistry implements IRLNRegistry { // map of identityCommitment -> messageLimit private mapIsWithdrawing: Map<string, boolean> private mapMessageLimit: Map<string, bigint> private group: Group constructor( readonly rlnIdentifier: bigint, readonly treeDepth: number = DEFAULT_MERKLE_TREE_DEPTH, ) { this.mapIsWithdrawing = new Map<string, boolean>() this.mapMessageLimit = new Map<string, bigint>() this.group = new Group(this.rlnIdentifier, this.treeDepth) } async isRegistered(identityCommitment: bigint): Promise<boolean> { const messageLimit = this.mapMessageLimit.get(identityCommitment.toString()) return messageLimit !== undefined } async getMerkleRoot(): Promise<bigint> { return BigInt(this.group.root) } async getMessageLimit(identityCommitment: bigint): Promise<bigint> { const messageLimit = this.mapMessageLimit.get(identityCommitment.toString()) if (messageLimit === undefined) { throw new Error('Identity commitment is not registered') } return messageLimit } async getRateCommitment(identityCommitment: bigint): Promise<bigint> { const messageLimit = await this.getMessageLimit(identityCommitment) return calculateRateCommitment(identityCommitment, messageLimit) } async getAllRateCommitments(): Promise<bigint[]> { return this.group.members.map((member) => BigInt(member)) } async generateMerkleProof(identityCommitment: bigint): Promise<MerkleProof> { const rateCommitment = await this.getRateCommitment(identityCommitment) const index = this.group.indexOf(rateCommitment) if (index === -1) { // Sanity check throw new Error('Rate commitment is not in the merkle tree. This should never happen.') } return this.group.generateMerkleProof(index) } async register(identityCommitment: bigint, messageLimit: bigint): Promise<void> { if (await this.isRegistered(identityCommitment)) { throw new Error('Identity commitment is already registered') } this.mapMessageLimit.set(identityCommitment.toString(), messageLimit) const rateCommitment = await this.getRateCommitment(identityCommitment) this.group.addMember(rateCommitment) } async withdraw(identitySecret: bigint): Promise<void> { const identityCommitment = poseidon([identitySecret]) if (!await this.isRegistered(identityCommitment)) { throw new Error('Identity commitment is not registered') } const isWithdrawing = this.mapIsWithdrawing.get(identityCommitment.toString()) if (isWithdrawing !== undefined) { throw new Error('Identity is already withdrawing') } this.mapIsWithdrawing.set(identityCommitment.toString(), true) } async releaseWithdrawal(identityCommitment: bigint): Promise<void> { const rateCommitment = await this.getRateCommitment(identityCommitment) const index = this.group.indexOf(rateCommitment) if (index === -1) { // Sanity check throw new Error('Rate commitment is not in the merkle tree. This should never happen') } const isWithdrawing = this.mapIsWithdrawing.get(identityCommitment.toString()) if (isWithdrawing === undefined) { throw new Error('Identity is not withdrawing') } this.mapIsWithdrawing.delete(identityCommitment.toString()) this.mapMessageLimit.delete(identityCommitment.toString()) this.group.removeMember(index) } async slash(identitySecret: bigint, _?: string): Promise<void> { const identityCommitment = poseidon([identitySecret]) const rateCommitment = await this.getRateCommitment(identityCommitment) const index = this.group.indexOf(rateCommitment) if (index === -1) { // Sanity check throw new Error('Rate commitment is not in the merkle tree. This should never happen') } this.mapIsWithdrawing.delete(identityCommitment.toString()) this.mapMessageLimit.delete(identityCommitment.toString()) this.group.removeMember(index) } }