DEV Community

Cover image for Exploring ZKSync: A Journey through ZKSync with the Golang SDK
tosynthegeek
tosynthegeek

Posted on • Edited on

Exploring ZKSync: A Journey through ZKSync with the Golang SDK

Introduction: Zero Knowledge Rollups & ZKSync


Zero Knowledge Technology: A Quick Overview

In cryptography, zero-knowledge proof or zero-knowledge protocols is a method by which one party can prove to another party that a given statement is true, without revealing any extra information beyond the proof.

What then are Zero knowledge rollups?

Zero-Knowledge (ZK) rollups are layer-2 scaling solutions for Ethereum that improve transaction throughput and reduce fees. This is achieved by bundling (or "rolling up") transactions into batches and executing them off-chain.

Off-chain computation reduces the amount of data stored on the Ethereum mainnet. Instead of sending each transaction individually, operators submit a cryptographic proof, using zero-knowledge proofs (ZKPs), that verifies the validity of the off-chain computations and the resulting state changes. These proofs are then submitted to the Ethereum mainnet for final settlement, ensuring the system’s security is maintained by leveraging the Ethereum blockchain.

ZKSync: Scaling Ethereum’s Technology and Values

ZkSync is a Zero Knowledge rollup, that uses cryptographic validity proofs to provide scalable and low-cost transactions on Ethereum. It is a user-centric zk-rollup platform with security, user experience, and developer experience as the core focus.


Introducing zkSync-go SDK

The zkSync-go SDK allows you to interact with the zkSync network, perform operations like transfers, deploying of contracts, and even using unique zkSync features like account abstractions.

In this post, we will use the Go SDK to:

  • Deposit tokens from Ethereum (L1) to the zkSync network(L2)

  • Withdraw from zkSync to Ethereum

  • Transfer from one address to another


Prerequisites

Before you dive in, you need the following prerequisites:

  • Golang >= 1.17, you can follow this installation guide.

  • Install the SDK for zkSync Era using the command below:
    go get github.com/zksync-sdk/zksync2-go

  • You need a basic understanding of Golang

Setting up the project

  • Create a new file directory and enable dependency tracking for your code
mkdir zksync

cd zksync

go mod init zksync

code .
Enter fullscreen mode Exit fullscreen mode
  • Create a new go file and import the following packages.
import (

"context"

"fmt"

"log"

"math/big"


"github.com/ethereum/go-ethereum/accounts/abi/bind"

"github.com/ethereum/go-ethereum/common"

"github.com/ethereum/go-ethereum/ethclient"

"github.com/zksync-sdk/zksync2-go/accounts"

"github.com/zksync-sdk/zksync2-go/clients"

"github.com/zksync-sdk/zksync2-go/utils"

)
Enter fullscreen mode Exit fullscreen mode
  • Create these as global variables as we would need them all through.
privateKey:= os.Getenv("PRIVATE_KEY") // Create a .env for your private key

toAddress:= common.HexToAddress("0xD109E8C395741b4b3130E3D84041F8F62aF765Ef") // Replacethis with address of your choice

zkSyncEraProvider := "https://sepolia.era.zksync.dev"

ethProvider:= ""  // Import your Ethereum provider URL here
Enter fullscreen mode Exit fullscreen mode

With this you have all we need. Now, dive in.


Interacting with zkSync using the Golang SDK

Implementing Deposit

We are going to start by establishing a connection to both the ZK and Ethereum clients through the providers.

// Connect to the zkSync network
ZKClient, err:= clients.Dial(zkSyncEraProvider)
if  err != nil {
log.Fatal(err.Error())
}
defer  ZKClient.Close() // Close on exit 

fmt.Println("zkSync client connected....")

// Connect to the Ethereum network
ethClient, err:= ethclient.Dial(ethProvider)
if  err != nil {
log.Fatal(err.Error())
}
defer  ethClient.Close() // Close on exit

fmt.Println("Ethereum clients connected......")
Enter fullscreen mode Exit fullscreen mode

