Zerokit RLN Module

Crates.io

The Zerokit RLN Module provides a Rust implementation for working with Rate-Limiting Nullifier RLN zkSNARK proofs and primitives. This module allows you to:

  • Generate and verify RLN proofs
  • Work with Merkle trees for commitment storage
  • Implement rate-limiting mechanisms for distributed systems

Quick Start

Important

Version 0.6.1 is required for WASM support or x32 architecture. Current version doesn't support these platforms due to dependency issues. WASM support will return in a future release.

Add RLN as dependency

We start by adding zerokit RLN to our Cargo.toml

[dependencies]
rln = { git = "https://github.com/vacp2p/zerokit" }

Basic Usage Example

Note that we need to pass to RLN object constructor the path where the graph file (graph.bin, built for the input tree size), the corresponding proving key (rln_final.zkey) or (rln_final_uncompr.arkzkey) and verification key (verification_key.arkvkey, optional) are found.

In the following we will use cursors as readers/writers for interfacing with RLN public APIs.

use rln::protocol::*;
use rln::public::*;
use std::io::Cursor;

// 1. Initialize RLN with parameters:
// - the tree height;
// - the tree config, if it is not defined, the default value will be set
let tree_height = 20;
let input = Cursor::new(json!({}).to_string());
let mut rln = RLN::new(tree_height, input);

// 2. Generate an identity keypair
let (identity_secret_hash, id_commitment) = keygen();

// 3. Add a rate commitment to the Merkle tree
let id_index = 10;
let user_message_limit = 10;
let rate_commitment = utils_poseidon_hash(&[id_commitment, user_message_limit]);
let mut buffer = Cursor::new(fr_to_bytes_le(&rate_commitment));
rln.set_leaf(id_index, &mut buffer).unwrap();

// 4. Set up external nullifier (epoch + app identifier)
// We generate epoch from a date seed and we ensure is
// mapped to a field element by hashing-to-field its content
let epoch = hash_to_field(b"Today at noon, this year");
// We generate rln_identifier from a date seed and we ensure is
// mapped to a field element by hashing-to-field its content
let rln_identifier = hash_to_field(b"test-rln-identifier");
let external_nullifier = utils_poseidon_hash(&[epoch, rln_identifier]);

// 5. Generate and verify a proof for a message
let signal = b"RLN is awesome";

// 6. Prepare input for generate_rln_proof API
// input_data is [ identity_secret<32> | id_index<8> | external_nullifier<32> | user_message_limit<32> | message_id<32> | signal_len<8> | signal<var> ]
let mut serialized: Vec<u8> = Vec::new();
serialized.append(&mut fr_to_bytes_le(&identity_secret_hash));
serialized.append(&mut normalize_usize(identity_index));
serialized.append(&mut fr_to_bytes_le(&user_message_limit));
serialized.append(&mut fr_to_bytes_le(&Fr::from(1)));
serialized.append(&mut fr_to_bytes_le(&external_nullifier));
serialized.append(&mut normalize_usize(signal.len()));
serialized.append(&mut signal.to_vec());

// 7. Generate a RLN proof
// We generate a RLN proof for proof_input
let mut input_buffer = Cursor::new(serialized);
let mut output_buffer = Cursor::new(Vec::<u8>::new());
rln.generate_rln_proof(&mut input_buffer, &mut output_buffer)
    .unwrap();

// We get the public outputs returned by the circuit evaluation
// The byte vector `proof_data` is serialized as `[ zk-proof | tree_root | external_nullifier | share_x | share_y | nullifier ]`.
let mut proof_data = output_buffer.into_inner();

// 8. Verify a RLN proof
// Input buffer is serialized as `[proof_data | signal_len | signal ]`, where `proof_data` is (computed as) the output obtained by `generate_rln_proof`.
let verify_data = prepare_verify_input(proof_data, signal);

// We verify the zk-proof against the provided proof values
let mut input_buffer = Cursor::new(verify_data);
let verified = rln.verify_rln_proof(&mut input_buffer).unwrap();

// We ensure the proof is valid
assert!(verified);

Comments for the code above for point 4

The external nullifier includes two parameters.

The first one is epoch and it's used to identify messages received in a certain time frame. It usually corresponds to the current UNIX time but can also be set to a random value or generated by a seed, provided that it corresponds to a field element.

The second one is rln_identifier and it's used to prevent a RLN ZK proof generated for one application to be re-used in another one.

Features

  • Multiple Backend Support: Choose between different zkey formats with feature flags
    • arkzkey: Use the optimized Arkworks-compatible zkey format (faster loading)
    • stateless: For stateless proof verification
  • Pre-compiled Circuits: Ready-to-use circuits with Merkle tree height of 20

Building and Testing

Prerequisites

git clone https://github.com/vacp2p/zerokit.git
make installdeps
cd zerokit/rln

Build Commands

# Build with default features
cargo make build

# Test with default features
cargo make test

# Test with specific features
cargo make test_arkzkey    # For arkzkey feature
cargo make test_stateless  # For stateless feature

Advanced: Custom Circuit Compilation

The rln (https://github.com/rate-limiting-nullifier/circom-rln) repository, which contains the RLN circuit implementation is using for pre-compiled RLN circuit for zerokit RLN. If you want to compile your own RLN circuit, you can follow the instructions below.

1. Compile ZK Circuits

# Clone the circom-rln repository
git clone https://github.com/rate-limiting-nullifier/circom-rln

# Install dependencies
cd circom-rln && npm install

# Build circuits
./scripts/build-circuits.sh rln

# Copy assets to resources directory
cp build/zkeyFiles/final.zkey ./resources/tree_height_20/rln_final.zkey

To customize the circuit parameters, modify circom-rln/circuit/rln.circom:

pragma circom 2.1.0;
include "./rln.circom";
component main { public [x, externalNullifier] } = RLN(N, M); // N = tree height, M = limit bit size

Note

However, if N is too big, this might require a bigger Powers of Tau ceremony than the one hardcoded in ./scripts/build-circuits.sh, which is 2^14.
In such case we refer to the official Circom documentation for instructions on how to run an appropriate Powers of Tau ceremony and Phase 2 in order to compile the desired circuit.
Currently, the rln module comes with one pre-compiled RLN circuit having Merkle tree of height 20.for details.

2. Generate Arkzkey Representation

For faster loading, compile the zkey file into the arkzkey format using ark-zkey.

Currently, the rln module comes with pre-compiled arkzkey keys for the RLN circuit.

3. Generate Witness Calculation Graph

The execution graph file used for witness calculation can be compiled following instructions in the circom-witnesscalc repository.

The rln module comes with pre-compiled execution graph files for the RLN circuit.

Get involved

Zerokit RLN public and FFI APIs allow interaction with many more features than what briefly showcased above.

We invite you to check our API documentation by running

cargo doc --no-deps

and look at unit tests to have an hint on how to interface and use them.

Detailed Protocol Flow

  1. Identity Creation: Generate a secret key and commitment
  2. Rate Commitment: Add commitment to a Merkle tree
  3. External Nullifier Setup: Combine epoch and application identifier
  4. Proof Generation: Create a zkSNARK proof that:
    • Proves membership in the Merkle tree
    • Ensures rate-limiting constraints are satisfied
    • Generates a nullifier to prevent double-usage
  5. Proof Verification: Verify the proof without revealing the prover's identity

Getting Involved