mirror of
https://github.com/Rate-Limiting-Nullifier/rln-docs.git
synced 2026-01-09 15:28:03 -05:00
docs(sss): add interpolation explanation
This commit is contained in:
@@ -10,6 +10,7 @@ edition = "2021"
|
||||
|
||||
[output.html]
|
||||
additional-js = ["mermaid.min.js", "mermaid-init.js"]
|
||||
mathjax-support = true
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 15
|
||||
|
||||
BIN
src/images/graph1.png
Normal file
BIN
src/images/graph1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
src/images/line.png
Normal file
BIN
src/images/line.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -20,36 +20,34 @@ Well, let's discuss them.
|
||||
## User registration
|
||||
The first part of **RLN** is registration. There is nothing special in **RLN** registration; it's almost the same process as in other protocols/apps with anonymous environments: we need to create a Merkle Tree, and every participant must submit a `commitment` and place it in the Merkle Tree, and after that to interact with the app every participant will create a zkProof's, that they are a *member of the tree* (we use an *Incremental Merkle Tree*, as it more *GAS efficient*).
|
||||
|
||||
So, each member generates a secret key, denoted by `a_0`. Identity commitment `q` is the hash (Poseidon) of the secret key: `q = Poseidon(a_0)`.
|
||||
So, each member generates a secret key, denoted by \\(a_0\\). Identity commitment \\(q\\) is the hash (Poseidon) of the secret key: \\(q = Poseidon(a_0)\\).
|
||||
|
||||
**RLN** wouldn't work if there were no punishment for spam; that's why to become a member, a user has to register and provide something at stake. So, whoever has our `a_0` can "slash" us.
|
||||
**RLN** wouldn't work if there were no punishment for spam; that's why to become a member, a user has to register and provide something at stake. So, whoever has our \\(a_0\\) can "slash" us.
|
||||
|
||||
The slight difference is that we must enable a *secret sharing* scheme (to split the `commitment` into parts). We need to come up with a polynomial. For simplicity we use linear polynomial (e.g. `f(x) = kx + b`). Therefore, with two points, we can reconstruct the polynomial and recover the secret.
|
||||
The slight difference is that we must enable a *secret sharing* scheme (to split the `commitment` into parts). We need to come up with a polynomial. For simplicity we use linear polynomial (e.g. \\(f(x) = kx + b\\). Therefore, with two points, we can reconstruct the polynomial and recover the secret.
|
||||
|
||||
Our polynomial will be: `A(x) = (a_0, a_1)`, where `a_1 = Poseidon(a_0, epoch)`.
|
||||
Our polynomial will be: \\(A(x) = a_1 * x + a_0\\), where \\(a_1 = Poseidon(a_0, epoch)\\).
|
||||
|
||||
Less strict: `A(x) = a_1 * x + a_0`.
|
||||
|
||||
`epoch` is a simple identifier (also called *external nullifier*). And each epoch, there is a polynomial with new `a_1` and the same `a_0`.
|
||||
`epoch` is a simple identifier (also called *external nullifier*). And each epoch, there is a polynomial with new \\(a_1\\) and the same \\(a_0\\).
|
||||
|
||||
## Signalling
|
||||
Now that the user is registered, he wants to interact with the system. Imagine that the system is an *anonymous chat* and the interaction is the sending of messages.
|
||||
So, to send a message user have to come up with *share* - the point `(x, y)` on her polynomial.
|
||||
We denote: `x = Poseidon(message), and y = A(x)`.
|
||||
So, to send a message user have to come up with *share* - the point \\((x, y)\\) on her polynomial.
|
||||
We denote: \\(x = Poseidon(message), y = A(x)\\).
|
||||
|
||||
Thus, if the same epoch user sends more than one message, their polynomial and, therefore, their secret (`a_0`) can be recovered.
|
||||
Thus, if the same epoch user sends more than one message, their polynomial and, therefore, their secret (\\(a_0\\)) can be recovered.
|
||||
|
||||
Of course, we somehow must prove that our `share = (x, y)` is valid (that this is really a point on our `polynomial = A(x)`), as well as we must prove other things are valid too, that's why we use zkSNARK. An explanation of the zk-circuits can be found in the next topic.
|
||||
Of course, we somehow must prove that our *share* = \\((x, y)\\) is valid (that this is really a point on our `polynomial = A(x)`), as well as we must prove other things are valid too, that's why we use zkSNARK. An explanation of the zk-circuits can be found in the next topic.
|
||||
|
||||
## Slashing
|
||||
As it's been said, if a user sends more than one message, everyone else will be able to recover his secret, slash them and take their stake.
|
||||
|
||||
## Some important notes
|
||||
There are also `nullifier` and `rln_identifier`, which can be found in the **RLN** protocol/circuits.
|
||||
There are also `nullifier` and `rln-identifier`, which can be found in the **RLN** protocol/circuits.
|
||||
|
||||
So, `rln_identifier` is just a random value that's unique per **RLN** app. It's used for additional cross-application security - to protect the user secrets from being compromised if they use the same credentials across different **RLN** apps. If `rln_identifier` is not present, the user uses the same credentials and sends a message in two different **RLN** apps using the same epoch, then their secret key can be revealed. Adding the `rln_identifier` field, we obscure the nullifier, so this kind of attack cannot happen. 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 nullifiers.
|
||||
So, `rln-identifier` is just a random value that's unique per **RLN** app. It's used for additional cross-application security - to protect the user secrets from being compromised if they use the same credentials across different **RLN** apps. If `rln-identifier` is not present, the user uses the same credentials and sends a message in two different **RLN** apps using the same epoch, then their secret key can be revealed. Adding the `rln-identifier` field, we obscure the nullifier, so this kind of attack cannot happen. 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 nullifiers.
|
||||
|
||||
Now, imagine there are a lot of users sending messages, and after each received message, we need to check if any member can be slashed. To do this, we can use all combinations of received *shares* and try to recover the polynomial, but this is a naive and non-optimal approach. Suppose we have a mechanism that will tell us about the connection between a person and their messages while not revealing their identity. In that case, we can solve this without brute-forcing all possibilities by using a public `nullifier` (`nullifier = Poseidon(a_1, rln_identifier)`), so if a user sends more than one message, it will be immediately visible to everyone.
|
||||
Now, imagine there are a lot of users sending messages, and after each received message, we need to check if any member can be slashed. To do this, we can use all combinations of received *shares* and try to recover the polynomial, but this is a naive and non-optimal approach. Suppose we have a mechanism that will tell us about the connection between a person and their messages while not revealing their identity. In that case, we can solve this without brute-forcing all possibilities by using a public `nullifier` (\\(Poseidon(a_1, rln-identifier)\\)), so if a user sends more than one message, it will be immediately visible to everyone.
|
||||
|
||||
Also, in our example (and [zk-chat](https://github.com/njofce/zk-chat) implementation), we use linear polynomial, but [SSS](sss.md) allows us to use various degree polynomials; therefore we can implement a protocol, where more than one signal (message) can be sent in per epoch.
|
||||
|
||||
|
||||
106
src/sss.md
106
src/sss.md
@@ -1,93 +1,41 @@
|
||||
# Shamir's Secret Sharing Scheme
|
||||
|
||||
*Shamirs Secret Sharing* allows to split the secret to `n` parts and restore it upon presentation any `m` parts (`m <= n`)
|
||||
*This topic is an explanation of **Shamir's Secret Sharing** scheme (**SSS**) also known as \\((k, n)\\) threshold secret sharing scheme. **SSS** is one of the key parts of **RLN** due to which we can share and restore the secret.*
|
||||
|
||||
[Sharmir's Secret Sharing wikipedia](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing) is a good reference to understand the concept.
|
||||
## Overview
|
||||
Imagine, if you have some important secret (secret key) and you don't want to store it anywhere. For that you can use *SSS* scheme. It allows you to split this secret into \\(n\\) parts (each individual part doesn't give any information about the secret) and restore this secret upon presentation of \\(k\\) \\((k <= n)\\) parts.
|
||||
|
||||
Reconstruction 1: https://github.com/akinovak/semaphore-lib/blob/5b9bb3210192c8e508eced7ef6579fd56e635ed0/src/rln.ts#L31
|
||||
```js
|
||||
retrievePrivateKey(x1: bigint, x2:bigint, y1:bigint, y2:bigint): Buffer | ArrayBuffer {
|
||||
const slope = Fq.div(Fq.sub(y2, y1), Fq.sub(x2, x1))
|
||||
const privateKey = Fq.sub(y1, Fq.mul(slope, x1));
|
||||
return bigintConversion.bigintToBuf(Fq.normalize(privateKey));
|
||||
}
|
||||
```
|
||||
For example, you have a secret and you want to split it into \\(n\\) parts/shares. You can divide these shares between your friends (1 share to 1 friend). Now when \\(k\\) of your friends reveal their share you can restore the secret.
|
||||
|
||||
Reconstruction 2: https://github.com/akinovak/semaphore-lib/blob/rln_signature_changes/test/index.ts#L250
|
||||
This scheme is also called \\((k, n)\\) *threshold secret sharing scheme*.
|
||||
|
||||
```js
|
||||
async function testRlnSlashingSimulation() {
|
||||
RLN.setHasher('poseidon');
|
||||
const identity = RLN.genIdentity();
|
||||
const privateKey = identity.keypair.privKey;
|
||||
This scheme is possible due to *polynomial interpolation* (especially Lagrange interpolation). Let's describe how *Lagrange interpolation* works and then how it's used in *SSS* scheme.
|
||||
|
||||
const leafIndex = 3;
|
||||
const idCommitments: Array<any> = [];
|
||||
## Polynomial (Lagrange) interpolation
|
||||
|
||||
for (let i=0; i<leafIndex;i++) {
|
||||
const tmpIdentity = OrdinarySemaphore.genIdentity();
|
||||
const tmpCommitment: any = RLN.genIdentityCommitment(identity.keypair.privKey);
|
||||
idCommitments.push(tmpCommitment);
|
||||
}
|
||||
*Interpolation* is a method of constructing (or restoring) new points/values (or function) based on the range of a set of known points/values (f.e. we can restore the line (linear function) from two points, that are from this line). Previous example actually describes how that works.
|
||||
<p align="center">
|
||||
<img src="./images/graph1.png" width="300">
|
||||
</p>
|
||||
<p align="center">
|
||||
<i>An unlimited number of parabolas (second degree polynomials) can be drawn through two points. To choose the only one, you need a third point.</i>
|
||||
</p>
|
||||
|
||||
idCommitments.push(RLN.genIdentityCommitment(privateKey))
|
||||
Thus, if we have a polynomial \\(f(x) = 3x + 2\\) we only need two points from this polynomial to restore it. Let's peek two random \\(x\\) values and calculate \\(f(x)\\):
|
||||
* For \\(x = 1\\) we have \\(f(1) = 3 * 1 + 2 = 5\\)
|
||||
* For \\(x = 10\\) we have \\(f(10) = 32\\)
|
||||
|
||||
const signal = 'hey hey';
|
||||
const x1: bigint = OrdinarySemaphore.genSignalHash(signal);
|
||||
const epoch: string = OrdinarySemaphore.genExternalNullifier('test-epoch');
|
||||
Now we have to shares: \\((1, 5)\\) and \\((10, 32)\\). If we draw a graph based on these two shares, we can easily see that this is the same line (function):
|
||||
<p align="center">
|
||||
<img src="./images/line.png" width="500" height="400">
|
||||
</p>
|
||||
|
||||
const vkeyPath: string = path.join('./rln-zkeyFiles', 'verification_key.json');
|
||||
const vKey = JSON.parse(fs.readFileSync(vkeyPath, 'utf-8'));
|
||||
We also can "restore" the function analytically. For that let's denote: \\[f(x) = y_1 * \frac{x - x_2}{x_1 - x_2} + y_2 * \frac{x - x_1}{x_2 - x_1}\\]
|
||||
where \\(x_1 = 5, x_2 = 10, y_1 = 5, y_2 = 32\\). If we make substitution we got: \\[f(x) = 3x + 2 \\]
|
||||
which is the same polynomial.
|
||||
|
||||
const wasmFilePath: string = path.join('./rln-zkeyFiles', 'rln.wasm');
|
||||
const finalZkeyPath: string = path.join('./rln-zkeyFiles', 'rln_final.zkey');
|
||||
The same techique can be made with every polynomial. Main thing to remember is that we need \\(n + 1\\) points to interpolate \\(n\\)-degree polynomial.
|
||||
|
||||
const witnessData: IWitnessData = await RLN.genProofFromIdentityCommitments(privateKey, epoch, signal, wasmFilePath, finalZkeyPath, idCommitments, 15, BigInt(0), 2);
|
||||
Now that we know how interpolation works, we can learn how it is used in SSS.
|
||||
|
||||
const a1 = RLN.calculateA1(privateKey, epoch);
|
||||
const y1 = RLN.calculateY(a1, privateKey, x1);
|
||||
const nullifier = RLN.genNullifier(a1);
|
||||
|
||||
const pubSignals = [y1, witnessData.root, nullifier, x1, epoch];
|
||||
|
||||
let res = await RLN.verifyProof(vKey, { proof: witnessData.fullProof.proof, publicSignals: pubSignals })
|
||||
if (res === true) {
|
||||
console.log("Verification OK");
|
||||
} else {
|
||||
console.log("Invalid proof");
|
||||
return;
|
||||
}
|
||||
|
||||
const signalSpam = "let's try spamming";
|
||||
const x2: bigint = OrdinarySemaphore.genSignalHash(signalSpam);
|
||||
|
||||
const witnessDataSpam: IWitnessData = await RLN.genProofFromIdentityCommitments(privateKey, epoch, signalSpam, wasmFilePath, finalZkeyPath, idCommitments, 15, BigInt(0), 2);
|
||||
|
||||
const a1Spam = RLN.calculateA1(privateKey, epoch);
|
||||
const y2 = RLN.calculateY(a1Spam, privateKey, x2);
|
||||
const nullifierSpam = RLN.genNullifier(a1Spam);
|
||||
|
||||
const pubSignalsSpam = [y2, witnessDataSpam.root, nullifierSpam, x2, epoch];
|
||||
|
||||
res = await RLN.verifyProof(vKey, { proof: witnessDataSpam.fullProof.proof, publicSignals: pubSignalsSpam })
|
||||
if (res === true) {
|
||||
console.log("Spam proof Verification OK");
|
||||
} else {
|
||||
console.log("Invalid proof");
|
||||
return;
|
||||
}
|
||||
|
||||
const identitySecret = RLN.calculateIdentitySecret(privateKey);
|
||||
|
||||
const retreivedPkey = bigintConversion.bufToBigint(RLN.retrievePrivateKey(x1, x2, y1, y2));
|
||||
|
||||
|
||||
if(Fq.eq(identitySecret, retreivedPkey)) {
|
||||
console.log("PK successfully reconstructed");
|
||||
} else {
|
||||
console.log("Error while reconstructing private key")
|
||||
}
|
||||
|
||||
// TODO: Add removal from tree example
|
||||
}
|
||||
```
|
||||
## Shamir's Secret Sharing
|
||||
|
||||
@@ -33,11 +33,11 @@ There are a number of use-cases for **RLN**, such as voting applications (1 vote
|
||||
The general anti-spam rule is usually in the form of:
|
||||
`Users must not make more than X interactions per epoch.`
|
||||
|
||||
The epoch can be translated as a time interval of `Y` units of time unit `Z.` For simplicity's sake, let's transform the rule into: `Users must not send more than one message per second.
|
||||
The epoch can be translated as a time interval of `Y` units of time unit `Z`. For simplicity's sake, let's transform the rule into: `Users must not send more than one message per second.
|
||||
|
||||
We can implement this using `Shamir's Secret Sharing` scheme ([*read more*](./sss.md)), which allows you to split a secret (f.e. to `n` parts) and recover it when any `m' of `n` parts ('m <= n`) are presented.
|
||||
We can implement this using *Shamir's Secret Sharing* scheme ([*read more*](./sss.md)), which allows you to split a secret (f.e. to `n` parts) and recover it when any `m` of `n` parts `(m <= n)` are presented.
|
||||
|
||||
Thus, users have to split their `secret_key` into `n` parts, and for every interaction, they have to reveal the new part of the `secret_key.` So, in addition to proving the membership in the `Merkle Tree,` users have to prove that the revealed part is truly the part of their `secret_key.`
|
||||
Thus, users have to split their `secret_key` into `n` parts, and for every interaction, they have to reveal the new part of the `secret_key.` So, in addition to proving the membership in the *Merkle Tree*, users have to prove that the revealed part is truly the part of their `secret_key.`
|
||||
|
||||
If they make more interactions than allowed per epoch, their secret key can be fully reconstructed.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user