This connection serves as a bridge, enabling you to perform actions on the ZKSync network and interact with the Ethereum mainnet.
Now let's create an instance of a wallet associated with the private key we passed in our .env. We also check for the current balance of this wallet so we can compare it to the balance after making the deposit.

// Create a new wallet using the private key and network clients
wallet, err:= accounts.NewWallet(common.Hex2Bytes(privateKey), &ZKClient, ethClient)
if  err != nil {
log.Fatal(err.Error())
}
fmt.Println("Wallet Created.... ")

// Get balance before deposit. Setting block number to nil so it returns balance at the latest block
balance, err:= wallet.Balance(context.Background(), utils.EthAddress, nil)
if  err != nil {
log.Fatal(err.Error())
}
fmt.Println("Balance before deposit is: ", balance)
Enter fullscreen mode Exit fullscreen mode

The Balance function allows us to check the balance of a specified token in an address. It accepts 3 arguments:

  • ctx: A context object used for cancellation or deadline management.
  • token: The address of the specific token you're interested in. utils.EthAddress represents the address of the wrapped Ethereum token on the zkSync network
  • at: The block number at which you want to query the balance, when set to nil returns the balance at the latest block.

At this step, we are ready to make a deposit from Ethereum to the zkSync network.

tx, err:= wallet.Deposit(nil, accounts.DepositTransaction{
Token: utils.EthAddress,
Amount: big.NewInt(1000000000), // In Wei
To: wallet.Address(), // Deposits to our own address
})

if  err != nil {
log.Fatal(err.Error())
}
fmt.Println("L1 Tx Hash: ", tx.Hash())
Enter fullscreen mode Exit fullscreen mode

This Deposit function called accounts.DepositTransaction contains details about the deposit transaction:

  • Token: utils.EthAddress: This specifies that the deposit is for the wrapped Ethereum token (ETH) on ZKSync.
  • Amount: big.NewInt(1000000000): This sets the deposit amount to 1,000,000,000 Wei (the smallest denomination of Ether).
  • To: wallet.Address(): This specifies the recipient's address for the deposit. Since it's set to wallet.Address(), the funds are being deposited to the same ZKSync wallet that is initiating the transaction.

This function returns a transaction from Ethereum and an error. So we have to confirm this transaction is successfully included in a block on Ethereum (L1) and retrieve the corresponding transaction receipt for further confirmation.

// Wait for the deposit transaction to be finalized on L1 (Ethereum) network
_, err = bind.WaitMined(context.Background(), ethClient, tx)
if  err != nil {
log.Panic(err)
}

// Get the receipt for Ethereum Transaction
ethReciept, err:= ethClient.TransactionReceipt(context.Background(), tx.Hash())
if  err != nil {
log.Fatal(err.Error())
}

fmt.Println("Reciept of Ethereum Tx: ", ethReciept)
Enter fullscreen mode Exit fullscreen mode

The _ indicates that the function might return a value, but we are not interested in it, we just want our transaction to be mined and successful.

Now you can easily use this receipt from Ethereum to retrieve the corresponding transaction on the ZKSync network. Like on Ethereum we also need to wait for the deposit to be finalized on zkSync.

zkTx, err:= ZKClient.L2TransactionFromPriorityOp(context.Background(), ethReciept)
if  err != nil {
log.Fatal(err.Error())
}
fmt.Println("L2 Hash:", zkTx)

// Wait for the deposit transaction to be finalized on L2 (zkSync) network (can take 5-7 minutes)
_, err = ZKClient.WaitMined(context.Background(), zkTx.Hash)
if  err != nil {
log.Fatal(err.Error())
}
Enter fullscreen mode Exit fullscreen mode

After some time, the deposit will be finalized on zkSync and we can proceed to check our zkSync balance once again to compare to the initial address.

