Understanding ZK Rollups with GNARK and immudb
Diving deep into ZK-Rollups with the help of GNARK and immudb
This article is part of a four series introduction to Zero-Knowledge Proofs:
Introduction (The Magic of Proving Without Revealing: An Introduction to Zero-Knowledge Proofs)
Example of ZK Proofs in Go (Unlocking the Power of Zero Knowledge Proofs with Gnark and Go)
What are ZK Rollups?
Blockchain is a distributed ledger technology, which means that every node on the network has a copy of the entire blockchain. This approach ensures security, immutability, and transparency, but it comes at a cost of scalability. As the number of nodes on the network grows, the amount of data that needs to be processed and stored increases, making the blockchain slower and more expensive to use.
To address this issue, developers have been working on different scaling solutions. One of the most promising ones is ZK Rollups.
ZK Rollups is a layer 2 scaling solution that aims to improve the scalability and efficiency of blockchain networks. Layer 2 solutions are built on top of the main blockchain and enable off-chain processing of transactions, reducing the load on the main chain.
The "Zero-Knowledge" part of ZK Rollups refers to the use of cryptographic proofs to verify the validity of the batch transaction without revealing the details of each individual transaction. This ensures the security and privacy of the users while still allowing for efficient and cost-effective transaction processing.
In a ZK Rollup, transactions are processed off-chain by a group of validators, who bundle them together into a single transaction. The validators then create a ZK proof that attests to the correctness of the transaction, without revealing any sensitive information about the transaction itself. The ZK proof is then submitted to the main blockchain (like ethereum), which verifies its validity and stores it as a single transaction.
Here is an example of how a ZK Rollup works:
User A wants to send 1 ETH to User B.
User A submits their transaction to the ZK Rollup operator, along with a small fee to cover the gas costs of processing the transaction.
The ZK Rollup operator aggregates User A's transaction with several other transactions into a single batch transaction. The ZK Rollup operator can be considered on the layer 2 solution.
The ZK Rollup operator generates a Zero-Knowledge proof that proves the validity of the batch transaction without revealing the details of each individual transaction.
The ZK Rollup operator submits the batch transaction and Zero-Knowledge proof to the Ethereum blockchain, which verifies the proof and processes the transaction.
User B receives the 1 ETH from User A, and the transaction is recorded on the Ethereum blockchain.
Why do we need ZK Rollups?
Blockchain networks like Ethereum are facing scalability issues due to their limited transaction processing capacity. Ethereum can process around 15 transactions per second (tps), while Visa, for example, can process up to 24,000 tps. As the number of users and transactions on the network grows, the network becomes slower and more expensive to use.
Moreover, as the size of the blockchain grows, the cost of storing it increases, making it more challenging for new users to join the network. For example, the Ethereum blockchain has grown to over 1 TB in size, making it challenging for small-scale nodes to store and process the entire blockchain.
ZK Rollups address these issues by enabling off-chain processing of transactions, reducing the load on the main chain, and decreasing the amount of data that needs to be processed and stored on the blockchain. This approach increases the transaction processing capacity of the network, reduces transaction fees, and makes it easier for new users to join the network.
ZK Rollup solutions
Several ZK Rollup solutions are currently under development, aiming to improve the scalability and efficiency of different blockchain networks. Some of the most promising ones include:
Optimistic Rollups: Optimistic Rollups use a similar approach to ZK Rollups, but instead of using ZK proofs, they use a fraud proof mechanism to ensure the correctness of the off-chain transactions. Optimistic Rollups are currently being developed for Ethereum and other blockchain networks.
ZK Sync: ZK Sync is a ZK Rollup solution that is being developed for Ethereum. It can process up to 2,000 tps and reduces the transaction fees by up to 100x.
Loopring: Loopring is a decentralized exchange (DEX) that uses ZK Rollups to improve the scalability and efficiency of the exchange. It can process up to 2,000 tps and has a gas cost that is 10x lower than Ethereum's.
Core Architecture
The core architecture of a ZK Rollup consists of two main components: the on-chain smart contract and the off-chain data availability component.
On-chain Smart Contract: The on-chain smart contract is responsible for verifying the validity of transactions submitted by users and for updating the state of the ZK Rollup on the main blockchain. The smart contract also maintains a Merkle tree of the transactions included in the ZK Rollup, which is used to generate a proof that can be verified by anyone.
Off-chain Data Availability: The off-chain component of the ZK Rollup is responsible for storing the actual transaction data and computing the proofs that are submitted to the smart contract. This off-chain component can be thought of as a separate chain that processes transactions off-chain and periodically submits the final state of the ZK Rollup to the main blockchain. The off-chain component can be implemented in various ways, such as using a sidechain or a state channel network. immudb can be used as an off-chain data aggregator.
The process of using a ZK Rollup involves several steps:
Users submit transactions to the off-chain component of the ZK Rollup.
The off-chain component processes the transactions and generates a proof of their validity. The proof is submitted to the smart contract on the main blockchain.
The smart contract verifies the proof and updates the state of the ZK Rollup on the main blockchain.
Users can withdraw their funds from the ZK Rollup by submitting a withdrawal request to the smart contract.
By using Zero-Knowledge proofs, ZK Rollups allow multiple transactions to be bundled together into a single transaction, reducing the amount of data that needs to be stored and processed on the main blockchain. This significantly improves the scalability of the network and reduces transaction fees.
HOW DO ZK-ROLLUPS SCALE ETHEREUM?
Transaction data compression
ZK-rollups extend the throughput on Ethereum’s base layer by taking computation off-chain, but the real boost for scaling comes from compressing transaction data. Ethereum’s block size limits the data each block can hold and, by extension, the number of transactions processed per block. By compressing transaction-related data, ZK-rollups significantly increase the number of transactions processed per block.
ZK-rollups can compress transaction data better than optimistic rollups since they don't have to post all the data required to validate each transaction. They only have to post the minimal data required to rebuild the latest state of accounts and balances on the rollup.
Recursive proofs
An advantage of zero-knowledge proofs is that proofs can verify other proofs. For example, a single ZK-SNARK can verify other ZK-SNARKs. Such "proof-of-proofs" are called recursive proofs and dramatically increase throughput on ZK-rollups.
Currently, validity proofs are generated on a block-by-block basis and submitted to the L1 contract for verification. However, verifying single block proofs limits the throughput that ZK-rollups can achieve since only one block can be finalized when the operator submits a proof.
Recursive proofs, however, make it possible to finalize several blocks with one validity proof. This is because the proving circuit recursively aggregates multiple block proofs until one final proof is created. The L2 operator submits this recursive proof, and if the contract accepts it, all the relevant blocks will be finalized instantly. With recursive proofs, the number of ZK-rollup transactions that can be finalized on Ethereum at intervals increases.
Building a simple ZK-ROLLUP operator with immudb
In a ZK Rollup architecture, the off-chain data availability component is responsible for storing the actual transaction data and computing the proofs that are submitted to the smart contract. We will use immudb to store this data in a tamper-proof and auditable manner, ensuring that the data cannot be modified or deleted.
Here's how it would work:
Users submit transactions to the off-chain component of the ZK Rollup.
The off-chain component stores the transaction data in immudb, ensuring its integrity and immutability. immudb has inbuilt support to generate merkle proofs for the batched transactions, which can be stored in the ZK-rollup’s state. A cryptographic hash of the Merkle tree’s root of the batched transactions is stored in the on-chain contract, allowing the rollup protocol to track changes in the state of the ZK-rollup.
The off-chain component generates a SNARK/STARK proof of the transaction data's validity, which is submitted to the smart contract on the main blockchain.
The smart contract verifies the proof and updates the state of the ZK Rollup on the main blockchain.
Let’s define a minimal example by defining the initial structs:
package main
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"github.com/codenotary/immudb/pkg/api/schema"
"github.com/codenotary/immudb/pkg/client"
"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/backend/groth16"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/frontend/cs/r1cs"
)
// User represents a user of the ZK Rollup
type User struct {
address string
balance int
}
// Transaction represents a transaction on the ZK Rollup
type Transaction struct {
from string
to string
amount int
sequence int
}
// Define the batch transaction struct
type BatchTransaction struct {
Transactions []Transaction
}
// InitUsers initializes the users of the ZK Rollup
func InitUsers() []User {
users := []User{
{
address: "user1",
balance: 100,
},
{
address: "user2",
balance: 100,
},
{
address: "user3",
balance: 100,
},
}
return users
}
// InitTransactions initializes the transactions on the ZK Rollup
func InitTransactions(users []User) []Transaction {
transactions := []Transaction{}
for i := 0; i < 10; i++ {
from := users[rand.Intn(len(users))].address
to := users[rand.Intn(len(users))].address
amount := rand.Intn(10)
sequence := i
transactions = append(transactions, Transaction{from, to, amount, sequence})
}
return transactions
}
State commitments
The rollup transitions to a new state after the execution of a new set of transactions. The operator who initiated the state transition is required to compute a new state root and submit to the on-chain contract. This can be done by adding the transaction batch and committing it as a single immudb transaction, which provides the batch merkle root, and inclusion proofs, allowing users to prove a transaction was included in the batch, and also to store the root state to capture the state change.
func main() {
// Initialize users and transactions
users := InitUsers()
transactions := InitTransactions(users)
batch := BatchTransaction{transactions}
// Create an immudb client
immuClient, err := client.NewImmuClient(client.DefaultOptions())
if err != nil {
fmt.Println(err)
return
}
defer immuClient.Disconnect()
// Create a database for the ZK Rollup off-chain component
dbName := "ZKRollupOffChainComponent"
_, err = immuClient.CreateDatabaseV2(context.Background(), dbName, nil)
if err != nil {
fmt.Println(err)
return
}
immuClient.UseDatabase(context.Background(), &schema.Database{DatabaseName: dbName})
// Store transaction data in Immudb
txns := make([]*schema.KeyValue, len(transactions))
for _, t := range transactions {
key := []byte(fmt.Sprintf("%s_%s_%d", t.from, t.to, t.sequence))
value := []byte(fmt.Sprintf("%d", t.amount))
txns = append(txns, &schema.KeyValue{Key: key, Value: value})
}
batchHeader, _ := immuClient.SetAll(context.Background(), &schema.SetRequest{KVs: txns})
// Batch Merkle root
fmt.Println("Batch Merkle root: ", batchHeader.BlRoot)
// Batch transaction ID
fmt.Println("Batch transaction ID: ", batchHeader.GetId())
// Verify inclusion of transaction data in the batch Merkle root
immuClient.VerifiedGetAt(context.Background(), txns[0].Key, batchHeader.GetId())
}
Validity proofs
Validity proofs allow parties to prove the correctness of a statement without revealing the statement itself—hence, they are also called zero-knowledge proofs. ZK-rollups use validity proofs to confirm the correctness of off-chain state transitions without having to re-execute transactions on Ethereum. These proofs can come in the form of a ZK-SNARK (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge) or ZK-STARK (Zero-Knowledge Scalable Transparent Argument of Knowledge).
Both SNARKs and STARKs help attest to the integrity of off-chain computation in ZK-rollups, although each proof type has distinctive features.
Proof generation
The ZK-Rollup operator can aggregate the transactions into a single immudb transaction and compile inputs for the proving circuit to compile into a succinct ZK-proof. Inserting this into immudb helps as it generates:
A Merkle root comprising all the transactions in the single batch immudb transaction.
Provide inclusion proofs for transactions to prove inclusion in the batch.
This Merkle root reflects the sole change in the ZK-rollup's state.
A very minimal example to illustrate the example is below:
// Define the circuit struct
type RollupCircuit struct {
BatchTransaction BatchTransaction `gnark:",public"`
}
// Define declares the circuit constraints
func (circuit *RollupCircuit) Define(api frontend.API) error {
return nil
}
// Generate a proof of the transaction data's validity
// and submit it to the smart contract on the main blockchain using GNARK
// Use gnark to generate a ZK proof that proves the validity of the batch transaction
var pk groth16.ProvingKey
var vk groth16.VerifyingKey
// Define the circuit, generate the proving key and verifying key, and generate the proof
var circuit RollupCircuit
circuit.BatchTransaction = batch
// Generate the proving key and verifying key
const (
nbConstraints = 1 << 10
pkFile = "rollup.pk"
vkFile = "rollup.vk"
)
// groth16 zkSNARK: Setup
ccs, _ := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &circuit)
pk, vk, _ = groth16.Setup(ccs)
// witness definition with some transaction
assignment := RollupCircuit{}
witness, _ := frontend.NewWitness(&assignment, ecc.BN254.ScalarField())
publicWitness, _ := witness.Public()
// groth16: Prove & Verify
proof, _ := groth16.Prove(ccs, pk, witness)
groth16.Verify(proof, vk, publicWitness)
// Store the batch transaction and ZK proof in immudb
// Serialize the batch transaction and ZK proof
batchBytes, err := json.Marshal(batch)
if err != nil {
panic(err)
}
proofBytes, err := json.Marshal(proof)
if err != nil {
panic(err)
}
// Store the batch transaction and ZK proof in immudb
_, err = immuClient.Set(context.Background(), []byte("batch"), batchBytes)
if err != nil {
panic(err)
}
_, err = immuClient.Set(context.Background(), []byte("proof"), proofBytes)
if err != nil {
panic(err)
}
fmt.Println("Batch transaction and ZK proof stored in immudb!")
// Send the proof to the layer 1 (or ethereum) smart contract
// Verify the proof and update the state of the ZK Rollup on the main blockchain (like ethereum)
You can then save the proof in immudb and send it to the layer 1 solution.
Conclusion
ZK Rollups represent an innovative and promising solution to the scalability problem faced by many blockchain networks today. By aggregating transactions off-chain and using zero-knowledge proofs to verify their validity on-chain, ZK Rollups can significantly increase the transaction throughput of a blockchain while maintaining its security and decentralization. Several projects, including Loopring, zkSync, and Polygon, have already implemented ZK Rollups or are working on developing their own solutions.
immudb can play a significant role in supporting ZK Rollups. By providing a secure and efficient off-chain data storage solution, immudb can help enable ZK Rollups to process a larger volume of transactions with greater efficiency and security. In addition, immudb's support for cryptographic proofs, such as inclusion and consistency proofs, can help ensure the validity and integrity of the data stored in the off-chain database.
Reference
https://ethereum.org/en/developers/docs/scaling/zk-rollups/
https://github.com/ConsenSys/gnark
https://immudb.io/