Kevin Mai-Husan Chia 75a73ea0ee Merge pull request #88 from mhchia/feat/update-rln-contract
build: upgrade to new contract
2023-08-31 19:02:56 +08:00
2023-02-24 17:46:08 +08:00
2023-07-10 01:26:59 +08:00
2023-08-16 17:10:58 +08:00
2023-08-16 17:10:58 +08:00
2023-02-06 17:42:58 +00:00
2023-08-16 21:02:42 +08:00
2023-02-06 17:41:43 +00:00
2023-07-10 01:05:21 +08:00
2023-07-10 01:05:21 +08:00
2023-07-10 01:03:46 +08:00

Rate Limiting Nullifier Javascript / Typescript Library

Contents

Description

RLN (Rate-Limiting Nullifier) is a zk-gadget/protocol that enables spam prevention in anonymous environments.

The core of RLN is in the circuit logic, documentation here. RLNjs provides easy management of the registry and proof creation.

RLN class is the core of RLNjs. It allows user to generate proofs, verify proofs, and detect spams. Also, user can register to RLN, withdraw, and slash spammers.

Tests Ran on an M2 Macbook Time
RLN Proof ~800ms
RLN Proof Verification ~130ms
Withdraw Proof ~260ms
Withdraw Proof Verification ~145ms

Install

Install rlnjs with npm:

npm install rlnjs

Build circuits and get the parameter files

Circuit parameter files circuit.wasm, final.zkey, and verification_key.json are needed when instantiating a RLN instance. You can choose to build the circuits with script or manually.

Run the script scripts/build-zkeys.sh and it will build the circuits for you.

./scripts/build-zkeys.sh

In the project root, you should can see the zkey files in the ./zkeyFiles folder.

$ tree ./zkeyFiles
zkeyFiles
├── rln
│   ├── circuit.wasm
│   ├── final.zkey
│   └── verification_key.json
└── withdraw
    ├── circuit.wasm
    ├── final.zkey
    └── verification_key.json

Manually clone and build the circuits

Circom needs to be installed, please see this link for installation instructions.

git clone https://github.com/Rate-Limiting-Nullifier/circom-rln.git &&
cd circom-rln

Make sure the depth of the merkle tree are the same in both rlnjs and rln-circuits, otherwise verification will fail. You need to rebuild the circuits every time the circuit is changed.

npm i &&
npm run build

The previous step should have produced the following files:

$ tree zkeyFiles
zkeyFiles
├── rln
│   ├── circuit.config.toml
│   ├── circuit.wasm
│   ├── final.zkey
│   └── verification_key.json
└── withdraw
    ├── circuit.config.toml
    ├── circuit.wasm
    ├── final.zkey
    └── verification_key.json

Usage

Initializing an RLN instance

Create RLN instance with the contract registry

The following snippet creates RLN instance with default settings.

const rlnIdentifier = BigInt(5566)
const contractAddress = "0x..."
const contractAtBlock = 12345678
const provider = new ethers.JsonRpcProvider(url)
const signer = await provider.getSigner(0)

// Create an RLN instance with the contract registry.
// ethers provider and the contract address are both required then.
const rln = RLN.createWithContractRegistry({
  /* These parameters are required */
  rlnIdentifier, // The unique id representing your application
  provider, // ethers.js provider
  contractAddress, // RLN contract address

  /* These parameters are optional */
  contractAtBlock, // The block number at which the RLN contract was deployed. If not given, default is 0
  signer, // ethers.js signer. If not given, users won't be able to execute write operations to the RLN contract
  // ... See all optional parameters in RLN constructor in src/rln.ts
})

Custom options can be passed to the RLN instance. Note that default tree depth is 20. If you're using a tree depth other than 20, you need to the circuit parameters when creating the RLN instance. See RLN constructor for all options.

import path from "path"

import { ethers } from "ethers"
import { Identity } from '@semaphore-protocol/identity'
import { RLN, IRLNRegsitry, ContractRLNRegistry } from "rlnjs"

