- Introduction: Zero Knowledge Rollups & zkSync
- Introducing zkSync-go SDK
- Interacting with zkSync using the Golang SDK
- Conclusion
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 .
- 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"
)
- 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
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......")
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)
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())
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 towallet.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)
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())
}
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)
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......")
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)
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())
}
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 towallet.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)
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......")
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)
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())
}
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)
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)