Building zkRollup (Part — 3)

Shishir Singh
cumberlandlabs
Published in
6 min readFeb 12, 2023
Uttarakhand (India) in summers

This article is in continuation of Part-1 and Part -2. In this part, I will be focusing on withdrawals from L1.

Withdraw from L1

Withdraw functionality is quite similar to Forge Batch Functionality as described in the earlier part of this series. Except for one major difference, if a user wishes to withdraw funds from Mainnet she can do so by submitting the Forge Batch transaction on L2 with the same From and To EdDSA address. Again there are many design choices available to fulfil Withdraw functionality, I will cover some of the pros and cons of these choices in future articles. For now, let's move ahead with the code walk-through.

Make sure you go through the earlier part of the series. This part has a solid dependency on it.

zkRollup — withdraw

Refer to l2Web3.js and its description in Part — 2 .

const xponentAbi = require('../build/contracts/xponentDB.json');
const Web3 = require('web3');
const web3 = new Web3('http://127.0.0.1:8545');
const xponentContractAddress = require('../deployData.json');
const xponentContract = new web3.eth.Contract(xponentAbi.abi, xponentContractAddress.xponentContractAddress);
const Trie = require('merkle-patricia-tree').BaseTrie;
const level = require('level').Level;
const { Account, BN, bufferToHex, rlp, keccak256 } = require('ethereumjs-util');
const db = new level('./db1') ;

async function getOnChainRoot () {
return new Promise( async resolve => {
await xponentContract.methods.transactionRoot().call
((err, result) => { return resolve(result) ; })
})
} ;

async function getOnChainWithdrawRoot () {
return new Promise( async resolve => {
await xponentContract.methods.withdrawRoot().call
((err, result) => { return resolve(result) ; })
})
} ;


async function getBalance (PubKeyX, PubKeyY) {
return new Promise( async resolve => {
await xponentContract.methods.accounts(PubKeyX, PubKeyY).call
((err, result) => { return resolve(result.balance) ; })
})
} ;

async function getTransactionNonce (PubKeyX, PubKeyY) {
return new Promise( async resolve => {
await xponentContract.methods.accounts(PubKeyX, PubKeyY).call
((err, result) => { return resolve(result.transaction_nonce) ; })
})
} ;

async function dbGet(key) {
return await db.get(key);
}
async function setGenesisRoot() {
const genesisRoot = "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421";
await dbInsertHex(0, genesisRoot); // state trie
await dbInsertHex(1, genesisRoot); // transaction trie
await dbInsertHex(2, genesisRoot); // storage trie
await dbInsertHex(3, genesisRoot); // receipt trie
}
async function trieInsert(key, value) {
var trie = new Trie(db);
await trie.put(key, value);
return trie.root;
}
async function trieDelete(key, root) {
var trie = new Trie(db, root);
await trie.del(key);
return trie.root;
}
async function trieInsertWithRoot(key, value, Root) {
var trie = new Trie(db, Root);
await trie.put(key, value);
return trie.root;
}

async function withdraw(publicSignals,proof,coordinatorPublicKey,coordinatorPrivateKey) {
// const transactionRoot = await xponentContract.methods.transactionRoot().call();
const nonce = await web3.eth.getTransactionCount(coordinatorPublicKey, 'latest');
const rawData = xponentContract.methods.withdraw(publicSignals, proof).encodeABI();
const rawTxn = {
nonce: nonce,
to: xponentContractAddress.xponentContractAddress,
gas: 3100000,
data: rawData
}
const signedTx = await web3.eth.accounts.signTransaction(rawTxn, coordinatorPrivateKey);
// console.log(signedTx) ;
const hash = await web3.eth.sendSignedTransaction(signedTx.rawTransaction) ;
return hash ;
}

async function forgeBatchOnChain(publicSignals,
proof,
coordinatorPublicKey,
coordinatorPrivateKey) {
const nonce = await web3.eth.getTransactionCount(coordinatorPublicKey, 'latest');
// const transactionRoot = await xponentContract.methods.transactionRoot().call();
const rawData = xponentContract.methods.forgeBatch(publicSignals, proof).encodeABI();
const rawTxn = {
nonce: nonce,
to: xponentContractAddress.xponentContractAddress,
gas: 3100000,
data: rawData
}
const signedTx = await web3.eth.accounts.signTransaction(rawTxn, coordinatorPrivateKey);
// console.log(signedTx) ;
const hash = await web3.eth.sendSignedTransaction(signedTx.rawTransaction) ;
// await stateUpdate(transactionObject,transactionCount,hash) ;
return hash ;
}