// Assume you have built `rln.circom` and `withdraw.circom` and have placed them under the folder ./zkeyFiles/rln
// and ./zkeyFiles/withdraw respectively.

/* rln circuit parameters */
const rlnZkeyFilesDir = path.join("zkeyFiles", "rln");
// zkeyFiles/rln/verification_key.json
const rlnVerificationKey = JSON.parse(
  fs.readFileSync(path.join(rlnZkeyFilesDir, "verification_key.json"), "utf-8")
)
// zkeyFiles/rln/circuit.wasm
const rlnWasmFilePath = path.join(rlnZkeyFilesDir, "circuit.wasm")
// zkeyFiles/rln/final.zkey
const rlnFinalZkeyPath = path.join(rlnZkeyFilesDir, "final.zkey")

/* withdraw circuit parameters */
const withdrawZkeyFilesDir = path.join("zkeyFiles", "withdraw")
// zkeyFiles/withdraw/circuit.wasm
const withdrawWasmFilePath = path.join(withdrawZkeyFilesDir, "circuit.wasm")
// zkeyFiles/withdraw/final.zkey
const withdrawFinalZkeyPath = path.join(withdrawZkeyFilesDir, "final.zkey")

const rlnIdentifier = BigInt(5566)
const treeDepth = 16
const provider = new ethers.JsonRpcProvider(url)
const contractAddress = "0x..."
const signer = await provider.getSigner(0)
const identity = new Identity("1234")

// Create an RLN instance with the contract registry.
// ethers provider and the contract address are both required then.
const rln = RLN.createWithContractRegistry({
  /* These parameters are required */
  rlnIdentifier, // The unique id representing your application
  provider, // ethers.js provider
  contractAddress, // RLN contract address

  /* These parameters are optional */
  contractAtBlock, // The block number at which the RLN contract was deployed. If not given, default is 0
  identity, // the semaphore identity. If not given, a new identity is created
  signer, // ethers.js signer. If not given, users won't be able to execute write operations to the RLN contract
  treeDepth, // The depth of the merkle tree. Default is 20
  wasmFilePath: rlnWasmFilePath, // The path to the rln circuit wasm file. If not given, `createProof` will not work
  finalZkeyPath: rlnFinalZkeyPath, // The path to the rln circuit final zkey file. If not given, `createProof` will not work
  verificationKey: rlnVerificationKey, // The rln circuit verification key. If not given, `verifyProof` will not work
  withdrawWasmFilePath, // The path to the withdraw circuit wasm file. If not given, `withdraw` will not work
  withdrawFinalZkeyPath, // The path to the withdraw circuit final zkey file. If not given, `withdraw` will not work

  // ... See all optional parameters in RLN constructor in src/rln.ts
})

Create RLN instance with other types of registries

You can also initialize an RLN instance with the constructor, but you need to provide a registry. This is particularly useful if you want to use a custom registry instead of the contract registry. For testing, you could use MemoryRLNRegistry and let different RLN instances use the same registry.

const registry: IRLNRegistry = new MemoryRLNRegistry(rlnIdentifier, treeDepth)
const rln1 = new RLN({rlnIdentifier, registry, treeDepth})
const rln2 = new RLN({rlnIdentifier, registry, treeDepth})

// ...Do something with rln1 and rln2

Prover-only and Verifier-only modes

RLN instance must at least be initialized with either wasmFilePath and finalZkeyPath, or verificationKey. If you only provide wasmFilePath and finalZkeyPath, you can only generate proofs. You will get an error if you try to verify a proof. If you only provide verificationKey, you can only verify proofs. You will get an error if you try to generate a proof. If you provide both, you can both generate and verify proofs.

const rlnProverOnly = new RLN({
  rlnIdentifier,
  registry,
  wasmFilePath: rlnWasmFilePath,
  finalZkeyPath: rlnFinalZkeyPath,
  // Missing `verificationKey`
  withdrawWasmFilePath,
  withdrawFinalZkeyPath,
})
const rlnVerifierOnly = new RLN({
  rlnIdentifier,
  registry,
  // Missing `wasmFilePath` and `finalZkeyPath`
  verificationKey: rlnVerificationKey,
  withdrawWasmFilePath,
  withdrawFinalZkeyPath,
})