// Get balance after deposit. Setting block number to nil so it returns balance at the latest block
balance, err= wallet.Balance(context.Background(), utils.EthAddress, nil)
if  err != nil {
log.Fatal(err.Error())
}
fmt.Println("Balance after deposit:", balance)
Enter fullscreen mode Exit fullscreen mode

Overall this should be your output on your console:

This is a brief overview of what we can do with the Go SDK, next we will go ahead to make a withdrawal from zkSync (L2) to Ethereum (L1).


Implementing Withdrawal

We would get started by creating a connection to both the Ethereum and zkSync networks using the providers declared.

// Connect to the zkSync network
ZKClient, err:= clients.Dial(zkSyncEraProvider)
if  err != nil {
log.Fatal(err.Error())
}
defer  ZKClient.Close() // Close on exit 

fmt.Println("zkSync client connected....")

// Connect to the Ethereum network
ethClient, err:= ethclient.Dial(ethProvider)
if  err != nil {
log.Fatal(err.Error())
}
defer  ethClient.Close() // Close on exit

fmt.Println("Ethereum clients connected......")
Enter fullscreen mode Exit fullscreen mode

This connection would allow you to interact and perform transactions on both networks.
The accounts package from the Go SDK allows us to create a new zkSync wallet object, with this we create an instance of wallet associated with the privateKey we passed in our .env.
For the sake of comparison, we would go ahead and check the current balance of Ethereum in this wallet.

// Create a new wallet using the private key and network clients
wallet, err:= accounts.NewWallet(common.Hex2Bytes(privateKey), &ZKClient, ethClient)
if  err != nil {
log.Fatal(err.Error())
}
fmt.Println("Wallet Created.... ")

// Get balance before withdrawal. Setting block number to nil so it returns balance at the latest block
balance, err:= wallet.Balance(context.Background(), utils.EthAddress, nil)
if  err != nil {
log.Fatal(err.Error())
}
fmt.Println("Balance before deposit is: ", balance)
Enter fullscreen mode Exit fullscreen mode

With this, we have all we need to initiate a withdrawal from zkSync to Ethereum using the withdrawal function.

// Initiate withdrawal from zkSync(L2) to Ethereum(L1)
tx, err:= wallet.Withdraw(nil, accounts.WithdrawalTransaction{
Amount: big.NewInt(1000000000),
Token: utils.EthAddress,
To: wallet.Address(),
})
if  err != nil {
log.Fatal(err.Error())
}
Enter fullscreen mode Exit fullscreen mode

The accounts.WithdrawalTransaction struct contains details about the withdrawal transaction we want to initiate.

  • Amount: big.NewInt(1000000000): This sets the withdrawal amount to 1,000,000,000 Wei (the smallest denomination of Ether).
  • Token: utils.EthAddress: This specifies that the withdrawal is for the wrapped Ethereum token (ETH) on ZKSync.
  • To: wallet.Address(): This specifies the recipient address for the withdrawal. Since it's set to wallet.Address(), the funds are being withdrawn back to our address on the Ethereum mainnet (L1) that corresponds to the ZKSync wallet.

Unlike for deposits, we do not have for the transaction to be mined. However, the withdrawal process still involves waiting for the transaction to be finalized on L1. This finalization confirms the state update on the mainnet and unlocks the corresponding funds on the L1 contract.

You can read more about withdrawal delay here

Now, we can easily check our balance after withdrawal to compare it to the initial balance before we initiated the withdrawal.

// Get balance after deposit. Setting block number to nil so it returns balance at the latest block
balance, err= wallet.Balance(context.Background(), utils.EthAddress, nil)
if  err != nil {
log.Fatal(err.Error())
}
fmt.Println("Balance after deposit:", balance)
Enter fullscreen mode Exit fullscreen mode

Your console output should look like this:

This is a brief overview of what we can do to perform a withdrawal from zkSync to Ethereum using the Go SDK, next we will go ahead to make a transfer from our address to another address.


Implementing Transfer

First step?
Guessed right. We have to establish a connection to both the Ethereum and zkSync networks using the providers declared to allow us to interact and perform transactions on both networks.