async function dbInsertHex(key, value) {
return await db.put(key, bufferToHex(value));
}
async function sendTransaction(transactionObject) {
if (parseInt(await getBalance(transactionObject.fromPubKeyX, transactionObject.fromPubKeyY)) > transactionObject.value) {
key = Buffer.from(rlp.encode([transactionObject.gas ,
transactionObject.fromPubKeyX,
transactionObject.fromPubKeyY,
transactionObject.nonce_from]));
value = Buffer.from(rlp.encode([transactionObject.value,
transactionObject.toPubKeyX,
transactionObject.toPubKeyY,
transactionObject.R8x,
transactionObject.R8y,
transactionObject.S,
transactionObject.nonce_to]));
const txnRootBuffer = Buffer.from((await dbGet(1)).slice(2), 'hex');
const txnRoot = await trieInsertWithRoot(key, value, txnRootBuffer);
dbInsertHex(1, txnRoot);
return bufferToHex(txnRoot);
} else
console.error("Insufficient balance !!");
}

async function getMemPoolTransaction () {
return new Promise( async resolve => {
let output = [];
let options = { limit: 2 } ;
let txnRoot = await dbGet(1);
let separator = "," ;
const txnRootBuffer = Buffer.from(txnRoot.slice(2), 'hex');
let trie = new Trie(db, txnRootBuffer);
trie.createReadStream({ options })
.on('data', data => {
output.push([ rlp.decode(data.key,true).data.toString().split(separator),
rlp.decode(data.value,true).data.toString().split(separator)]);
})
.on('end', () => { resolve(output); });
});
}

module.exports = {
sendTransaction, getBalance, dbGet,
setGenesisRoot, getMemPoolTransaction, getOnChainRoot,getTransactionNonce,
forgeBatchOnChain,getOnChainWithdrawRoot,withdraw
};

To test the withdrawal functionality we can make use of l2Web3Test.js. In this code, withdrawal functions begin with fetching the latest withdrawal root from L1 and generating the withdrawal proof. Once the proof is successfully generated user can directly call withdrawal() on the L1 smart contract with the proof and public signals.

const xponentAbi = require('../build/contracts/xponentDB.json');
const snarkjs = require("snarkjs");
const Web3 = require('web3');
const web3 = new Web3('http://127.0.0.1:8545');
const testData = require('./testData.json');
const l2Wallet = require('../src/l2Wallet');
const l2Web3 = require('../src/l2Web3.js');
const l2VM = require('../src/l2VM.js');
const transactionCount = 2;
const fs = require("fs");
const zKeyFilePath = "../build/MultipleTransaction2_circuit_final.zkey";
const wasmFilePath = "../build/MultipleTransaction2_js/MultipleTransaction2.wasm" ;
const withdrawZKeyFilePath = "../buildWithdraw/withdraw_circuit_final.zkey";
const withdrawWasmFilePath = "../buildWithdraw/Withdraw_js/Withdraw.wasm" ;

