build: upgrade to new contract
Rate Limiting Nullifier Javascript / Typescript Library
Contents
- Rate Limiting Nullifier Javascript / Typescript Library
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.
With script (Recommended)
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
verifyProof(epoch, message, proof)andsaveProof(proof)are different.verifyProofnot only verifies the snark proof but ensure the proof matchesepochandmessage, whilesaveProof()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 forepochandmessageand also detect spams, they should call bothverifyProofandsaveProof.
saveProofis 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.