L2 — Deep into zkSync Source Code
Layer2 is a big trend. We will be exploring the L2 topic and posting some discussions lately. The interesting thing about blockchain technologies is the development and iteration of one technology usually creates pathway to other technologies. Zero-Knowledge Proof expands the scope of blockchain L2, providing an alternative implementation of L2 — the zk Rollup. Taking it from another angle, the progress of blockchain technology advancement is made slowly, in need of all talents to glue together different technologies from multiple disciplines, and making impossible possible.
Matter Labs has taken deep dive into zk Rollup and they own a series of open source projects. Let us get started with zkSync.
Here is zkSync source repo, all logics implemented in rust:
https://github.com/matter-labs/zksync.git
Note that you should refer to the dev branch instead of master branch for complete source code. Master branch is where compiled executables reside. The last submission for the code referred in this article is shown as below:
commit dcdddeda4d6eb1dace5ec56cf9fe091ed5e062fd
Merge: 37c5976bd aab5608c3
Author: Vitaly Drogan <vd@matterlabs.dev>
Date: Thu Sep 17 17:07:05 2020 +0300
Merge pull request #948 from matter-labs/popzxc-minor-sdk-bugfix
Minor SDK bugfix
1.0 Source Code Structure
zkSync implements all aspects of L2. Looking at the log history for zkSync, we see this project was created in November 2018, so it has 2 years development history. Relating to all public repos of matter labs, we can tell they have planned a long journey map for zk rollup, and in fact they have already traveled a long journey. The takeaway is to think big and to start small, one step at a time. The complete structure of the source code follows below:
bin — some binaries and footprints
contracts — smart contract logic
core — L2 core logic
docker — docker realted configurations
docs — documentation. Here it explains in details the contract design for zkSync, circuit implementation, smart contract, K8 configuration, and how to start with it, etc.
etc — config specs
js — zkSync client app and explorer browser
zkSync covers a lot of solid contents. This article talks mainly about the general logic of zkSync, emphasizing on smart contract and core logics. All zkSync core implementation is under the core directory:
core/circuit — zero-knowledge proof circuit implementation
core/crypto_exports — zero-knowledge proof low level encapsulation
core/data_restore — restoration related logic
core/eth_client — eth client app encapsulation
core/loadtest — pressure test related logic
core/models — zkSync model defination
core/plasma — zkSync state implementation
core/prover — zero-knowledge proof generator
core/server — implementation of all servers, including the eth Watch and Sender, fee_ticker. zero-knowledge prover server, API server, and so on.
core/storage — all state storage and endurance logic.
2.0 Overall Structure
As of now zkSync mainly implements the transaction feature of L2. L2 has its own account system and states. zkSync modular function and inter-relationship shows as below:
Eth Watch and Eth Sender is in charge of watch and send zkSync smart contract transaction. Mem Pool takes care of transactions.There are two types of transaction: L1 transaction and L2 transaction. Block Proposer packs transaction, and updates Plasma State. After the Plasma State change, it generates proof required information through Block Committer. Zero-knowledge proof generates by Plonk proving system, including Prover Server and Proving Client. L2 API provides interfaces to UI information visualizations and transaction submission.
3.0 L2 Account System and Plasma State
zkSync does not generate new account independently. zkSync L2 accounts maps to L1 accounts, sharing one set of private key.
In short, L1 private key ECDSA signature result is the private key for L2 account. L2 account private key is represented by the Scalar value on the JubJub curve, and corresponding public key is the point on the elliptic curve. L2 state contains two parts: account, and Token balance under accounts.
Account and number of supported tokens is defined in etc/env/dev.env:
ACCOUNT_TREE_DEPTH=32
BALANCE_TREE_DEPTH=11
2³² L2 accounts, and 2¹¹ Tokens are currently supported. Every L2 count has its unique id, starting from 0. ID 0 is the Validator account by default. Account nodes include below information (PubKeyHash represents the public key for L2 account):
Each Token has its unique id. Token node contains below information:
Token supported by zkSync is under L1 smart contract maintenance. Please refer to function addToken at contracts/contracts/Governance.sol.
4.0 Transaction Type and Blocks
zkSync now supports these transaction (operational) types:
- Noop — No operation(L2)
- Transfer — transfer(L2)
- Transfer to new — transfer to new account(L2)
- Withdraw (Partial Exit)- cash withdrawal(L2)
- Deposit -cash deposit to account(L1)
- Change pubkey — change L2 public key information(L2)
- Full exit — end transaction initiated from Layer1 and exit L2(L1)
- close operation — endtransaction initiated from Layer 2 and exit(deprecated in new version, L2)
L2 Block information is rather simple, summarized as below:
L2 Block information includes: new root hash of Plasma state, recipient account for block transaction fee and transactions. There are two types of transactions: L2 transaction, the “normal” transaction from L2 perspective; transactions initiated from L1, including Deposit and Full exit.
Transactions initiated for L1 are usually marked as “Priority” in the code. Relatively speaking, L1 transactions do have higher priotity than L2 ones.
Take Deposit transaction as an example, let us go through the workflow of L2 account creation:
Creating an account in L2 involves two transaction types. Deposit is transaction of L1 Priority. When Eth Watcher detects the transaction, it initiates the L2 transaction, and adds to the Plasma state. After the account created, it updates L2 public key information by using Change pubkey.
5.0 Basic Workflow
After determining the account system and states, the whole L2 package and Plasma State update process is quite clear:
Everything starts with NewTx. We add created Transaction to Mem Pool through API. Refer to add_tx function for detailed logics core/server/src/mempool.rs. Block Proposer checks valid transactions in Mem Pool and packs them periodically. For detailed logic please see function run_block_proposer_task in core/server/src/block_proposer.rs.
After blocks are packaged successfully, the Plasma state is then updated. For detailed logic please see function execute_tx_batch in core/server/src/state_keeper.rs.
After updaing the Plasma State, we need to prove the new Plasma State. For detailed logic please see function commit_block in core/server/src/committer.rs.
The proof will be sent to L1 through Eth Sender.
6.0 Plonk Proving System
zkSync utilizes Plonk zero-knowledge proof algorithm to prove correctness of the L2 transaction and state. Plonk zero-knowledge proof algorithm, different from the Groth16 algorithm, is an Universal zero-knowledge proof algorithm. Technical specs about Plonk algorithm will come later with more details in a separate article. zkSync designs a relatively independent Plonk proving system, demonstrated below:
Plonk proving system involves three functional modules: Block Committer,Prover Server and Prover. Data is transferred among these three models through Storage module. Block Committer saves Block information in Storage, Prover Server then extracts Block information from Storage and generate information needed for generating proof , save again to Storage. Prover gets data required for proving from Storage, and generates zero-knowledge proof.
This diagram shows inner relationship between Prover Server and Prover.
Details logic please refer to core/server/src/prover_server and core/prover.
7.0 zkSync circuit implementation
zkSync increments Plonk zero-knowledge proof based on the foundation of bellman. zkSync has a very interesting circuit design, which will come later in more details separately so we don’t deviate too far from here. In short, zkSync implements with Chunk design, which splits the transaction into multiple Chunks to support different block size. For folks looking into zkSync source code, you should prepare yourself with a good understanding of Chunk logic before going to the circuit logic.
8.0 State Confirmation and L1 Smart Contract
zkSync transaction can be initiated by both Layer1 or Layer2. There are three states in zkSync transaction lifecycle: 1/Request 2/ Committed 3/ Verified. Only Verified operation is the state of confirmation. The transaction flow diagram viewing from the state perspective is shown below:
After certain transaction packaged, Pub Data information related to the transaction will be submitted to blockchain, and therefore transaction changes to Committed state. The block where transaction happened submits prrof, and then enters Verified state, which is also known as the confirmed state.
L1 smart contract provides commitBlock and verifyBlock interfaces to implement submission function for Pub Data information and proof information;
function commitBlock(
uint32 _blockNumber,
uint32 _feeAccount,
bytes32[] calldata _newBlockInfo,
bytes calldata _publicData,
bytes calldata _ethWitness,
uint32[] calldata _ethWitnessSizes
)
function verifyBlock(uint32 _blockNumber, uint256[] calldata _proof, bytes calldata _withdrawalsData)
external nonReentrant
Interested folks can check out the detailed logic at contracts/contracts/ZkSync.sol.
Summary:
Through zk Rollup protocol, zkSync implements L2 balance transfer. zkSync is a wholesome project, which serves a great reference for studying L2. zkSync utilized Plonk zero-knowledge proof algorithm to prove correctness of state to L1. Plonk algorithm is an Universal zero-knowledge proof algorithm, in need of just one-time trusted settings. zkSync circuit takes Chunk design to support various block sizes.