Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/GuthL/roll_up_circom_tutorial
This repository contains a tutorial on how to build roll_up with Circom
https://github.com/GuthL/roll_up_circom_tutorial
Last synced: 3 months ago
JSON representation
This repository contains a tutorial on how to build roll_up with Circom
- Host: GitHub
- URL: https://github.com/GuthL/roll_up_circom_tutorial
- Owner: GuthL
- License: gpl-3.0
- Created: 2019-03-04T14:28:19.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2019-04-09T13:21:32.000Z (almost 6 years ago)
- Last Synced: 2024-08-01T18:24:42.781Z (6 months ago)
- Language: JavaScript
- Homepage:
- Size: 49.8 KB
- Stars: 83
- Watchers: 3
- Forks: 20
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
- awesome-circom - Building roll ups in circom
README
# Roll\_up tutorial, a layer 1 SNARK-based scalability solution for Ethereum
## Introduction
roll\_up is a name for the pattern of performing merkle tree updates, signature validations inside a succinct proof system. This allows us to make dapps with throughput of between 100tps and 37000 tps on ethereum today.
This has a transformative scaling implications. We can do 500 tps\* and still maintain data availability guarantees of Ethereum. We end up including with our snark a diff between state t and state t+1 as well as a proof that the transition from t to t+1 is correct.
![](https://i.imgur.com/E5oDG1a.png)
### Data availability options
In a bunch of contexts we don't need to have all this data available. For example, we could build a non-custodial exchange where the exchange operator is able to deprive me of access to my funds, which would still be a strict improvement over centralized exchanges. There are a bunch of less critical applications that can enter this model and simply do a redeployment if this attack happens. For example crypto kitties, on-chain twitter would be good candidates for this kind of approach.If we remove the need to have data availability on chain, we will be able to reach 8000 tps. If we weaken our assumptions further and stake the operator and slash them if they ever publish a proof that is invalid, we can reduce the gas costs from 500k gas to the gas cost of putting a snark proof in storage. 288 bytes of storage space. 640k gas per kilo byte. So that means we can approach 34000 tps if we don't validate snarks or put data on chain. We only need to validate them if they are incorrect and then we can slash the operator.
The tools to build with snarks are improving to the point where you can make a mixer in a 3 day hackathon. You can also make roll_up style dapps.
Here we introduce you to the tools that circom provides. It gives a nice dev experience but still needs some work on the proving time optimizations. But it should be enough to play around with and if you want to go to prod at the hackathon we include some ideas about doing this in the disclaimer section.\* Note we ignore the cost of creating the snark proof and assume the operator is able to bear these costs. Which is less 100 USD per proof and is sub cent per transaction. This cost only needs to be paid by a single participant.
## Operator paradigm
We have a new paradigm where users create signatures and an operator create snarks that aggregate these signatures together and perform state transitions based upon the rules defined in the snark.
The state of the system is defined by a merkle root.
A snark takes the previous merkle root as an input performs some state transition defined by the snark and produces a new merkle root as the output. Our smart contract tracks this merkle root.
Inside our snark we define the rules of our state transition. It defines what state transitions are legal and illegal.
## Pre-requirements
Check out this circom intro https://github.com/iden3/circom/blob/master/TUTORIAL.md
```
npm install -g circom
npm install -g snarkjs
git clone https://github.com/iden3/circomlib
git clone https://github.com/GuthL/roll_up_circom_tutorial```
Move the scripts from this repository (roll_up_circom_tutorial/leaf_update, roll_up_circom_tutorial/signature_verification, roll_up_circom_tutorial/tokens_transfer) to the root of circomlib project.## Signature validation
We put a public key in our merkle tree and prove we have a signature that was created by that public key for a message of size 80 bits. In the root of the circomlib project, save the following snippet under eddsa_mimc_verifier.circom
```
include "./circuits/eddsamimc.circom";component main = EdDSAMiMCVerifier();
```
To generate the circuit usable by snarkjs, run:
```
circom eddsa_mimc_verifier.circom -o eddsa_mimc_verifier.cir
```From circomlib, you can use eddsa.js to generate an input. Copy the following snippet into a file named input.js. Then, run `node input.js` to generate the input.json which snarkjs recognises.
```
const eddsa = require("./src/eddsa.js");
const snarkjs = require("snarkjs");
const fs = require('fs');
var util = require('util');const bigInt = snarkjs.bigInt;
const msg = bigInt(9999);
const prvKey = Buffer.from("0000000000000000000000000000000000000000000000000000000000000001", "hex");
const pubKey = eddsa.prv2pub(prvKey);
const signature = eddsa.signMiMC(prvKey, msg);
const inputs = {
enabled: 1,
Ax: pubKey[0].toString(),
Ay: pubKey[1].toString(),
R8x: signature.R8[0].toString(),
R8y: signature.R8[1].toString(),
S: signature.S.toString(),
M: msg.toString()}fs.writeFileSync('./input.json', JSON.stringify(inputs) , 'utf-8');
```Then test your circuit by running the following command:
```
snarkjs calculatewitness -c eddsa_mimc_verifier.cir
```## Permissioned merkle tree update
So now lets say we want to update the leaf in the merkle tree
but the only let people update the leaf is if they have the current public key. The leaf index in the tree represents an NFT token owned a user.Save the following snippet under leaf_update.circom
```
include "./circuits/mimc.circom";
include "./circuits/eddsamimc.circom";
include "./circuits/bitify.circom";template Main(n) {
signal private input paths_to_root[n-1];signal input current_state;
signal input pubkey_x;
signal input pubkey_y;
signal input R8x;
signal input R8y;
signal input S;
signal input nonce;signal output out;
var i;
component old_hash = MultiMiMC7(3,91);
old_hash.in[0] <== pubkey_x;
old_hash.in[1] <== pubkey_y;
old_hash.in[2] <== nonce;
component old_merkle[n-1];
old_merkle[0] = MultiMiMC7(2,91);
old_merkle[0].in[0] <== old_hash.out;
old_merkle[0].in[1] <== paths_to_root[0];
for (i=1; i= token_balance_to;nonce_from != NONCE_MAX_VALUE;
token_type_from === token_type_to;// accounts updates
component new_hash_from = MultiMiMC7(4,91);
new_hash_from.in[0] <== pubkey_x;
new_hash_from.in[1] <== token_balance_from-amount;
new_hash_from.in[2] <== nonce_from+1;
new_hash_from.in[3] <== token_type_from;
component new_merkle_from[n-1];
new_merkle_from[0] = MultiMiMC7(2,91);
new_merkle_from[0].in[0] <== new_hash_from.out - paths2root_from_pos[0]* (new_hash_from.out - paths2new_root_from[0]);
new_merkle_from[0].in[1] <== paths2new_root_from[0] - paths2root_from_pos[0]* (paths2new_root_from[0] - new_hash_from.out);
for (i=1; i mapping (address => uint128 )) tokenBalances;
mapping (address => uint256) etherBalances;
mapping (address => PublicKey) userKeys;
uint256 userCount;uint256[][] publicKeyQueue;
uint256[] amountQueue;uint256 constant tokenId = 1;
uint256 state;
IDepositVerifier depositVerifier;
ITransferVerifier transferVerifier;
IWithdrawVerifier withdrawVerifier;
event KeyRegistered(address indexed user, uint256 x, uint256 y);
event Deposit(address indexed user, uint256 x, uint256 y, address indexed token, uint128 amount);
event Withdraw(address indexed user, address indexed token, uint128 amount);
event StateUpdated(uint256 oldState, uint256 newState);
event StateRejected(uint256 oldState, uint256 rejectedState);
// User Functionsfunction registerKey(uint256 x, uint256 y) public payable {
// require(userKeys[msg.sender] == 0, "User has existing key");
userKeys[msg.sender] = PublicKey(x,y);
emit KeyRegistered(msg.sender, x, y);
if (msg.value > 0) {
etherBalances[msg.sender] = msg.value;
}
}function getKey(address _user) public view returns(uint256, uint256) {
return (userKeys[_user].x, userKeys[_user].y);
}
function getMyKey() public view returns(uint256, uint256) {
return (userKeys[msg.sender].x, userKeys[msg.sender].y);
}
function depositEther() public payable {
etherBalances[msg.sender] = msg.value;
}
function withdrawEther() public payable {
msg.sender.transfer(etherBalances[msg.sender]);
}
// Requires approval for ERC20 token first
function depositToken(address _token, uint128 _amount) public {
require(tokenBalances[msg.sender][_token] == 0, "Only one deposit per token for now!");
require(ERC20(_token).allowance(msg.sender, address(this)) >= _amount, "Not enough allowance");
tokenBalances[msg.sender][_token].add(_amount);
addToQueue(userKeys[msg.sender], _amount);require(ERC20(_token).transferFrom(msg.sender, address(this), _amount), "Token transfer failed");
emit Deposit(msg.sender, userKeys[msg.sender].x, userKeys[msg.sender].y, _token, _amount);
}function addToQueue(PublicKey memory _publicKey, uint256 _amount) internal {
// If zero slot empty
if (publicKeyQueue[0][0] == 0) {
publicKeyQueue[0][0] = _publicKey.x;
publicKeyQueue[0][1] = _publicKey.y;
amountQueue[0] = _amount;
}
// If one slot empty
else if (publicKeyQueue[1][0] == 0) {
publicKeyQueue[1][0] = _publicKey.x;
publicKeyQueue[1][1] = _publicKey.y;
amountQueue[1] = _amount;
}else {
revert("No space in Queue!");
}
}function clearQueueIndex(uint256 _index) internal {
require(_index <= 2, "Only 2 index slots!");
publicKeyQueue[_index][0] = 0;
publicKeyQueue[_index][1] = 0;
amountQueue[_index] = 0;
}
function withdrawToken(address _token, address _recipient, uint128 _amount, uint256 _proof, uint256 _newState) public onlyOwner() {
//TODO: We will get this value from the state
require(tokenBalances[msg.sender][_token] >= _amount, "Attempt to withdraw more than user has");tokenBalances[msg.sender][_token].sub(_amount);
require(ERC20(_token).transfer(msg.sender, _amount), "Token transfer failed");
emit Withdraw(msg.sender, _token, _amount);
}// State Functions
function getState() public view returns(uint256) {
return state;
}function setState(uint256 _newState) public onlyOwner() {
state = _newState;
}// Input = Proof
function verifyDeposit(uint[2] memory a, uint[2][2] memory b, uint[2] memory c, uint256 _newState) public onlyOwner() returns(bool) {
uint256 _newUserCount = userCount.add(2);uint256[12] memory input = [
state,
userCount,
publicKeyQueue[0][0],
publicKeyQueue[0][1],
publicKeyQueue[1][0],
publicKeyQueue[1][1],
amountQueue[0],
amountQueue[1],
tokenId,
tokenId,
_newState,
_newUserCount
];require(depositVerifier.verifyProof(a, b, c, input));
clearQueueIndex(0);
clearQueueIndex(1);
}function setDepositVerifier(address _newAddress) public onlyOwner() {
depositVerifier = IDepositVerifier(_newAddress);
}function setTransferVerifier(address _newAddress) public onlyOwner() {
transferVerifier = ITransferVerifier(_newAddress);
}function setWithdrawVerifier(address _newAddress) public onlyOwner() {
withdrawVerifier = IWithdrawVerifier(_newAddress);
}
}
```## Prover race conditions
The prover takes x seconds to create a proof. Therefore we need the merkle root to be the same at the end of the proof as at the start.
So we need to stagger the depsoits and withdraws that change the token balances.
## Prover logic
```
const eddsa = require("./snarks/circomlib/src/eddsa.js");
const snarkjs = require("snarkjs");
const MIMC = require('./snarks/circomlib/src/mimc7.js')
const assert = require('assert');
const fs = require('fs');const NONCE_MAX_VALUE = 100;
function merkleTree(leafs, elements_to_proof){
var i;
var j;
var h;
const hash_leafs = leafs.map(x => MIMC.multiHash([x]));
const hash_leafs_l = [[],[],[],[],[],[],[]];
var tmp_elements_to_proof = elements_to_proof;
const proofs = [[],[]];const tmp1 = elements_to_proof[0].toString(2).padStart(6,'0').split('').reverse();
const tmp2 = elements_to_proof[1].toString(2).padStart(6,'0').split('').reverse();
const paths = [tmp1.map(x => parseInt(x,10)),
tmp2.map(x => parseInt(x,10))];//console.log(hash_leafs);
hash_leafs_l[0] = hash_leafs;for (h = 1; h<6;h++){
for (i = 0; i Math.floor(x/2))
}return [MIMC.multiHash([hash_leafs_l[5][0],hash_leafs_l[5][1]]), proofs, paths];
}function merkleTree1(leafs, elements_to_proof){
var i;
var j;
var h;
const hash_leafs = leafs.map(x => MIMC.multiHash([x]));
const hash_leafs_l = [[],[],[],[],[],[],[]];
var tmp_elements_to_proof = elements_to_proof;
const proofs = [[],[]];const tmp1 = elements_to_proof[0].toString(2).padStart(6,'0').split('').reverse();
const paths = [tmp1.map(x => parseInt(x,10))];//console.log(hash_leafs);
hash_leafs_l[0] = hash_leafs;for (h = 1; h<7;h++){
for (i = 0; i Math.floor(x/2))
}return [MIMC.multiHash([hash_leafs_l[5][0],hash_leafs_l[5][1]]), proofs, paths];
}function verifyTransfer(batchTransactions, leafsSet){
for (t in batchTransactions){const old_account_from = MIMC.multiHash([t.pubkey[0],t.token_balance_from,t.nonce,t.token_type]);
assert(leafsSet.contains(old_account_from));const old_account_to = MIMC.multiHash([t.to[0],t.token_balance_to,t.nonce_to,t.token_type_to]);
assert(leafsSet.contains(old_account_to));const msg = MIMC.multiHash([old_account_from, old_account_to]);
assert(eddsa.verifyMiMC(t.pubKey, [t.R8x, t.R8y, t.S], msg));
assert(t.token_balance_from - t.amount <= t.token_balance_from);
assert(t.token_balance_to + t.amount >= t.token_balance_to);assert(t.nonce_from < NONCE_MAX_VALUE);
assert(t.token_type_from == t.token_type_to);
}
}function verifyDeposit(batchTransactions, leafsSet){
for (t in batchTransactions){
}
}function verifyWithdraw(batchTransactions, leafsSet){
for (t in batchTransactions){const old_account_from = MIMC.multiHash([t.pubkey[0],t.token_balance_from,t.nonce,t.token_type]);
assert(leafsSet.contains(old_account_from));const msg = MIMC.multiHash([old_account_from, t.withdraw]);
assert(eddsa.verifyMiMC(t.pubKey, [t.R8x, t.R8y, t.S], msg));
assert(t.token_balance_from - t.amount <= t.token_balance_from);
assert(t.nonce_from < NONCE_MAX_VALUE);
assert(t.token_type_from == t.token_type_to);
}
}function generateWitnessDeposit(batchTransactions, leafsSet, current_state, current_index){
verifyDeposit(batchTransactions, leafsSet);
var index = current_index;
var state = current_state;var pubkey = [];
var deposit = [];
var token_type = [];
var paths2root = [];
var i;for (i=0; i x.toString(10)),
deposit: deposit.map(x => x.toString(10)),
token_type: (1).toString(10),
paths2root: paths2root.map(x => x.toString(10)),
new_state: state.toString(10),
new_index: (current_index+batchTransactions.length).toString(10)
}return inputs;
}function generateWitnessTransfer(batchTransactions, leafsSet, current_state){
verifyTransfer(batchTransactions, leafsSet);
var index_proof = [];
var state = current_state;
for (t in batchTransactions){let old_account_from = MIMC.multiHash([t.pubkey[0],t.token_balance_from,t.nonce,t.token_type]);
let old_account_to = MIMC.multiHash([t.to[0],t.token_balance_to,t.nonce_to,t.token_type_to]);leafsSet[leafsSet.indexOf(old_account_from)] = MIMC.multiHash([t.pubkey[0],t.token_balance_from-amount,t.nonce+1,t.token_type])
leafsSet[leafsSet.indexOf(old_account_to)] = MIMC.multiHash([t.to[0],t.token_balance_to+amount,t.nonce_to+1,t.token_type])
let tree = merkleTree(leafsSet, [leafsSet.indexOf(old_account_from), leafsSet.indexOf(old_account_to)]);
}
}function generateWitnessWithdraw(batchTransactions, leafsSet){
verifyWithdraw(batchTransactions, leafsSet);
var index_proof = [];
for (t in batchTransactions){let old_account_from = MIMC.multiHash([t.pubkey[0],t.token_balance_from,t.nonce,t.token_type]);
var index = leafsSet.indexOf(old_account_from)
leafsSet[index] = MIMC.multiHash([t.pubkey[0],t.token_balance_from-amount,t.nonce+1,t.token_type])
index_proof.push(index)
}
const tree = merkleTree(leafsSet, index_proof);
}
```## Homework :P
We need to add deposits and withdraws to the tutorial
Instead of just storing the public key in the leaf we can store arbitrary information. Can you build
1. NFT
2. tweets on chain,
3. votes
4. Staked tokensanything you can store in the EVM you can store here.
## Disclaimer
1. Circom is not really fast enough to natively create proofs and trusted setups for merkle trees deeper than 12 hashes. or 2 trnasactions per block so we increase things
2. This does not undermine the central claim here that we can do 500 tps on ethereum for a large subset of dapp logics. The reason being that we can use circom as a user frindly developer enviroment and pass all the proving and setup requiremntes to bellman which is much faster.
3. Even then bellman takes ~15 mintues to create a proof of AWS 40 core server. We can produce proofs in parallel that costs about 100 usd per proof. This is still sub sent per transaction which is really cheap compared to eth.