Accessing Identity and Identity Commitment

When an RLN instance is initialized without identity given, it creates an Identity for you. You can access identity and its commitment using rln.identity and rln.identityCommitment respectively.

// Example of accessing the generated identity commitment
const identity = rln.identity
const identityCommitment = rln.identityCommitment

Registering

const messageLimit = BigInt(1);
// This registers the identity commitment to the registry
// If you're using ContractRLNRegistry, you will send a transaction to the RLN contract, sending tokens, and get registered.
await rln.register(messageLimit);
console.log(await rln.isRegistered()) // true

Generating a proof

const epoch = BigInt(123)
const message = "Hello World"
const proof = await rln.createProof(epoch, message);

You can generate a proof for an epoch and a message by calling rln.createProof(). For the same epoch, you can only generate up to messageLimit proofs, each of them with a unique messageId within the range [0, messageLimit-1]. Message id is not required here because after registering, there is a message id counter inside to avoid reaching the rate limit.

Note that the built-in MemoryMessageIDCounter is not persistent. If you stop the application and restart it in the same epoch, you might risk spamming. If you want to persist the message id counter, you can implement your own message id counter by implementing the IMessageIDCounter interface and set it with rln.setMessageIDCounter().

Withdrawing

// This withdraws the identity commitment from the registry.
// If you're using ContractRLNRegistry, you will send a transaction to the RLN contract, and get the tokens back.
await rln.withdraw();
// after withdrawing, you still need to wait for the freezePeriod in order to release the withdrawal
console.log(await rln.isRegistered()) // true

// If you're using ContractRLNRegistry, after `freezePeriod` (i.e. `freezePeriod + 1` blocks), you can release the withdrawal and successfully get the funds back
await rln.releaseWithdrawal();
console.log(await rln.isRegistered()) // false

Verifying a proof

const proofResult = await rln.verifyProof(epoch, message, proof) // true or false

A proof can be invalid in the following conditions:

  • Proof mismatches epoch, message, or rlnIdentifier
  • The snark proof itself is invalid

Saving a proof

User should save all proofs they receive to detect spams. You can save a proof by calling rln.saveProof(). The return value is an object indicating the status of the proof.

const result = await rln.saveProof(proof)
// status can be VALID, DUPLICATE, BREACH.
// - VALID means the proof is successfully added to the cache
// - DUPLICATE means the proof is already saved before
// - BREACH means the added proof breaches the rate limit, in which case the `secret` is recovered and is accessible by `result.secret`
const status = result.status
// if status is "breach", you can get the secret by
const secret = result.secret
  1. verifyProof(epoch, message, proof) and saveProof(proof) are different. verifyProof not only verifies the snark proof but ensure the proof matches epoch and message, while saveProof() does not verify the snark proof at all. saveProof() checks if the proof will spam and adds the proof to cache for future spam detection. If one wants to make sure the proof is for epoch and message and also detect spams, they should call both verifyProof and saveProof.
  1. saveProof is not persistent. If you restart the application, you might fail to detect some spams. If you want to persist the proof cache, you can implement your own proof cache by implementing the ICache interface and set it in the constructor.

Slashing a user

const slashReceiver = "0x0000000000000000000000000000000000001234"
await rln.slash(secret, receiver) // user using the secret gets slashed and the funds go to the receiver

If receiver is not given, the funds will go to the signer.

await rln.slash(secret) // funds go to the signer

Example

Please see the examples here. We have examples for NodeJS and browser.

Tests

npm test

Bugs, Questions & Features

If you find any bugs, have any questions, or would like to propose new features, feel free to open an issue.

License

RLNjs is released under the MIT license.

Description
No description provided
Readme MIT 86 MiB
Languages
TypeScript 95.8%
Shell 2.2%
JavaScript 2%