let onChainRoot ='';
const init = async() => {
// await l2Web3.setGenesisRoot() ;
onChainRoot = await l2Web3.getOnChainRoot() ;
const coordinatorPublicKey = testData.coordinatorPublicKey ;
const coordinatorPrivateKey = testData.coordinatorPrivateKey ;
let publicKeyFrom = new Array (transactionCount) ;
let publicKeyTo = new Array (transactionCount) ;
let signature = new Array (transactionCount) ;
let nonce_from = new Array (transactionCount) ;
let nonce_to = new Array (transactionCount) ;

console.log(await l2Web3.dbGet(1)) ;

for (var i = 0 ; i < transactionCount ; i++){
publicKeyFrom[i] = await l2Wallet.createEddsaWallet(testData.usersFromPrivateKey[i]);
publicKeyTo[i] = await l2Wallet.createEddsaWallet(testData.usersToPrivateKey[i]);
nonce_from[i] = await l2Web3.getTransactionNonce(publicKeyFrom[i][0],publicKeyFrom[i][1]) ;
nonce_to[i] = await l2Web3.getTransactionNonce(publicKeyTo[i][0],publicKeyTo[i][1]) ;
signature[i] = await l2Wallet.signEddsa(testData.usersFromPrivateKey[i],
[onChainRoot,publicKeyFrom[i][0],publicKeyFrom[i][1],
nonce_from[i],(520000 + i * 456).toString(),
publicKeyTo[i][0],publicKeyTo[i][1]]) ;
await l2Web3.sendTransaction({
gas: (21000 + i).toString(),
nonce_from: nonce_from[i],
fromPubKeyX: publicKeyFrom[i][0],
fromPubKeyY: publicKeyFrom[i][1],
value: (520000 + i * 456).toString(),
toPubKeyX:publicKeyTo[i][0],
toPubKeyY: publicKeyTo[i][1],
R8x: signature[i][0],
R8y:signature[i][1],
S:signature[i][2],
nonce_to: nonce_to[i],
})
}
console.log(await l2Web3.dbGet(1)) ;
let memPoolTransactions = [] ;
memPoolTransactions = (await l2Web3.getMemPoolTransaction()).slice(0, 2);
console.log(memPoolTransactions);
let proofAndPublicSignals = [] ;
proofAndPublicSignals = await l2VM.generateProof(
memPoolTransactions,
wasmFilePath,
zKeyFilePath) ;
fs.writeFileSync('proof.txt',proofAndPublicSignals[0],{ flag: 'w' });
fs.writeFileSync('publicSignals.json',JSON.stringify(proofAndPublicSignals[1]),{ flag: 'w' });

const proof = fs.readFileSync('proof.txt', { encoding: 'utf8' });
const publicSignals = require('./publicSignals.json');
console.log(await l2Web3.forgeBatchOnChain(publicSignals,
proof,coordinatorPublicKey,coordinatorPrivateKey) ) ;

console.log('Forge batch onchain Done !!');
console.log('Start generating withdraw proof..');

const initialOnChainWithdrawRoot = await l2Web3.getOnChainWithdrawRoot() ;
const withdrawPubKey = await l2Wallet.createEddsaWallet(testData.usersFromPrivateKey[0]);
const withdrawToAddress = testData.withdrawToAddress;
const withdrawSignature = await l2Wallet.signEddsa(testData.usersFromPrivateKey[0],
[withdrawPubKey[0],
withdrawPubKey[1],
withdrawToAddress,
(520000).toString(),
initialOnChainWithdrawRoot]) ;

let withdrawProofAndPublicSignals = [] ;
withdrawProofAndPublicSignals = await l2VM.generateWithdrawProof({
fromPubKeyX:withdrawPubKey[0],
fromPubKeyY: withdrawPubKey[1],
amount: (520000).toString(),
to:withdrawToAddress,
R8x: withdrawSignature[0],
R8y:withdrawSignature[1],
S:withdrawSignature[2],
},withdrawWasmFilePath,withdrawZKeyFilePath
) ;

fs.writeFileSync('WithdrawProof.txt',withdrawProofAndPublicSignals[0],{ flag: 'w' });
fs.writeFileSync('WithdrawPublicSignals.json',JSON.stringify(withdrawProofAndPublicSignals[1]),{ flag: 'w' });
console.log('Generate WithdrawProof Done Succesfully!!');
console.log(`Before withdraw balance = ${await web3.eth.getBalance(withdrawToAddress)} `) ;

const withdrawProof = fs.readFileSync('WithdrawProof.txt', { encoding: 'utf8' });
const withdrawPublicSignals = require('./WithdrawPublicSignals.json');

console.log(await l2Web3.withdraw(withdrawPublicSignals,
withdrawProof,coordinatorPublicKey,coordinatorPrivateKey) ) ;
console.log(`After withdraw balance = ${await web3.eth.getBalance(withdrawToAddress)} `) ;
}
init() ;

Withdraw Circuit

One of the remaining circuits from Part-1 is the withdrawal circuit. The main functionality of this circuit is to verify the signature and provide the latest root. This circuit will take the following inputs:

initialRoot — Latest on chain withdraw root

finalOnChainRoot — final root post transaction

pub_from_x — EdDSA public key X

pub_from_y — EdDSA public key Y

amount — amount to be transferred

to — Hexadecimal (decimal equivalent) Bytes20 recipient ETH address

R8x- EdDSA Signature component

R8y-EdDSA Signature component

S-EdDSA Signature component

pragma circom 2.0.0;

include "../node_modules/circomlib/circuits/mimc.circom";
include "../node_modules/circomlib/circuits/eddsamimc.circom";

template WithdrawTransaction() {
signal input initialRoot ;
signal input finalOnChainRoot ;
signal input pub_from_x;
signal input pub_from_y;
signal input amount;
signal input to;
signal input R8x;
signal input R8y;
signal input S;

signal output out;
component message = MultiMiMC7(5,91);
component verifier = EdDSAMiMCVerifier();
message.k <== 0 ;
message.in[0] <== pub_from_x ;
message.in[1] <== pub_from_y ;
message.in[2] <== to ;
message.in[3] <== amount ;
message.in[4] <== initialRoot ;

verifier.enabled <== 1;
verifier.Ax <== pub_from_x;
verifier.Ay <== pub_from_y;
verifier.R8x <== R8x ;
verifier.R8y <== R8y;
verifier.S <== S;
verifier.M <== message.out;

component final_root = MultiMiMC7(1,91);
final_root.k <== 0 ;
final_root.in[0] <== initialRoot ;

finalOnChainRoot === final_root.out ;

out <== 1;
}

component main {public [initialRoot, finalOnChainRoot, pub_from_x, pub_from_y, to, amount]} = WithdrawTransaction();

Part -4

In part-4 I will be explaining the on-chain code for this roll-up.

--

--