Zero-Knowledge Rollup (ERC-20 Token) — Part 2
This is in continuation of my previous article, where we learned about implementing the asset verifier circuits and the relevant functions of smart contracts.
In this article, we will understand and implement the functions of the ZK-rollups Web3:-
- User depositing the ERC20 on the mainnet.
- Implementing a database for Layer 2 (MongoDB or LevelDB).
- The user submits a transaction on Layer 2.
Implementing Web3 For mainnet Functions
Before depositing tokens to the rollup contract, the user has to approve the ERC20 token to the contract.
module.exports = {
approveTransaction: async(
web3,
spender,
amount,
from,
xponentTokenContractAddress,
privateKey
) => {
const params = web3.eth.abi.encodeParameters(
["address", "uint256"],
[spender, amount]
);
// function hash for forgeBatch is 0x00aeef8a
const data = "0x095ea7b3" + params.slice(2);
const transactionObject = {
from,
to: xponentTokenContractAddress,
value: 0,
gas: 2000000,
data,
};
const transaction = await signAndSendTransaction(
transactionObject,
web3,
privateKey
);
return transaction
},
depositTransaction: async(
web3,
publixKeyX,
publixKeyY,
amount,
from,
assetRollupContractAddress,
privateKey
) => {
const params = web3.eth.abi.encodeParameters(
["uint256", "uint256", "uint256"],
[publixKeyX, publixKeyY, amount]
);
// function hash for forgeBatch is 0x00aeef8a
const data = "0x00aeef8a" + params.slice(2);
const transactionObject = {
from,
to: assetRollupContractAddress,
value: 0,
gas: 2000000,
data,
};
const transaction = await signAndSendTransaction(
transactionObject,
web3,
privateKey
);
return transaction;
}
}
In the above depositTransaction
function, the user calls the deposit function 0x00aeef8a
of the smart contract created in the previous article. Once the transaction is successful user can check the balance on the mainnet using the following function:-
getBalanceForPublicKey: async(
web3,
publixKeyX,
publixKeyY,
abis,
smartContractAddress
) => {
const assetRollupContract = new web3.eth.Contract(
abis,
smartContractAddress
);
const balance = await assetRollupContract.methods.getBalance(
publixKeyX,
publixKeyY
).call();
return (balance);
}
Setup Database For Layer 2
The ZK-Rollup need to persist all the incoming transactions into a database so that the coordinator can fetch the transaction from this database instance and create the bundle.
insertMempool: async(
transaction
) => {
try{
const client = new MongoClient(url);
const db = client.db(DBName);
await db.collection(transactionCollectionsName).insertOne(transaction);
} catch (err) {
console.log(err)
}
},
insertTransactionTrie: async(
transactions
) => {
try{
const client = new MongoClient(url);
const db = client.db(DBName);
await db.collection(transactionReceiptCollectionsName).insertMany(transactions);
} catch (err) {
console.log(err)
}
},
readPendingTransaction: async() => {
try{
const client = new MongoClient(url);
const db = client.db(DBName);
const transactions = db.collection(transactionCollectionsName).find({status: "pending"}).limit(4).toArray();
return (transactions);
} catch (err) {
console.log(err)
}
},
Currently, I am using MongoDB but feel free to use any database as required. For example, if you want to use levelDB you can refer to the following helper functions:-
getDbObject: async() => {
return (db);
},
getFromKey: async(
key
) => {
try {
var response = await db.get(key);
return (response);
} catch ( error ) {
return (null);
}
},
putKeyValue: async(
key,
value
) => {
try {
await db.put(key, value);
return (true);
} catch (error) {
return ( error );
}
}
Implementing Web3 For Layer2 Functions (ZK-Rollup Wallet)
Users need to sign and send transactions to the Layer2 database in order to send tokens to another user. They can use the following functions:-
signTransaction: async(
mimcjs,
eddsa,
privateKey,
tokenBalance,
nonce,
tokenType
) => {
let hashLeaf = await mimcMultiHash(
mimcjs,
[publicKey[0], tokenBalance, nonce, tokenType]
);
return (
eddsa.signMiMC(
privateKey,
hashLeaf
)
);
}
sendTransaction: async(
web3,
publicKeyFrom,
publicKeyTo,
signature,
amount,
assetRollupAbis,
assetRollupContractAddress,
tokenType
) => {
const balanceFrom = await getBalanceForPublicKey(web3,publicKeyFrom[0], publicKeyFrom[1], assetRollupAbis, assetRollupContractAddress);
const balanceTo = await getBalanceForPublicKey(web3,publicKeyTo[0], publicKeyTo[1], assetRollupAbis, assetRollupContractAddress);
const nonceFrom = await getTransactionNonceForPublicKey(web3, publicKeyFrom[0], publicKeyFrom[1], assetRollupAbis, assetRollupContractAddress);
const nonceTo = await getTransactionNonceForPublicKey(web3, publicKeyTo[0], publicKeyTo[1], assetRollupAbis, assetRollupContractAddress);
if ( balanceFrom < amount ) {
console.log("Insufficient balance, Get lost!!");
} else {
let signatureString = {
R8: [
signature.R8[0].toString(16),
signature.R8[1].toString(16)
],
S: signature.S.toString()
}
const trasnsaction = {
transactionHash: await keccakEncode(publicKeyFrom.toString(16) + publicKeyTo.toString(16) + amount),
publicKeyFrom: [
publicKeyFrom[0].toString(16),
publicKeyFrom[1].toString(16)
],
publicKeyTo: [
publicKeyTo[0].toString(16),
publicKeyTo[1].toString(16)
],
balanceFrom,
balanceTo,
nonceFrom,
nonceTo,
signature: signatureString,
amount,
tokenType,
status: "pending"
};
await insertMempool(trasnsaction);
console.log("Transaction added!!");
}
}
The above sendTransaction
function performs the following steps, to successfully verify and submit a transaction in mempool.
- Fetch real-time
balance
andtransactionNonce
from the mainnet. - Compute the
transactionHash
by keccakEncode the public key from, the public key to and the amount. - Add the transaction, as per the standard schema(mentioned below) to the database mempool collection.
Layer 2 Transaction Schema
- Transaction Hash — Hash of the transaction computed by keccakEncode the public key from, the public key to and the amount.
- Public Key From — The x and y components of the public key of the sender.
- Public Key To — The x and y components of the public key of the receiver.
- Balance From — The current balance of the sender(Fetched from the mainnet).
- Balance To — The current balance of the receiver(Fetched from the mainnet).
- Nonce From — The current transaction Nonce of the sender(Fetched from the mainnet).
- Nonce To — The current transaction Nonce of the receiver(Fetched from the mainnet).
- Signature — The R8 and S components of the user signed signature.
- Amount — Amount of tokens to be sent.
- TokenType — The token type, currently we have hardcoded to 1. This can be used when the rollup is enhanced to support multiple ERC20 tokens.
- Status —Current status of the transaction(Pending, Rejected, Failed, Success).
Refer to the following example of a transaction object:-
{
"transactionHash" : "0xaf07a8ba982267470a6752504c56a80a0f62f83a68a08db37af7490c057a11ca",
"publicKeyFrom" : [
"188,249,7,4,118,211,98,112,107,156,88,82,78,82,143,115,173,176,99,254,23,57,166,220,197,4,39,199,118,10,213,22",
"177,91,35,216,17,211,238,181,186,49,89,235,21,60,229,196,130,218,167,223,49,72,48,121,3,114,109,54,81,9,45,1"
],
"publicKeyTo" : [
"209,202,79,131,122,141,200,35,147,67,106,176,113,232,59,13,199,113,37,155,197,248,126,198,46,211,209,50,141,37,209,45",
"66,29,27,219,111,82,63,231,115,239,117,68,54,133,180,201,88,37,75,93,157,109,203,41,144,96,67,177,15,187,71,18"
],
"balanceFrom" : "80000",
"balanceTo" : "0",
"nonceFrom" : "0",
"nonceTo" : "0",
"signature" : {
"R8" : [
"45,186,156,186,25,22,147,97,214,235,232,5,5,243,219,251,236,62,120,200,210,54,230,222,180,32,114,13,13,201,254,21",
"228,195,151,231,145,166,18,249,248,184,41,189,167,45,74,100,246,177,183,57,217,220,109,154,156,154,19,133,121,35,62,9"
],
"S" : "448801619597017883385222875687173756286391679080139236315948137096436768385"
},
"amount" : 40,
"tokenType" : 1,
"status" : "pending"
}
Next Steps
In the next part of this series we will go through the following:-
- Coordinator Forge a bundle of 4 transactions and generate the proof.
- The Coordinator submits the proof on the mainnet.
References
I’d love to hear any feedback or questions. You can ask the question by leaving a comment, and I will try my best to answer it.