<p>The following specification covers the RLN construct as well as some auxiliary libraries useful for interacting with it.
Rate limiting nullifier (RLN) is a construct based on zero-knowledge proofs that provides an anonymous rate-limited signaling/messaging framework suitable for decentralized (and centralized) environments.
Anonymity refers to the unlinkability of messages to their owner.</p>
<h1id="motivation">
Motivation
<aclass="anchor"href="#motivation">#</a>
</h1>
<p>RLN guarantees a messaging rate is enforced cryptographically while preserving the anonymity of the message owners.
A wide range of applications can benefit from RLN and provide desirable security features.
For example, an e-voting system can integrate RLN to contain the voting rate while protecting the voters-vote unlinkability.
Another use case is to protect an anonymous messaging system against DDoS and spam attacks by containing messaging rate of users.
This latter use case is explained in <ahref="/spec/17">17/WAKU2-RLN-RELAY RFC</a>.</p>
<h1id="flow">
Flow
<aclass="anchor"href="#flow">#</a>
</h1>
<p>The users participate in the protocol by first registering to an application-defined group referred by the <em>membership group</em>.
Registration to the group is mandatory for signaling in the application.
After registration, group members can generate Zero-knowledge Proof of membership for their signals and can participate in the application.
Usually, the membership requires a financial or social stake which
is beneficial for the prevention of Sybil attacks and double-signaling.
Group members are allowed to send one signal per external nullifier (an identifier that groups signals and can be thought of as a voting booth. Usually a timestamp or time interval in the RLN apps).
If a user generates more signals than allowed,
the user risks being slashed - by revealing his membership secret credentials.
If the financial stake is put in place, the user also risks his stake being taken.</p>
<p>Generally the flow can be described by the following steps:</p>
<ol>
<li>Registration</li>
<li>Signaling</li>
<li>Verification and slashing</li>
</ol>
<h2id="registration">
Registration
<aclass="anchor"href="#registration">#</a>
</h2>
<p>Depending on the application requirements, the registration can be implemented in different ways, for example:</p>
<ul>
<li>centralized registrations, by using a central server</li>
<li>decentralized registrations, by using a smart contract</li>
<p>What is important is that the users’ identity commitments (explained in section <ahref="#user-identity">User Indetity</a>) are stored in a Merkle tree,
</code></pre><p>For registration, the user needs to submit their <code>identity_commitment</code> (along with any additional registration requirements) to the registry.
Upon registration, they should receive <code>leaf_index</code> value which represents their position in the merkle tree.
Receiving a <code>leaf_index</code> is not a hard requirement and is application specific.
The other way around is the users calculating the <code>leaf_index</code> themselves upon successful registration.</p>
<h2id="signaling">
Signaling
<aclass="anchor"href="#signaling">#</a>
</h2>
<p>After registration,
the users can participate in the application by sending signals to the other participants in a decentralised manner or to a centralised server.
Along with their signal,
they need to generate a ZK-Proof by using the circuit with the specification described above.</p>
<p>For generating a proof,
the users need to obtain the required parameters or compute them themselves,
depending on the application implementation and client libraries supported by the application.
For example the users can store the membership merkle tree on their end and
generate a merkle proof whenever they want to generate a signal.</p>
<p>The merkle proof should be obtained locally or from a trusted third party.
By using the <ahref="https://github.com/appliedzkp/incrementalquintree/blob/master/ts/IncrementalQuinTree.ts">incremental merkle tree algorithm</a>,
the merkle can be obtained by providing the <code>leaf_index</code> of the <code>identity_commitment</code>.
The proof (<code>merkle_proof</code>) is composed of the following fields:</p>
<pretabindex="0"><code>{
root: bigint
indices: number[]
path_elements: bigint[][]
}
</code></pre><ol>
<li><strong>root</strong> - The root of membership group Merkle tree at the time of publishing the message</li>
<li><strong>indices</strong> - The index fields of the leafs in the merkle tree - used by the merkle tree algorithm for verification</li>
<li><strong>path_elements</strong> - Auxiliary data structure used for storing the path to the leaf - used by the merkle proof algorithm for verificaton</li>
</ol>
<h4id="generating-proof">
Generating proof
<aclass="anchor"href="#generating-proof">#</a>
</h4>
<p>For proof generation,
the user need to submit the following fields to the circuit:</p>
<pretabindex="0"><code> {
identity_secret: identity_secret_hash,
path_elements: merkle_proof.path_elements,
identity_path_index: merkle_proof.indices,
x: signal_hash,
external_nullifier: external_nullifier,
rln_identifier: rln_identifier
}
</code></pre><h4id="calculating-output">
Calculating output
<aclass="anchor"href="#calculating-output">#</a>
</h4>
<p>The proof output is calculated locally,
in order for the required fields for proof verification to be sent along with the proof.
The proof output is composed of the <code>y</code> share of the secret equation and the <code>internal_nullifier</code>.
The <code>internal_nullifier</code> represents an unique fingerprint for the user for the <code>external_nullifier</code>.
The following fields are needed for proof output calculation:</p>
<pretabindex="0"><code>{
identity_secret_hash: bigint,
external_nullifier: bigint,
rln_identifier: bigint,
x: bigint,
}
</code></pre><p>The output <code>[y, internal_nullifier]</code> is calculated in the following way:</p>
</code></pre><p>It relies on the properties of the <ahref="https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing">Shamir’s Secret sharing scheme</a>.</p>
The received message is checked to ensure it is not duplicate.
The duplicate message check is performed by verifying that the <code>x</code> and <code>y</code> fields do not exist in the <code>messaging_metadata</code> object.
If the <code>x</code> and <code>y</code> fields exist in the <code>x_shares</code> and <code>y_shares</code> array for the <code>external_nullifier</code> and
the <code>internal_nullifier</code> the message can be considered as a duplicate.
<p>The <code>zk_proof</code> should be verified by providing the <code>zk_proof</code> field to the circuit verifier along with the <code>public_signal</code>:</p>
<pretabindex="0"><code>[
y,
merkle_proof.root,
internal_nullifier,
signal_hash,
external_nullifier,
rln_identifier
]
</code></pre><p>If the proof verification is correct,
the verification continues, otherwise the message is discarded.</p>
<p>After the proof is verified the <code>x</code>, and <code>y</code> fields are added to the <code>x_shares</code> and <code>y_shares</code> arrays of the <code>messaging_metadata</code><code>external_nullifier</code> and <code>internal_nullifier</code> object.
If the length of the arrays is equal to the signaling threshold (<code>limit</code>), the user can be slashed.</p>
<h4id="slashing">
Slashing
<aclass="anchor"href="#slashing">#</a>
</h4>
<p>After the verification, the user can be slashed if two different shares are present to reconstruct their <code>identity_secret_hash</code> from <code>x_shares</code> and <code>y_shares</code> fields,
for their <code>internal_nullifier</code>.
The secret can be retreived by the properties of the Shamir’s secret sharing scheme.
In particular the secret (<code>a_0</code>) can be retrieved by computing <ahref="https://en.wikipedia.org/wiki/Lagrange_polynomial">Lagrange polynomials</a>.</p>
<p>After the secret is retreived,
the user’s <code>identity_commitment</code> can be generated from the secret and it can be used for removing the user from the membership merkle tree (zeroing out the leaf that contains the user’s <code>identity_commitment</code>).
Additionally, depending on the application the <code>identity_secret_hash</code> can be used for taking the user’s provided stake.</p>
<h1id="technical-overview">
Technical overview
<aclass="anchor"href="#technical-overview">#</a>
</h1>
<p>The main RLN construct is implemented using a <ahref="https://z.cash/technology/zksnarks/">ZK-SNARK</a> circuit.
However it is helpful to describe the other necessary outside components for interaction with the circuit,
which together with the ZK-SNARK circuit enable the above mentioned features.</p>
<td>Financial or social stake required for registering in the RLN applications. Common stake examples are: locking cryptocurrency (financial), linking reputable social identity.</td>
</tr>
<tr>
<td><strong>Identity secret</strong></td>
<td>An array of two unique random components (identity nullifier and identity trapdoor), which must be kept private by the user. Secret hash and identity commitment are derived from this array.</td>
</tr>
<tr>
<td><strong>Identity nullifier</strong></td>
<td>Random 32 byte value used as component for identity secret generation.</td>
</tr>
<tr>
<td><strong>Identity trapdoor</strong></td>
<td>Random 32 byte value used as component for identity secret generation.</td>
</tr>
<tr>
<td><strong>Identity secret hash</strong></td>
<td>The hash of the identity secret, obtained using the Poseidon hash function. It is used for deriving the identity commitment of the user, and as a private input for zk proof generation. The secret hash should be kept private by the user.</td>
</tr>
<tr>
<td><strong>Identity commitment</strong></td>
<td>Hash obtained from the <code>Identity secret hash</code> by using the poseidon hash function. It is used by the users for registering in the protocol.</td>
</tr>
<tr>
<td><strong>Signal</strong></td>
<td>The message generated by a user. It is an arbitrary bit string that may represent a chat message, a URL request, protobuf message, etc.</td>
</tr>
<tr>
<td><strong>Signal hash</strong></td>
<td>Keccak hash of the signal, used as an input in the RLN circuit.</td>
</tr>
<tr>
<td><strong>RLN Identifier</strong></td>
<td>Random finite field value unique per RLN app. It is used for additional cross-application security. The role of the RLN identifier is protection of the user secrets being compromised if signals are being generated with the same credentials at different apps.</td>
</tr>
<tr>
<td><strong>RLN membership tree</strong></td>
<td>Merkle tree data structure, filled with identity commitments of the users. Serves as a data structure that ensures user registrations.</td>
</tr>
<tr>
<td><strong>Merkle proof</strong></td>
<td>Proof that a user is member of the RLN membership tree.</td>
<td>Keccak hash of the signal, same as signal hash (Defined above).</td>
</tr>
<tr>
<td><strong>A0</strong></td>
<td>The identity secret hash.</td>
</tr>
<tr>
<td><strong>A1</strong></td>
<td>Poseidon hash of [A0, External nullifier] (see about External nullifier below).</td>
</tr>
<tr>
<td><strong>y</strong></td>
<td>The result of the polynomial equation (y = a0 + a1*x). The public output of the circuit.</td>
</tr>
<tr>
<td><strong>External nullifier</strong></td>
<td><code>keccak256</code> hash of a string. An identifier that groups signals and can be thought of as a voting booth. Usually a timestamp or time interval in the RLN apps.</td>
</tr>
<tr>
<td><strong>Internal nullifier</strong></td>
<td>Poseidon hash of [A1, RLN Identifier]. This field ensures that a user can send only one valid signal per external nullifier without risking being slashed. Public input of the circuit.</td>
<p>Anonymous signaling with a controlled rate limit is enabled by proving that the user is part of a group which has high barriers to entry (form of stake) and
enabling secret reveal if more than 1 unique signal is produced per external nullifier.
The membership part is implemented using membership <ahref="https://en.wikipedia.org/wiki/Merkle_tree">merkle trees</a> and merkle proofs,
while the secret reveal part is enabled by using the Shamir’s Secret Sharing scheme.
Essentially the protocol requires the users to generate zero-knowledge proof to be able to send signals and participate in the application.
The zero knowledge proof proves that the user is member of a group,
but also enforces the user to share part of their secret for each signal in an external nullifier.
The external nullifier is usually represented by timestamp or a time interval.
It can also be thought of as a voting booth in voting applications.</p>
<p>The ZK Circuit is implemented using a <ahref="https://eprint.iacr.org/2016/260.pdf">Groth-16 ZK-SNARK</a>,
using the <ahref="https://docs.circom.io/">circomlib</a> library.</p>
<h3id="system-parameters">
System parameters
<aclass="anchor"href="#system-parameters">#</a>
</h3>
<ul>
<li><code>n_levels</code> - merkle tree depth</li>
<li><code>root</code> - the rln membership tree root</li>
<li><code>internal_nullifier</code></li>
</ul>
<h3id="hash-function">
Hash function
<aclass="anchor"href="#hash-function">#</a>
</h3>
<p>Canonical <ahref="https://eprint.iacr.org/2019/458.pdf">Poseidon hash implementation</a> is used,
as implemented in the <ahref="https://github.com/iden3/circomlib/blob/master/circuits/poseidon.circom">circomlib library</a>, according to the Poseidon paper.
This Poseidon hash version (canonical implementation) uses the following parameters:</p>
<p>For a valid signal, a user’s <code>identity_commitment</code> (more on identity commitments below) must exist in identity membership tree.
Membership is proven by providing a membership proof (witness).
The fields from the membership proof required for the verification are:
<code>path_elements</code> and <code>identity_path_index</code>.</p>
<p><ahref="https://github.com/appliedzkp/incrementalquintree">IncrementalQuinTree</a> algorithm is used for constructing the Membership merkle tree.
The circuits are reused from this repository.
You can find out more details about the IncrementalQuinTree algorithm <ahref="https://ethresear.ch/t/gas-and-circuit-constraint-benchmarks-of-binary-and-quinary-incremental-merkle-trees-using-the-poseidon-hash-function/7446">here</a>.</p>
<p>Slashing is enabled by using polynomials and <ahref="https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing">Shamir’s Secret sharing</a>.
In order to produce a valid proof, <code>identity_secret_hash</code> as a private input to the circuit.
Then a secret equation is created in the form of:</p>
<pretabindex="0"><code>y = a_0 + x*a_1,
</code></pre><p>where <code>a_0</code> is the <code>identity_secret_hash</code> and <code>a_1 = hash(a_0, external nullifier)</code>.
Along with the generated proof,
the users need to provide a (x, y) share which satisfies the line equation,
in order for their proof to be verified.
<code>x</code> is the hashed signal, while the <code>y</code> is the circuit output.
With more than one pair of unique shares, anyone can derive <code>a_0</code>, the <code>identity_secret_hash</code> .
The hash of a signal will be evaluation point <code>x</code>.
So that a member who sends more than one unique signal per <code>external_nullifier</code> risks their identity secret being revealed.</p>
<p>Note that shares used for different external nullifiers and different RLN apps cannot be used to derive the secret key.</p>
<p>The <code>rln_identifier</code> is a random value from a finite field,
unique per RLN app,
and is used for additional cross-application security - to protect the user secrets being compromised if they use the same credentials accross different RLN apps.
If <code>rln_identifier</code> is not present,
the user uses the same credentials and sends a different message for two different RLN apps using the same <code>external_nullifier</code>,
then their user signals can be grouped by the <code>internal_nullifier</code> which could lead the user’s secret revealed.
This is because two separate signals under the same <code>internal_nullifier</code> can be treated as rate limiting violation.
With adding the <code>rln_identifier</code> field we obscure the <code>internal_nullifier</code>,
so this kind of attack can be hardened because we don’t have the same <code>internal_nullifier</code> anymore.
The only kind of attack that is possible is if we have an entity with a global view of all messages,
and they try to brute force different combinations of <code>x</code> and <code>y</code> shares for different <code>internal_nullifier</code>s.</p>
<p>RLN is an experimental and still un-audited technology. This means that the circuits have not been yet audited.
Another consideration is the security of the underlying primitives.
zk-SNARKS require a trusted setup for generating a prover and verifier keys.
The standard for this is to use trusted <ahref="https://en.wikipedia.org/wiki/Secure_multi-party_computation">Multi-Party Computation (MPC)</a> ceremony,
which requires two phases.
Trusted MPC ceremony has not yet been performed for the RLN circuits.</p>
<p>There are few additional tools implemented for easier integrations and usage of the RLN protocol.</p>
<p><ahref="https://github.com/appliedzkp/zk-kit"><code>zk-kit</code></a> is a typescript library which exposes APIs for identity credentials generation,
as well as proof generation.
It supports various protocols (<code>Semaphore</code>, <code>RLN</code>),.</p>
<p><ahref="https://github.com/akinovak/zk-keeper"><code>zk-keeper</code></a> is a browser plugin which allows for safe credential storing and proof generation.
</span></span><spanstyle="display:flex;"><span><spanstyle="color:#66d9ef">const</span><spanstyle="color:#a6e22e">LEAVES</span><spanstyle="color:#f92672">=</span> [...]; <spanstyle="color:#75715e">// leaves should be an array of the leaf values of the membership merkle tree
</span></span></span><spanstyle="display:flex;"><span><spanstyle="color:#75715e"></span><spanstyle="color:#75715e">// the identity commitment generated below should be present in the LEAVES array
</span></span><spanstyle="display:flex;"><span><spanstyle="color:#75715e">// this is for illustrative purposes only. The identityCommitment should be present in the LEAVES array above.
</span></span><spanstyle="display:flex;"><span><spanstyle="color:#66d9ef">const</span><spanstyle="color:#a6e22e">wasmFilePath</span><spanstyle="color:#f92672">=</span><spanstyle="color:#a6e22e">path</span>.<spanstyle="color:#a6e22e">join</span>(<spanstyle="color:#a6e22e">zkeyFiles</span>, <spanstyle="color:#e6db74">"rln"</span>, <spanstyle="color:#e6db74">"rln.wasm"</span>) <spanstyle="color:#75715e">// path to the WASM compiled circuit
</span></span></span><spanstyle="display:flex;"><span><spanstyle="color:#75715e"></span><spanstyle="color:#66d9ef">const</span><spanstyle="color:#a6e22e">finalZkeyPath</span><spanstyle="color:#f92672">=</span><spanstyle="color:#a6e22e">path</span>.<spanstyle="color:#a6e22e">join</span>(<spanstyle="color:#a6e22e">zkeyFiles</span>, <spanstyle="color:#e6db74">"rln"</span>, <spanstyle="color:#e6db74">"rln_final.zkey"</span>) <spanstyle="color:#75715e">// path to the prover key
</span></span><spanstyle="display:flex;"><span><spanstyle="color:#66d9ef">const</span><spanstyle="color:#a6e22e">vkeyPath</span><spanstyle="color:#f92672">=</span><spanstyle="color:#a6e22e">path</span>.<spanstyle="color:#a6e22e">join</span>(<spanstyle="color:#a6e22e">zkeyFiles</span>, <spanstyle="color:#e6db74">"rln"</span>, <spanstyle="color:#e6db74">"verification_key.json"</span>) <spanstyle="color:#75715e">// Path to the verifier key
</span></span></span><spanstyle="display:flex;"><span><spanstyle="color:#75715e"></span><spanstyle="color:#66d9ef">const</span><spanstyle="color:#a6e22e">vKey</span><spanstyle="color:#f92672">=</span><spanstyle="color:#a6e22e">JSON</span>.<spanstyle="color:#a6e22e">parse</span>(<spanstyle="color:#a6e22e">fs</span>.<spanstyle="color:#a6e22e">readFileSync</span>(<spanstyle="color:#a6e22e">vkeyPath</span>, <spanstyle="color:#e6db74">"utf-8"</span>)) <spanstyle="color:#75715e">// The verifier key
</span></span></code></pre></div><p>For more details please visit the <ahref="https://github.com/appliedzkp/zk-kit"><code>zk-kit</code></a> library.</p>