// Connect to the zkSync network
ZKClient, err:= clients.Dial(zkSyncEraProvider)
if  err != nil {
log.Fatal(err.Error())
}
defer  ZKClient.Close() // Close on exit 

fmt.Println("zkSync client connected....")

// Connect to the Ethereum network
ethClient, err:= ethclient.Dial(ethProvider)
if  err != nil {
log.Fatal(err.Error())
}
defer  ethClient.Close() // Close on exit

fmt.Println("Ethereum clients connected......")
Enter fullscreen mode Exit fullscreen mode

If you recall from the global variables we created, we have a variable toAddress, which is the recipient of our transfer.
After checking for the current balance of our wallet created, we also checked the current balance of this recipient address using the zkClient connection we established, setting the block number to nil to indicate we are looking for the current balance.

// Create a new wallet using the private key and network clients
wallet, err:= accounts.NewWallet(common.Hex2Bytes(privateKey), &ZKClient, ethClient)
if  err != nil {
log.Fatal(err.Error())
} 

fmt.Println("Wallet Created....")
fmt.Println("Checking both balance before initiating transfer....")

// Get wallet balance before transfer
myBalance, err:= wallet.Balance(context.Background(), utils.EthAddress, nil)
if  err != nil {
log.Fatal(err.Error())
}

fmt.Println("My balance before transfer is: ", myBalance)  

// Check recipient balance before transfer
recipientBalance, err:= ZKClient.BalanceAt(context.Background(), toAddress, nil)
if  err != nil {
log.Fatal(err.Error())
}

fmt.Println("Recipient balance before transfer is: ", recipientBalance)
Enter fullscreen mode Exit fullscreen mode

With this we have all we need to initiate a transfer, passing all the details about the transfer transaction:

  • Token: utils.EthAddress: This specifies that the transfer involves the wrapped Ethereum token (ETH) on ZKSync.
  • Amount: big.NewInt(1000000000): This sets the transfer amount to 1,000,000,000 Wei (the smallest denomination of Ether).
  • To: toAddress: This specifies the recipient address for the transfer.
// Transfer token from my wallet to the recipient address
tx, err:= wallet.Transfer(nil, accounts.TransferTransaction{
Token: utils.EthAddress,
Amount: big.NewInt(1000000000),
To: toAddress,
})
if  err != nil {
log.Fatal(err.Error())
}

fmt.Println("Transaction Hash: ", tx.Hash())
fmt.Println("Waiting for transaction to be mined on the blockchain...")

_, err = ZKClient.WaitMined(context.Background(), tx.Hash())
if  err != nil {
log.Fatal(err.Error())
}
Enter fullscreen mode Exit fullscreen mode

After initiating, you have to wait for the ZK-rollup system to process and finalize the batch containing your transfer. This finalization ensures the validity of the transaction and updates the state of the ZKSync network accordingly.

Read more Finality on Ethereum and zkSync here

At this point, we can check for our balance after the transfer to compare it to our balance before the transfer.

// Check wallet balance after transfer
myBalance, err = wallet.Balance(context.Background(), utils.EthAddress, nil)
if  err != nil {
log.Fatal(err.Error())
}

fmt.Println("My balance after transfer is: ", myBalance)

// Check recipient balance after transfer
recipientBalance, err = ZKClient.BalanceAt(context.Background(), toAddress, nil)
if  err != nil {
log.Fatal(err.Error())
}

fmt.Println("Recipient balance after transfer is: ", recipientBalance)
Enter fullscreen mode Exit fullscreen mode

Now we can run our code using the command below: go run transfer/main.go

We should have this output on our console:

You see how easy and fun it is to play around with the Go SDK and how we can perform transfer with it.

Conclusion

This tutorial taught you how to use the Go SDK to interact with the zkSync network and perform transactions. You can find all the code in the zkSync Interaction repo. Feel free to make any contributions.

Here are some helpful resources for understanding and building on zkSync:

Top comments (0)