15 unstable releases (3 breaking)

0.5.1 Jun 20, 2024
0.4.3 Apr 8, 2024
0.4.2 Mar 7, 2024
0.4.1 Oct 4, 2023
0.3.0 Jul 31, 2023

#12 in #zk-proofs

MIT/Apache

4.5MB
3.5K SLoC

Zerokit RLN Module

This module provides APIs to manage, compute and verify RLN zkSNARK proofs and RLN primitives.

Pre-requisites

Install dependencies and clone repo

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

Build and Test

To build and test, run the following commands within the module folder

cargo make build
cargo make test

Compile ZK circuits

The rln (https://github.com/rate-limiting-nullifier/circom-rln) repository, which contains the RLN circuit implementation is a submodule of zerokit RLN.

To compile the RLN circuit

# Update submodules
git submodule update --init --recursive

# Install rln dependencies
cd vendor/rln/ && npm install

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

# Copy over assets
cp build/zkeyFiles/rln-final.zkey ../../resources/tree_height_15
cp build/zkeyFiles/rln.wasm ../../resources/tree_height_15

Note that the above code snippet will compile a RLN circuit with a Merkle tree of height equal 15 based on the default value set in vendor/rln/circuit/rln.circom.

In order to compile a RLN circuit with Merkle tree height N, it suffices to change vendor/rln/circuit/rln.circom to

pragma circom 2.0.0;

include "./rln-base.circom";

component main {public [x, epoch, rln_identifier ]} = RLN(N);

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 2 pre-compiled RLN circuits having Merkle tree of height 20 and 32, respectively.

Getting started

Add RLN as dependency

We start by adding zerokit RLN to our Cargo.toml

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

Create a RLN object

First, we need to create a RLN object for a chosen input Merkle tree size.

Note that we need to pass to RLN object constructor the path where the circuit (rln.wasm, built for the input tree size), the corresponding proving key (rln_final.zkey) or (rln_final.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;

// We set the RLN 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());

// We create a new RLN instance
let mut rln = RLN::new(tree_height, input);

Generate an identity keypair

We generate an identity keypair

// We generate an identity pair
let mut buffer = Cursor::new(Vec::<u8>::new());
rln.key_gen(&mut buffer).unwrap();

// We deserialize the keygen output to obtain
// the identity_secret and id_commitment
let (identity_secret_hash, id_commitment) = deserialize_identity_pair(buffer.into_inner());

Add Rate commitment to the RLN Merkle tree

// We define the tree index where id_commitment will be added
let id_index = 10;
let user_message_limit = 10;

// We serialize id_commitment and pass it to set_leaf
let rate_commitment = poseidon_hash(&[id_commitment, user_message_limit]);
let mut buffer = Cursor::new(serialize_field_element(rate_commitment));
rln.set_leaf(id_index, &mut buffer).unwrap();

Note that when tree leaves are not explicitly set by the user (in this example, all those with index less and greater than 10), their values is set to an hardcoded default (all-0 bytes in current implementation).

Set external nullifier

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.

// 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 = poseidon_hash(&[epoch, rln_identifier]);

Set signal

The signal is the message for which we are computing a RLN proof.

// We set our signal
let signal = b"RLN is awesome";

Generate a RLN proof

We prepare the input to the proof generation routine.

Input buffer is serialized as [ identity_key | id_index | external_nullifier | user_message_limit | message_id | signal_len | signal ].

// We prepare input to the proof generation routine
let proof_input = prepare_prove_input(identity_secret_hash, id_index, external_nullifier, signal);

We are now ready to generate a RLN ZK proof along with the public outputs of the ZK circuit evaluation.


// We generate a RLN proof for proof_input
let mut in_buffer = Cursor::new(proof_input);
let mut out_buffer = Cursor::new(Vec::<u8>::new());
rln.generate_rln_proof(&mut in_buffer, &mut out_buffer)
    .unwrap();

// We get the public outputs returned by the circuit evaluation
let proof_data = out_buffer.into_inner();

The byte vector proof_data is serialized as [ zk-proof | tree_root | external_nullifier | share_x | share_y | nullifier ].

Verify a RLN proof

We prepare the input to the proof verification routine.

Input buffer is serialized as [proof_data | signal_len | signal ], where proof_data is (computed as) the output obtained by generate_rln_proof.

// We prepare input to the proof verification routine
let verify_data = prepare_verify_input(proof_data, signal);

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

We check if the proof verification was successful:

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

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.

Dependencies

~27–39MB
~549K SLoC