Awesome
RollupNC_tutorial
This is a circom and snarkjs tutorial, using RollupNC as an example. It takes you through how to build RollupNC, circuit by circuit, with generated inputs to test the circuits out.
(Created for IC3 2019 and inspired by GuthL's rollup circom tutorial.)
Setting up this tutorial
- We are using
node v10.16.0
, which you can possibly install using nvm - Clone this repo:
git clone https://github.com/therealyingtong/RollupNC_tutorial
- Clone the submodules:
git submodule update --init --recursive
- this should clone
circomlib
. We are using v0.0.6 for this tutorial. To make sure we're using the same commit, dogit checkout 77928872169b7179c9eee545afe0a972d15b1e64
in thecircomlib
repository.
- Install npm packages in both the root repository and the
circomlib
submodule:npm i
NB: there's a circom syntax highlighter in VS code! otherwise one can make do with C#
highlighting.
Exercises
Simple arithmetic constraints
cd 1_simple_arithmetic
This is a contrived example to familiarise ourselves with the syntax of circom
and how it works with snarkjs
.
Let's write a circuit to check:
- that the sum of two private inputs
a + b
is equal to a public inputc
; - that the product
b * c
is equal to private inputd
;
Create a new file named circuit.circom
with the following content:
template SimpleChecks() {
signal private input a;
signal private input b;
signal input c;
signal private input d;
signal output out;
// force a + b = c
a + b === c;
// force b * c = d
// fill this in
// output c + d
out <== c + d;
}
component main = SimpleChecks();
NB: there's a circom syntax highlighter in VS code! otherwise one can make do with C#
highlighting.
-
Compile your circuit
circom circuit.circom -o circuit.json
. -
Generate your input
node generate_circuit_input.js
(generatesinput.json
). -
Calculate the witness
snarkjs calculatewitness -c circuit.json -i input.json
. This generateswitness.json
. -
Perform the trusted setup to get your
proving_key.json
andverification_key.json
:snarkjs setup -c circuit.json --protocol groth
. -
Generate the proof
snarkjs proof -w witness.json --pk proving_key.json
. This generatesproof.json
andpublic.json
. -
Verify the proof
snarkjs verify
.
Challenge
Modify the circuit and input to take in length-4 arrays of a
, b
, c
, and d
, and perform the checks in a for
loop. Output the sums of c
and d
arrays. To get you started:
template SimpleChecks(k) {
signal private input a[k];
signal private input b[k];
signal input c[k];
signal private input d[k];
signal output out;
var sum = 0;
for (var i = 0; i < k; i++){
// force a + b = c
a[i] + b[i] === c[i];
// force b * c = d
// fill this in
// add up c and d arrays
// use the variable 'sum' defined outside the for loop
}
// output sum of c and d arrays
out <== sum;
}
component main = SimpleChecks(4);
Verifying an EdDSA signature
cd 2_verify_eddsa
This example works with useful libraries in circomlib
. Note: we are using v0.0.6 of circomlib
.
Create a new file named circuit.circom
with the following content:
include "../circomlib/circuits/eddsamimc.circom";
template VerifyEdDSAMiMC() {
signal input from_x;
signal input from_y;
signal input R8x;
signal input R8y;
signal input S;
signal input M;
component verifier = EdDSAMiMCVerifier();
verifier.enabled <== 1;
verifier.Ax <== from_x;
verifier.Ay <== from_y;
verifier.R8x <== R8x
verifier.R8y <== R8y
verifier.S <== S;
verifier.M <== M;
}
component main = VerifyEdDSAMiMC();
Generate your input node generate_circuit_input.js
(generates input.json
).
You know the drill from here!
Challenge
Modify the circuit and input to take in a length-3 preimage of the message as a private input, and hash them inside the circuit. To get you started:
include "../circomlib/circuits/eddsamimc.circom";
include "../circomlib/circuits/mimc.circom";
template VerifyEdDSAMiMC(k) {
signal input from_x;
signal input from_y;
signal input R8x;
signal input R8y;
signal input S;
signal private input preimage[k];
component M = MultiMiMC7(k,91);
M.in[0] <== // the first element of your preimage
M.in[1] <== // the second element of your preimage
M.in[2] <== // the third element of your preimage
component verifier = EdDSAMiMCVerifier();
verifier.enabled <== 1;
verifier.Ax <== from_x;
verifier.Ay <== from_y;
verifier.R8x <== R8x;
verifier.R8y <== R8y;
verifier.S <== S;
verifier.M <== M.out;
}
component main = VerifyEdDSAMiMC(3);
Verifying a Merkle proof
cd 3_verify_merkle
This example shows how to write smaller templates and use them as components in the main circuit. To verify a Merkle proof, we need to take in a leaf and its Merkle root, along with the path from the leaf to the root. Let's break this down into two circuits:
get_merkle_root.circom
: this takes a leaf and a Merkle path and returns the computed Merkle root.leaf_existence.circom
: this compares an expected Merkle root with a computed Merkle root.
Create new file named get_merkle_root.circom
and paste this code in:
include "../circomlib/circuits/mimc.circom";
template GetMerkleRoot(k){
// k is depth of tree
signal input leaf;
signal input paths2_root[k];
signal input paths2_root_pos[k];
signal output out;
// hash of first two entries in tx Merkle proof
component merkle_root[k];
merkle_root[0] = MultiMiMC7(2,91);
merkle_root[0].in[0] <== leaf - paths2_root_pos[0]* (leaf - paths2_root[0]);
merkle_root[0].in[1] <== paths2_root[0] - paths2_root_pos[0]* (paths2_root[0] - leaf);
// hash of all other entries in tx Merkle proof
for (var v = 1; v < k; v++){
merkle_root[v] = MultiMiMC7(2,91);
merkle_root[v].in[0] <== paths2_root[v] - paths2_root_pos[v]* (paths2_root[v] - merkle_root[v-1].out);
merkle_root[v].in[1] <== //can you figure this one out?
}
// output computed Merkle root
out <== merkle_root[k-1].out;
}
component main = GetMerkleRoot(2);
Try to fill in the second line of the for
loop using the pattern from the lines before. (The solution is in sample_get_merkle_root.circom
.)
Now, make the second file leaf_existence.circom
and paste this in:
include "./get_merkle_root.circom";
// checks for existence of leaf in tree of depth k
template LeafExistence(k){
// k is depth of tree
signal input leaf;
signal input root;
signal input paths2_root_pos[k];
signal input paths2_root[k];
component computed_root = GetMerkleRoot(k);
computed_root.leaf <== leaf;
for (var w = 0; w < k; w++){
computed_root.paths2_root[w] <== // assign elements from paths2_root
computed_root.paths2_root_pos[w] <== // assign elements from paths2_root_pos
}
// equality constraint: input tx root === computed tx root
root === computed_root.out;
}
component main = LeafExistence(2);
Make sure to REMOVE component main = GetMerkleRoot(2)
from get_merkle_root.circom
.
Modify your input to work with leaf_existence.circom
.
Challenge
Like you did in the EdDSA verification exercise, provide the preimage of the leaf hash as private inputs to leaf_existence.circom
, and hash them in the circuit.
Processing a single transaction
Let's define a transaction as:
class Transaction = {
from: eddsa_pubkey,
to: eddsa_pubkey,
amount: integer
}
and an account as:
class Account = {
pubkey: eddsa_pubkey,
balance: integer
}
NB: we also have a nonce for protection against replay attacks, but for simplicity let's consider it in the next example.
In RollupNC, processing a single transaction involves:
- checking that the sender account existsin a tree of accounts,
accounts_root
- checking that the hash of the transaction was signed by the sender
- debiting the sender account
- updating the
accounts_root
to getintermediate_root
- checking that the receiver account exists in
intermediate_root
- crediting the receiver account
- updating the
accounts_root
to getfinal_root
Create a file called circuit.circom
and put in this code. Fill in the signals for each component. Then, compile your circuit and test it against the input.json
generated by running node generate_circuit_input.js
.
include "./leaf_existence.circom";
include "./verify_eddsamimc.circom";
include "./get_merkle_root.circom";
include "../circomlib/circuits/mimc.circom";
template ProcessTx(k){
// k is depth of accounts tree
// accounts tree info
signal input accounts_root;
signal private input intermediate_root;
signal private input accounts_pubkeys[2**k, 2];
signal private input accounts_balances[2**k];
// transactions info
signal private input sender_pubkey[2];
signal private input sender_balance;
signal private input receiver_pubkey[2];
signal private input receiver_balance;
signal private input amount;
signal private input signature_R8x;
signal private input signature_R8y;
signal private input signature_S;
signal private input sender_proof[k];
signal private input sender_proof_pos[k];
signal private input receiver_proof[k];
signal private input receiver_proof_pos[k];
// output
signal output new_accounts_root;
// verify sender account exists in accounts_root
component senderExistence = LeafExistence(k, 3);
senderExistence.preimage[0] <== sender_pubkey[0];
senderExistence.preimage[1] <== sender_pubkey[1];
senderExistence.preimage[2] <== sender_balance;
senderExistence.root <== accounts_root;
for (var i = 0; i < k; i++){
senderExistence.paths2_root_pos[i] <== sender_proof_pos[i];
senderExistence.paths2_root[i] <== sender_proof[i];
}
// check that transaction was signed by sender
component signatureCheck = VerifyEdDSAMiMC(5);
signatureCheck.from_x <== sender_pubkey[0];
signatureCheck.from_y <== sender_pubkey[1];
signatureCheck.R8x <== signature_R8x;
signatureCheck.R8y <== signature_R8y;
signatureCheck.S <== signature_S;
signatureCheck.preimage[0] <== sender_pubkey[0];
signatureCheck.preimage[1] <== sender_pubkey[1];
signatureCheck.preimage[2] <== receiver_pubkey[0];
signatureCheck.preimage[3] <== receiver_pubkey[1];
signatureCheck.preimage[4] <== amount;
// debit sender account and hash new sender leaf
component newSenderLeaf = MultiMiMC7(3,91){
newSenderLeaf.in[0] <== sender_pubkey[0];
newSenderLeaf.in[1] <== sender_pubkey[1];
newSenderLeaf.in[2] <== sender_balance - amount;
}
// update accounts_root
component computed_intermediate_root = GetMerkleRoot(k);
computed_intermediate_root.leaf <== newSenderLeaf.out;
for (var i = 0; i < k; i++){
computed_intermediate_root.paths2_root_pos[i] <== sender_proof_pos[i];
computed_intermediate_root.paths2_root[i] <== sender_proof[i];
}
// check that computed_intermediate_root.out === intermediate_root
computed_intermediate_root.out === intermediate_root;
// verify receiver account exists in intermediate_root
component receiverExistence = LeafExistence(k, 3);
// provide the appropriate signals to this component! see senderExistence for reference
// credit receiver account and hash new receiver leaf
component newReceiverLeaf = MultiMiMC7(3,91){
// provide the appropriate signals to this component! see newSenderLeaf for reference
}
// update accounts_root
component computed_final_root = GetMerkleRoot(k);
// provide the appropriate signals to this component! see computed_intermediate_root for reference
// output final accounts_root
new_accounts_root <== computed_final_root.out;
}
component main = ProcessTx(1);
Processing multiple transactions
Processing multiple transactions requires us to update the accounts_root
many times before we arrive at the final one. This means we have to pre-compute all the intermediate_roots
and pass them to the circuit to use in validating Merkle proofs.
Check out https://github.com/therealyingtong/RollupNC/blob/master/snark_circuit/multiple_tokens_transfer_and_withdraw.circom to see how it was implemented.
If conditions and comparators
If conditions and comparators
Although circom's parser (see parser/jaz.jison) supports if
statements, because circom compiles the DSL into
arithmetic circuits, circuits whose behavior depends on the value of an input can have unexpected behavior.
For example, consider the following example
template BadForceEqualIfEnabled() {
signal input enabled;
signal input in[2];
if (enabled) {
in[1] === in[0]
}
}
component main BadForceEqualIfEnabled()
First compile the circuit:
circom circuit.circom -o circuit.json
Create the input.json
file:
{"enabled": 0, "in": [1,2]}
snarkjs calculatewitness
Now, in witness.json, change the enabled flag (the second array element) to 1. (If it was set in the previous step the compiler will not calculate a witness because it sees a constraint that cannot be satisfied.)
snarkjs setup --protocol groth
snarkjs proof --protocol groth
snarkjs verify
Should get INVALID - we get OK in the bad case.
As an exercise, implement the circuit properly (or refer to circuits/circomlib/comparators.circom).
{"enabled": 0, "in": [1,2]}