Skip to content

Instantly share code, notes, and snippets.

@rizary
Created March 28, 2022 23:26
Show Gist options
  • Select an option

  • Save rizary/4065d13e26382f6cfa018eb80a9f1cd3 to your computer and use it in GitHub Desktop.

Select an option

Save rizary/4065d13e26382f6cfa018eb80a9f1cd3 to your computer and use it in GitHub Desktop.
RollupNC review
// Deposits ETH or ERC20 tokens onto L2
function deposit(
uint[2] memory pubkey,
uint amount,
uint tokenType
) public payable {
if ( tokenType == 0 ) {
// reserved the token type if sender is same as coordinator
require(
msg.sender == coordinator,
"tokenType 0 is reserved for coordinator");
require(
amount == 0 && msg.value == 0,
"tokenType 0 does not have real value");
} else if ( tokenType == 1 ) {
// ETH
require(
msg.value > 0 && msg.value >= amount,
"msg.value must at least equal stated amount in wei");
} else if ( tokenType > 1 ) {
// ERC20 token
require(
amount > 0,
"token deposit must be greater than 0");
// Andika: Initiate interface
address tokenContractAddress = tokenRegistry.registeredTokens(tokenType);
tokenContract = IERC20(tokenContractAddress);
// Andika: check if approval is already granted
require(
tokenContract.transferFrom(msg.sender, address(this), amount),
"token transfer not approved"
);
}
// Andika: create deposit array and insert necessary value
uint[] memory depositArray = new uint[](5);
depositArray[0] = pubkey[0];
depositArray[1] = pubkey[1];
depositArray[2] = amount;
depositArray[3] = 0;
depositArray[4] = tokenType;
// Andika: Hash the deposit
uint depositHash = mimcMerkle.hashMiMC(
depositArray
);
// Store the deposit hash into pendingDeposits
pendingDeposits.push(depositHash);
// Broadcast deposit
emit RequestDeposit(pubkey, amount, tokenType);
queueNumber++;
uint tmpDepositSubtreeHeight = 0;
uint tmp = queueNumber;
// Andika: this is where the computation of the root happened. So for For every 2^n, the root is computed and stored at pendingDeposits[0]
// meanwhile, for every 2 deposits, compute branch(es) and store in pendingDeposits[1..]
while(tmp % 2 == 0){
uint[] memory array = new uint[](2);
array[0] = pendingDeposits[pendingDeposits.length - 2];
array[1] = pendingDeposits[pendingDeposits.length - 1];
pendingDeposits[pendingDeposits.length - 2] = mimcMerkle.hashMiMC(
array
);
removeDeposit(pendingDeposits.length - 1);
tmp = tmp / 2;
tmpDepositSubtreeHeight++;
}
// Increment deposit tree height when sufficient leafs are accumulated and a new level is formed
if (tmpDepositSubtreeHeight > depositSubtreeHeight){
depositSubtreeHeight = tmpDepositSubtreeHeight;
}
}
template Main(n,m) {
// n is depth of balance tree
// m is depth of transaction (tx) tree
// for each proof, update 2**m (i.e. number of leafs) txs
// Merkle root of tx tree
signal input txRoot;
// Merkle proofs of txs in tx tree needed for constructing root
signal private input paths2txRoot[2**m, m];
// Binary vector indicating whether node in tx proof is left or right
// 0 when node at left, 1 when at right
signal private input paths2txRootPos[2**m, m];
// Merkle root of old balance tree
signal input currentState;
// Intermediate Merkle roots of balance tree
// Two for each tx:
// 2*i is before send, 2*i+1 is after send / before receive (2*i+2 is after receive / before next send)
// The last element is the final root
signal private input intermediateRoots[2**(m+1)+1];
// Merkle proofs of sender accounts in balance tree needed for constructing root
signal private input paths2rootFrom[2**m, n];
// Binary vector indicating whether node in balance proof for sender account is left or right
signal private input paths2rootFromPos[2**m, n];
// Merkle proofs of receiver accounts in balance tree needed for constructing root
signal private input paths2rootTo[2**m, n];
// Binary vector indicating whether node in balance proof for receiver account is left or right
signal private input paths2rootToPos[2**m, n];
// Tx info, 10 fields
signal private input fromX[2**m]; //sender address x coordinate
signal private input fromY[2**m]; //sender address y coordinate
signal private input fromIndex[2**m]; //sender account leaf index
signal private input toX[2**m]; // receiver address x coordinate
signal private input toY[2**m]; // receiver address y coordinate
signal private input nonceFrom[2**m]; // sender account nonce
signal private input amount[2**m]; // amount being transferred
signal private input tokenTypeFrom[2**m]; // sender token type
signal private input R8x[2**m]; // sender signature
signal private input R8y[2**m]; // sender signature
signal private input S[2**m]; // sender signature
// Additional account info (not included in tx)
signal private input balanceFrom[2**m]; // sender token balance
signal private input balanceTo[2**m]; // receiver token balance
signal private input nonceTo[2**m]; // receiver account nonce
signal private input tokenTypeTo[2**m]; // receiver token type
// New balance tree Merkle root
signal output out;
var NONCE_MAX_VALUE = 100;
// Constant zero address
var ZERO_ADDRESS_X = 0;
var ZERO_ADDRESS_Y = 0;
component txExistence[2**m];
component senderExistence[2**m];
component ifBothHighForceEqual[2**m];
component newSender[2**m];
component computedRootFromNewSender[2**m];
component receiverExistence[2**m];
component newReceiver[2**m];
component allLow[2**m];
component ifThenElse[2**m];
component computedRootFromNewReceiver[2**m];
currentState === intermediateRoots[0];
for (var i = 0; i < 2**m; i++) {
// Andika: Make sure the transaction is existed in the
// transaction tree.
txExistence[i] = TxExistence(m);
txExistence[i].fromX <== fromX[i];
txExistence[i].fromY <== fromY[i];
txExistence[i].fromIndex <== fromIndex[i];
txExistence[i].toX <== toX[i];
txExistence[i].toY <== toY[i];
txExistence[i].nonce <== nonceFrom[i];
txExistence[i].amount <== amount[i];
txExistence[i].tokenType <== tokenTypeFrom[i];
// Andika: check whether the txRoot computed is the same with current input txRoot
txExistence[i].txRoot <== txRoot;
for (var j = 0; j < m; j++){
txExistence[i].paths2rootPos[j] <== paths2txRootPos[i, j] ;
txExistence[i].paths2root[j] <== paths2txRoot[i, j];
}
// Andika: make sure signatures are valid
txExistence[i].R8x <== R8x[i];
txExistence[i].R8y <== R8y[i];
txExistence[i].S <== S[i];
// Andika: Make sure senders exist in balance tree
senderExistence[i] = BalanceExistence(n);
senderExistence[i].x <== fromX[i];
senderExistence[i].y <== fromY[i];
senderExistence[i].balance <== balanceFrom[i];
senderExistence[i].nonce <== nonceFrom[i];
senderExistence[i].tokenType <== tokenTypeFrom[i];
// Andika: Make sure that input intermediate balance roots equal roots computed
senderExistence[i].balanceRoot <== intermediateRoots[2*i];
for (var j = 0; j < n; j++){
senderExistence[i].paths2rootPos[j] <== paths2rootFromPos[i, j];
senderExistence[i].paths2root[j] <== paths2rootFrom[i, j];
}
// Sanity check of amounts being transferred
balanceFrom[i] - amount[i] <= balanceFrom[i];
balanceTo[i] + amount[i] >= balanceTo[i];
// Check that nonce is within limit
nonceFrom[i] != NONCE_MAX_VALUE;
//-----CHECK TOKEN TYPES === IF NON-WITHDRAWS-----//
ifBothHighForceEqual[i] = IfBothHighForceEqual();
// Andika: make sure that toX and toY are not 0
ifBothHighForceEqual[i].check1 <== toX[i];
ifBothHighForceEqual[i].check2 <== toY[i];
// Andika: If yes, then make sure that tokenTypes are identical
ifBothHighForceEqual[i].a <== tokenTypeTo[i];
ifBothHighForceEqual[i].b <== tokenTypeFrom[i];
//-----END CHECK TOKEN TYPES-----//
//-----CHECK SENDER IN TREE 2 AFTER DEDUCTING-----//
// Compute new sender leafs
newSender[i] = BalanceLeaf();
newSender[i].x <== fromX[i];
newSender[i].y <== fromY[i];
newSender[i].balance <== balanceFrom[i] - amount[i]; // subtract amount from sender balance
newSender[i].nonce <== nonceFrom[i] + 1; // increase sender nonce
newSender[i].tokenType <== tokenTypeFrom[i];
// Compute intermediate roots from new sender leafs
computedRootFromNewSender[i] = GetMerkleRoot(n);
computedRootFromNewSender[i].leaf <== newSender[i].out;
for (var j = 0; j < n; j++){
computedRootFromNewSender[i].paths2root[j] <== paths2rootFrom[i, j];
computedRootFromNewSender[i].paths2rootPos[j] <== paths2rootFromPos[i, j];
}
// Andika: make sure that computed intermediate roots are consistent with input intermediate balance roots
computedRootFromNewSender[i].out === intermediateRoots[2*i + 1];
//-----END SENDER IN TREE 2 AFTER DEDUCTING CHECK-----//
// Andika: make sure receivers exist in balance tree
receiverExistence[i] = BalanceExistence(n);
receiverExistence[i].x <== toX[i];
receiverExistence[i].y <== toY[i];
receiverExistence[i].balance <== balanceTo[i];
receiverExistence[i].nonce <== nonceTo[i];
receiverExistence[i].tokenType <== tokenTypeTo[i];
//Andika: make sure input intermediate balance roots equal roots computed
receiverExistence[i].balanceRoot <== intermediateRoots[2*i + 1];
for (var j = 0; j < n; j++){
receiverExistence[i].paths2rootPos[j] <== paths2rootToPos[i, j] ;
receiverExistence[i].paths2root[j] <== paths2rootTo[i, j];
}
//-----CHECK RECEIVER IN TREE 3 AFTER INCREMENTING-----//
// Compute new receiver leafs
newReceiver[i] = BalanceLeaf();
newReceiver[i].x <== toX[i];
newReceiver[i].y <== toY[i];
// If receiver is zero address, do not change balance
// Otherwise add amount to receiver balance
allLow[i] = AllLow(2);
allLow[i].in[0] <== toX[i];
allLow[i].in[1] <== toY[i];
ifThenElse[i] = IfAThenBElseC();
ifThenElse[i].aCond <== allLow[i].out; // aCond = 1 if toX and toY are zeroes
ifThenElse[i].bBranch <== balanceTo[i];
ifThenElse[i].cBranch <== balanceTo[i] + amount[i];
newReceiver[i].balance <== ifThenElse[i].out;
newReceiver[i].nonce <== nonceTo[i];
newReceiver[i].tokenType <== tokenTypeTo[i];
// Compute intermediate roots from new receiver leafs
computedRootFromNewReceiver[i] = GetMerkleRoot(n);
computedRootFromNewReceiver[i].leaf <== newReceiver[i].out;
for (var j = 0; j < n; j++){
computedRootFromNewReceiver[i].paths2root[j] <== paths2rootTo[i, j];
computedRootFromNewReceiver[i].paths2rootPos[j] <== paths2rootToPos[i, j];
}
// Andika: make sure computed intermediate roots are consistent with input intermediate balance roots
computedRootFromNewReceiver[i].out === intermediateRoots[2*i + 2];
//-----END CHECK RECEIVER IN TREE 3 AFTER INCREMENTING-----//
}
// Output final Merkle root of balance tree
out <== computedRootFromNewReceiver[2**m - 1].out;
}
// Andika: This function updates the Merkle root stored on L1
function updateState(
// Andika: this is where the parameter from circuit
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[3] memory input // [newRoot, txRoot, oldRoot]
) public onlyCoordinator {
// Andika: make sure that the current root on L1 was used as old root during proof generation
require(currentRoot == input[2], "input does not match current root");
// Andika: see if zero-knowledge proof is valid
require(update_verifyProof(a,b,c,input),
"SNARK proof is invalid");
// Andika: updating the merkle root stored on L1
currentRoot = input[0];
// Andika: update the contract
updateNumber++;
updates[input[1]] = updateNumber;
// Andika: send broadcast on the state update contain new, tx and old Root
emit UpdatedState(input[0], input[1], input[2]);
}
// Andika: This function is to withdraw ETH or ERC20 tokens from L2
function withdraw(
uint[9] memory txInfo, // [pubkeyX, pubkeyY, index, toX ,toY, nonce, amount, token_type_from, txRoot]
uint[] memory position,
uint[] memory proof,
address payable recipient,
// Andika: this is the parameter from the circuit's proof generated
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c
) public{
// Andika: make sure token type is not of reserved type
require(txInfo[7] > 0, "invalid tokenType");
// Andika: make sure that transaction (tx) tree exists
require(updates[txInfo[8]] > 0, "txRoot does not exist");
uint[] memory txArray = new uint[](8);
for (uint i = 0; i < 8; i++){
txArray[i] = txInfo[i];
}
// Andika: Hash the tx
uint txLeaf = mimcMerkle.hashMiMC(txArray);
// Andika: Check if txRoot matches with the root derived from the provided tx
require(txInfo[8] == mimcMerkle.getRootFromProof(
txLeaf, position, proof),
"transaction does not exist in specified transactions root"
);
// Andika: add the nonce and recipient address into a message array
uint[] memory msgArray = new uint[](2);
msgArray[0] = txInfo[5]; // nonce
msgArray[1] = uint(recipient);
// Andika: see if zero-knowledge proof is valid
require(withdraw_verifyProof(
a, b, c,
[txInfo[0], txInfo[1], mimcMerkle.hashMiMC(msgArray)] // [pubkeyX, pubkeyY, msgHash]
),
"eddsa signature is not valid");
// Andika: Transfer token to recipient on L1
if (txInfo[7] == 1){
// ETH
recipient.transfer(txInfo[6]);
} else {
// ERC20 token
address tokenContractAddress = tokenRegistry.registeredTokens(txInfo[7]);
tokenContract = IERC20(tokenContractAddress);
require(
tokenContract.transfer(recipient, txInfo[6]),
"transfer failed"
);
}
// Andika: send Broadcast withdrawal
emit Withdraw(txInfo, recipient);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment