fix(32/RLN): update RFC according to design changes (#561)

* fix(rln): update rfc according to design changes

* fix(rln): formatting table

* fix(rln): format table

* fix(rln): typos, distinction epoch/external nullifier, rust code, removed obsolete stuff

* fix(rln): wrong capital letter in link

* fix(rln): specify internal_nullifier depends on app
This commit is contained in:
G 2022-12-13 19:58:21 +01:00 committed by GitHub
parent 2b72238134
commit 66d73a27f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 139 additions and 103 deletions

View File

@ -33,7 +33,7 @@ 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).
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).
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.
@ -52,7 +52,7 @@ Depending on the application requirements, the registration can be implemented i
- decentralized registrations, by using a smart contract
What is important is that the users' identity commitments (explained in section [User Indetity](#user-identity)) are stored in a Merkle tree,
and the users can obtain a merkle proof proving that they are part of the group.
and the users can obtain a Merkle proof proving that they are part of the group.
Also depending on the application requirements,
usually a financial or social stake is introduced.
@ -76,7 +76,7 @@ The user's identity is composed of:
```
For registration, the user needs to submit their `identity_commitment` (along with any additional registration requirements) to the registry.
Upon registration, they should receive `leaf_index` value which represents their position in the merkle tree.
Upon registration, they should receive `leaf_index` value which represents their position in the Merkle tree.
Receiving a `leaf_index` is not a hard requirement and is application specific.
The other way around is the users calculating the `leaf_index` themselves upon successful registration.
@ -90,8 +90,8 @@ they need to generate a ZK-Proof by using the circuit with the specification des
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.
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.
### Implementation notes
@ -101,14 +101,18 @@ The signal hash can be generated by hashing the raw signal (or content) using th
#### External nullifier
The external nullifier can be generated by hashing a raw string (i.e UNIX timestamp) value using `keccak256`.
The external nullifier MUST be computed as the Poseidon hash of the current epoch (e.g. a value equal to or derived from the current UNIX timestamp divided by the epoch length) and the RLN identifier.
#### Obtaining merkle proof
```
external_nullifier = poseidonHash([epoch, rln_identifier])
```
The merkle proof should be obtained locally or from a trusted third party.
By using the [incremental merkle tree algorithm](https://github.com/appliedzkp/incrementalquintree/blob/master/ts/IncrementalQuinTree.ts),
the merkle can be obtained by providing the `leaf_index` of the `identity_commitment`.
The proof (`merkle_proof`) is composed of the following fields:
#### Obtaining Merkle proof
The Merkle proof should be obtained locally or from a trusted third party.
By using the [incremental Merkle tree algorithm](https://github.com/appliedzkp/incrementalquintree/blob/master/ts/IncrementalQuinTree.ts),
the Merkle can be obtained by providing the `leaf_index` of the `identity_commitment`.
The proof (`Merkle_proof`) is composed of the following fields:
```
{
@ -119,8 +123,8 @@ The proof (`merkle_proof`) is composed of the following fields:
```
1. **root** - The root of membership group Merkle tree at the time of publishing the message
2. **indices** - The index fields of the leafs in the merkle tree - used by the merkle tree algorithm for verification
3. **path_elements** - Auxiliary data structure used for storing the path to the leaf - used by the merkle proof algorithm for verificaton
2. **indices** - The index fields of the leafs in the Merkle tree - used by the Merkle tree algorithm for verification
3. **path_elements** - Auxiliary data structure used for storing the path to the leaf - used by the Merkle proof algorithm for verificaton
#### Generating proof
@ -131,10 +135,10 @@ the user need to submit the following fields to the circuit:
```
{
identity_secret: identity_secret_hash,
path_elements: merkle_proof.path_elements,
identity_path_index: merkle_proof.indices,
path_elements: Merkle_proof.path_elements,
identity_path_index: Merkle_proof.indices,
x: signal_hash,
external_nullifier: external_nullifier,
epoch: epoch,
rln_identifier: rln_identifier
}
```
@ -145,13 +149,13 @@ the user need to submit the following fields to the circuit:
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 `y` share of the secret equation and the `internal_nullifier`.
The `internal_nullifier` represents an unique fingerprint for the user for the `external_nullifier`.
The `internal_nullifier` represents a unique fingerprint of a user for a given `epoch` and app.
The following fields are needed for proof output calculation:
```
{
identity_secret_hash: bigint,
external_nullifier: bigint,
epoch: bigint,
rln_identifier: bigint,
x: bigint,
}
@ -160,12 +164,14 @@ The following fields are needed for proof output calculation:
The output `[y, internal_nullifier]` is calculated in the following way:
```
a_0 = identity_secret
external_nullifier = poseidonHash([epoch, rln_identifier])
a_0 = identity_secret_hash
a_1 = poseidonHash([a0, external_nullifier])
y = a_0 + x * a_1
internal_nullifier = poseidonHash([a_1, rln_identifier])
internal_nullifier = poseidonHash([a_1])
```
It relies on the properties of the [Shamir's Secret sharing scheme](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing).
@ -191,8 +197,8 @@ the following fields might be required:
```
{
root: merkle_proof.root,
external_nullifier: external_nullifier
root: Merkle_proof.root,
epoch: epoch
}
```
@ -209,9 +215,9 @@ the slashing will be implemented on each user's client.
### Implementation notes
Each user of the protocol (server or otherwise) will need to store metadata for each message received by each user,
for the given `external_nullifier`.
The data can be deleted when the `external_nullifier` passes.
Storing metadata is required, so that if a user sends more than one unique signal per `external_nullifier`,
for the given `epoch`.
The data can be deleted when the `epoch` passes.
Storing metadata is required, so that if a user sends more than one unique signal per `epoch`,
they can be slashed and removed from the protocol.
The metadata stored contains the `x`, `y` shares and the `internal_nullifier` for the user for each message.
If enough such shares are present, the user's secret can be retreived.
@ -238,7 +244,7 @@ The output message verification consists of the following steps:
- spam verification
**1. `external_nullifier` correctness**
Upon received `output_message`, first the `external_nullifier` field is checked,
Upon received `output_message`, first the `epoch` and `rln_identifier` fields are checked,
to ensure that the message matches the current `external_nullifier`.
If the `external_nullifier` is correct the verification continues, otherwise, the message is discarded.
@ -256,10 +262,10 @@ The `zk_proof` should be verified by providing the `zk_proof` field to the circu
```
[
y,
merkle_proof.root,
Merkle_proof.root,
internal_nullifier,
signal_hash,
external_nullifier,
x, # signal_hash
epoch,
rln_identifier
]
```
@ -280,13 +286,13 @@ The secret can be retreived by the properties of the Shamir's secret sharing sch
In particular the secret (`a_0`) can be retrieved by computing [Lagrange polynomials](https://en.wikipedia.org/wiki/Lagrange_polynomial).
After the secret is retreived,
the user's `identity_commitment` 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 `identity_commitment`).
the user's `identity_commitment` 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 `identity_commitment`).
Additionally, depending on the application the `identity_secret_hash` can be used for taking the user's provided stake.
# Technical overview
The main RLN construct is implemented using a [ZK-SNARK](https://z.cash/technology/zksnarks/) circuit.
However it is helpful to describe the other necessary outside components for interaction with the 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.
@ -302,8 +308,8 @@ which together with the ZK-SNARK circuit enable the above mentioned features.
| **Identity secret hash** | 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. |
| **Identity commitment** | Hash obtained from the `Identity secret hash` by using the poseidon hash function. It is used by the users for registering in the protocol. |
| **Signal** | The message generated by a user. It is an arbitrary bit string that may represent a chat message, a URL request, protobuf message, etc. |
| **Signal hash** | Keccak hash of the signal, used as an input in the RLN circuit. |
| **RLN Identifier** | 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. |
| **Signal hash** | Keccak256 hash of the signal modulo circuit's field characteristic, used as an input in the RLN circuit. |
| **RLN Identifier** | 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 from being compromised when signals are being generated with the same credentials in different apps. |
| **RLN membership tree** | Merkle tree data structure, filled with identity commitments of the users. Serves as a data structure that ensures user registrations. |
| **Merkle proof** | Proof that a user is member of the RLN membership tree. |
@ -316,8 +322,8 @@ which together with the ZK-SNARK circuit enable the above mentioned features.
| **A0** | The identity secret hash. |
| **A1** | Poseidon hash of [A0, External nullifier] (see about External nullifier below). |
| **y** | The result of the polynomial equation (y = a0 + a1*x). The public output of the circuit. |
| **External nullifier** | `keccak256` 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. |
| **Internal nullifier** | 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. |
| **External nullifier** | Poseidon hash of [Epoch, RLN Identifier]. An identifier that groups signals and can be thought of as a voting booth. |
| **Internal nullifier** | Poseidon hash of [A1]. This field ensures that a user can send only one valid signal per external nullifier without risking being slashed. Public input of the circuit. |
@ -325,7 +331,7 @@ which together with the ZK-SNARK circuit enable the above mentioned features.
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 [merkle trees](https://en.wikipedia.org/wiki/Merkle_tree) and merkle proofs,
The membership part is implemented using membership [Merkle trees](https://en.wikipedia.org/wiki/Merkle_tree) 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,
@ -339,14 +345,14 @@ using the [circomlib](https://docs.circom.io/) library.
### System parameters
- `n_levels` - merkle tree depth
- `n_levels` - Merkle tree depth
### Circuit parameters
**Public Inputs**
- `x`
- `external_nullifier`
- `epoch`
- `rln_identifier`
**Private Inputs**
@ -365,9 +371,17 @@ Canonical [Poseidon hash implementation](https://eprint.iacr.org/2019/458.pdf) i
as implemented in the [circomlib library](https://github.com/iden3/circomlib/blob/master/circuits/poseidon.circom), according to the Poseidon paper.
This Poseidon hash version (canonical implementation) uses the following parameters:
- `t`: 3
- `RF`: 8
- `RP`: 57
| Hash inputs | `t` | `RF` | `RP`|
|:---:|:---:|:---:|:---:|
|1 | 2 | 8 | 56|
|2 | 3 | 8 | 57|
|3 | 4 | 8 | 56|
|4 | 5 | 8 | 60|
|5 | 6 | 8 | 60|
|6 | 7 | 8 | 63|
|7 | 8 | 8 | 64|
|8 | 9 | 8 | 63|
### Membership implementation
@ -376,9 +390,9 @@ Membership is proven by providing a membership proof (witness).
The fields from the membership proof required for the verification are:
`path_elements` and `identity_path_index`.
[IncrementalQuinTree](https://github.com/appliedzkp/incrementalquintree) algorithm is used for constructing the Membership merkle tree.
[IncrementalQuinTree](https://github.com/appliedzkp/incrementalquintree) 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 [here](https://ethresear.ch/t/gas-and-circuit-constraint-benchmarks-of-binary-and-quinary-incremental-merkle-trees-using-the-poseidon-hash-function/7446).
You can find out more details about the IncrementalQuinTree algorithm [here](https://ethresear.ch/t/gas-and-circuit-constraint-benchmarks-of-binary-and-quinary-incremental-Merkle-trees-using-the-poseidon-hash-function/7446).
### Slashing and Shamir's Secret Sharing
@ -387,19 +401,21 @@ In order to produce a valid proof, `identity_secret_hash` as a private input to
Then a secret equation is created in the form of:
```
y = a_0 + x*a_1,
y = a_0 + x * a_1,
```
where `a_0` is the `identity_secret_hash` and `a_1 = hash(a_0, external nullifier)`.
Along with the generated proof,
the users need to provide a (x, y) share which satisfies the line equation,
the users need to provide a `(x, y)` share which satisfies the line equation,
in order for their proof to be verified.
`x` is the hashed signal, while the `y` is the circuit output.
With more than one pair of unique shares, anyone can derive `a_0`, the `identity_secret_hash` .
The hash of a signal will be evaluation point `x`.
So that a member who sends more than one unique signal per `external_nullifier` risks their identity secret being revealed.
With more than one pair of unique shares, anyone can derive `a_0`, i.e. the `identity_secret_hash` .
The hash of a signal will be the evaluation point `x`.
In this way, a member who sends more than one unique signal per `external_nullifier` risks their identity secret being revealed.
Note that shares used for different external nullifiers and different RLN apps cannot be used to derive the secret key.
Note that shares used in different epochs and different RLN apps cannot be used to derive the identity secret hash.
Thanks to the `external_nullifier` definition, also shares computed from same secret within same epoch but in different RLN apps cannot be used to derive the identity secret hash.
The `rln_identifier` is a random value from a finite field,
unique per RLN app,
@ -410,14 +426,11 @@ then their user signals can be grouped by the `internal_nullifier` which could l
This is because two separate signals under the same `internal_nullifier` can be treated as rate limiting violation.
With adding the `rln_identifier` field we obscure the `internal_nullifier`,
so this kind of attack can be hardened because we don't have the same `internal_nullifier` 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 `x` and `y` shares for different `internal_nullifier`s.
## Identity credentials generation
In order to be able to generate valid proofs, the users need to be part of the identity membership merkle tree.
They are part of the identity membership merkle tree if their `identity_commitment` is placed in a leaf in the tree.
In order to be able to generate valid proofs, the users need to be part of the identity membership Merkle tree.
They are part of the identity membership Merkle tree if their `identity_commitment` is placed in a leaf in the tree.
The identity credentials of a user are composed of:
@ -518,85 +531,108 @@ Also for RLN we do a single secret component input for the circuit.
Thus we need to hash the secret array (two components) to a secret hash,
and we use that as a secret component input.
# Apendix C: Auxiliary tooling
# Appendix C: Auxiliary tooling
There are few additional tools implemented for easier integrations and usage of the RLN protocol.
[`zerokit`](https://github.com/vacp2p/zerokit) is a set of Zero Knowledge modules, written in Rust and designed to be used in many different environments.
Among different modules, it supports `Semaphore` and `RLN`.
[`zk-kit`](https://github.com/appliedzkp/zk-kit) is a typescript library which exposes APIs for identity credentials generation,
as well as proof generation.
It supports various protocols (`Semaphore`, `RLN`),.
It supports various protocols (`Semaphore`, `RLN`).
[`zk-keeper`](https://github.com/akinovak/zk-keeper) is a browser plugin which allows for safe credential storing and proof generation.
You can think of MetaMask for ZK-Proofs.
It uses `zk-kit` under the hood.
# Apendix D: Example usage
# Appendix D: Example usage
The following examples are code snippets using the `zk-kit` library.
The examples are written in [typescript](https://www.typescriptlang.org/).
The following examples are code snippets using the `zerokit` RLN module.
The examples are written in [rust](https://www.rust-lang.org/).
## Creating a RLN object
```rust
use rln::protocol::*;
use rln::public::*;
use std::io::Cursor;
// We set the RLN parameters:
// - the tree height;
// - the circuit resource folder (requires a trailing "/").
let tree_height = 20;
let resources = Cursor::new("../zerokit/rln/resources/tree_height_20/");
// We create a new RLN instance
let mut rln = RLN::new(tree_height, resources);
```
## Generating identity credentials
```typescript
import { ZkIdentity } from "@zk-kit/identity"
```rust
// We generate an identity tuple
let mut buffer = Cursor::new(Vec::<u8>::new());
rln.extended_key_gen(&mut buffer).unwrap();
const identity = new ZkIdentity()
const identityCommitment = identity.genIdentityCommitment()
const secretHash = identity.getSecretHash()
// We deserialize the keygen output to obtain
// the identiy_secret and id_commitment
let (identity_trapdoor, identity_nullifier, identity_secret_hash, id_commitment) = deserialize_identity_tuple(buffer.into_inner());
```
## Adding ID commitment to the RLN Merkle tree
```rust
// We define the tree index where id_commitment will be added
let id_index = 10;
// We serialize id_commitment and pass it to set_leaf
let mut buffer = Cursor::new(serialize_field_element(id_commitment));
rln.set_leaf(id_index, &mut buffer).unwrap();
```
## Setting epoch and signal
```rust
// 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 set our signal
let signal = b"RLN is awesome";
```
## Generating proof
```typescript
import { RLN, MerkleProof, FullProof, genSignalHash, generateMerkleProof, genExternalNullifier } from '@zk-kit/protocols'
import { ZkIdentity } from '@zk-kit/identity'
import { bigintToHex, hexToBigint } from 'bigint-conversion'
const ZERO_VALUE = BigInt(0);
const TREE_DEPTH = 15;
const LEAVES_PER_NODE = 2;
const LEAVES = [...]; // leaves should be an array of the leaf values of the membership merkle tree
// the identity commitment generated below should be present in the LEAVES array
```rust
// We prepare input to the proof generation routine
let proof_input = prepare_prove_input(identity_secret, id_index, epoch, signal);
// this is for illustrative purposes only. The identityCommitment should be present in the LEAVES array above.
const identity = new ZkIdentity()
const secretHash = identity.getSecretHash()
const identityCommitment = identity.genIdentityCommitment()
// 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();
const signal = "hey"
const signalHash = genSignalHash(signal)
const epoch = genExternalNullifier("test-epoch")
const rlnIdentifier = RLN.genIdentifier()
const merkleProof = generateMerkleProof(TREE_DEPTH, ZERO_VALUE, LEAVES_PER_NODE, LEAVES, identityCommitment)
const witness = RLN.genWitness(secretHash, merkleProof, epoch, signal, rlnIdentifier)
const [y, nullifier] = RLN.calculateOutput(secretHash, BigInt(epoch), rlnIdentifier, signalHash)
const publicSignals = [y, merkleProof.root, nullifier, signalHash, epoch, rlnIdentifier]
const wasmFilePath = path.join(zkeyFiles, "rln", "rln.wasm") // path to the WASM compiled circuit
const finalZkeyPath = path.join(zkeyFiles, "rln", "rln_final.zkey") // path to the prover key
const fullProof = await RLN.genProof(witness, wasmFilePath, finalZkeyPath)
// We get the public outputs returned by the circuit evaluation
let proof_data = out_buffer.into_inner();
```
## Verifiying proof
```typescript
import { RLN } from '@zk-kit/protocols'
// Public signal and the proof are received from the proving party
// const publicSignals = [y, merkleProof.root, nullifier, signalHash, epoch, rlnIdentifier]
// const proof = (await RLN.genProof(witness, wasmFilePath, finalZkeyPath)).proof
```rust
// We prepare input to the proof verification routine
let verify_data = prepare_verify_input(proof_data, signal);
const vkeyPath = path.join(zkeyFiles, "rln", "verification_key.json") // Path to the verifier key
const vKey = JSON.parse(fs.readFileSync(vkeyPath, "utf-8")) // The verifier key
// 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();
const response = await RLN.verifyProof(vKey, { proof: proof, publicSignals })
// We ensure the proof is valid
assert!(verified);
```
For more details please visit the [`zk-kit`](https://github.com/appliedzkp/zk-kit) library.
For more details please visit the [`zerokit`](https://github.com/vacp2p/zerokit) library.
# Copyright