@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
text/typescript
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)
}
}