Layer2 – zkSync源代码导读

Layer2是一个大方向。最近一段时间,会围绕L2写一些文章。区块链技术有趣的地方就是一种技术的发展打开了一扇窗。零知识证明的发展拓宽了区块链L2的视角,提供了L2的另外一种实现可能,即zk Rollup。从另外一个角度看,区块链的进步是缓慢的,需要很多优秀的人才跨界把不同的技术揉杂在一起,让不可能成为可能。
matter labs在zk Rollup方向的积累比较深,有一系列的开源项目可以学习。让我们从zkSync开始吧。
zkSync的源代码地址,所有的逻辑使用rust语言开发:
https://github.com/matter-labs/zksync.git
特别注意的是,查看完整源代码需要查看dev分支,并不是master分支。master分支提供编译后的各种执行程序。本文采用的代码的最后一个提交信息如下:
commit dcdddeda4d6eb1dace5ec56cf9fe091ed5e062fd
Merge: 37c5976bd aab5608c3
Author: Vitaly Drogan <[email protected]>
Date:   Thu Sep 17 17:07:05 2020 +0300
    Merge pull request #948 from matter-labs/popzxc-minor-sdk-bugfix
    
    Minor SDK bugfix
1. 源代码目录结构

zkSync项目实现了L2相关的方方面面。查看zkSync的提交记录,这个项目创建于2018年的11月份,已经有两年的开发时间。从matter labs公开的各种项目,matter labs在zk rollup想的很远,也已经走的蛮远。细想想,踏踏实实地思考区块链技术,认认真真地把一小点积累做好。整个源代码目录结构如下:

bin – 一些二进制程序和脚步
contracts – 智能合约逻辑
core – L2的核心逻辑
docker – docker相关的配置
docs – 文档,比较详细的解释了zkSync的协议设计,电路实现,智能合约,K8如何配置,以及如何启动等等。
etc – 配置信息
js – zkSync客户端以及浏览器(explorer)
zkSync的内容比较多,这篇文章主要给大家讲讲zkSync的大体逻辑结构,重点介绍智能合约以及core的相关逻辑。所有zkSync的核心逻辑都实现在core的目录下:

core/circuit – 零知识证明电路实现
core/crypto_exports – 零知识证明底层库封装
core/data_restore – 状态恢复的相关逻辑
core/eth_client – eth客户端的封装
core/loadtest – 压力测试相关逻辑
core/models – zkSync的模型定义
core/plasma – zkSync的状态实现
core/prover – 零知识证明的证明生成引擎
core/server – 各种server的实现,包括eth的监测(watch)和交易发送(sender),交易费用的计算(fee_ticker),零知识证明的服务器(prover server)以及API服务等等
core/storage – 所有状态的存储以及持久化逻辑
2. 整体架构
zkSync目前主要实现了L2的转账功能。L2有自己的账户系统以及状态。zkSync总体的功能模块以及相互之间的关系如下:

Eth Watch以及Eth Sender负责监控和发送zkSync智能合约的交易。Mem Pool负责收集交易。交易分为两种两种:L1交易和L2交易。Block Proposer将交易打包,并更改世界状态(Plasma State)。在世界状态更改后,通过Block Committer生成证明需要的信息。零知识的证明通过Plonk证明系统生成,其中包括Prover Server和Proving Client。API提供接口实现UI的信息展示和L2的交易提交。
3. L2账户系统和世界状态
zkSync并不需要独立生成新账户。zkSync的L2账户和L1账户一一对应,“共享”一份私钥。

简单的说,L1的私钥的ECDSA签名的结果作为L2账户的私钥。L2账户的秘钥是JubJub曲线上的Scalar值,对应的公钥是对应椭圆曲线上的点。L2的状态包括两部分:账户以及账户下所有Token的余额。

账户以及支持的Token个数定义在etc/env/dev.env文件中:
ACCOUNT_TREE_DEPTH=32
BALANCE_TREE_DEPTH=11
目前支持2^32个L2账户,2^11个Token。每个L2账户都有一个唯一的编号,从零开始。编号0,默认为Validator的账户。Account节点包括如下信息(其中PubKeyHash就是L2账户的公钥信息):

每个Token也都有唯一编号。Token节点包括如下信息:

zkSync支持的Token信息由L1智能合约维护,具体逻辑可以查看contracts/contracts/Governance.sol的addToken函数。
4. 交易类型和区块
zkSync目前支持如下交易类型(操作类型):
Noop – 空操作(L2)
Transfer – 转账(L2)
Transfer to new – 转账给新账户(L2)
Withdraw (Partial Exit)- 转出资金(L2)
Deposit -转入资金(L1)
Change pubkey – 更改L2的公钥信息(L2)
Full exit – 退出,从Layer1发起的交易,退出L2(L1)
close操作 – 退出,从Layer2发起的交易(新版本已经废除,L2)
L2的区块信息相对简单,总体的结构如下:

L2的区块信息包括:新的世界状态的树根(new root hash),区块交易费用接收账户以及多个交易信息。交易信息分为两种:一种是L2的交易,从L2的角度看,这些才是“正常”交易(transaction);另外一种是从L1发起的交易(Deposit以及Full exit)。
从L1发起的交易,在代码中常常被标记为”Priority”交易。相对来说,从L1发起的交易,相对L2的交易优先级确实更高一些。
以Deposit交易为例,介绍一下L2账户创建的流程:

在L2创建账户,其实涉及到两个交易类型。Deposit是L1的Priority的交易。在Eth Watch监测到交易后,发起L2的交易,并在世界状态添加状态。在账户创建后,再通过Change pubkey更新L2的公钥信息。
5. 基本流程
在明确了账户系统以及状态后,整个L2的打包以及更改世界状态的流程还是比较清晰的:

一切从NewTx开始。通过API,创建好的Transaction添加到Mem Pool。详细逻辑可以查看core/server/src/mempool.rs的add_tx函数。Block Proposer每隔一段时间,查看Mem Pool中的有效交易,并打包。详细逻辑可以查看core/server/src/block_proposer.rs的run_block_proposer_task函数。
区块打包成功后,更新世界状态,详细逻辑可以查看core/server/src/state_keeper.rs的execute_tx_batch。
更新了世界状态后,要将最新的世界状态进行证明,详细逻辑可以查看core/server/src/committer.rs的commit_block函数。
证明通过Eth Sender发送给L1。
6. Plonk证明系统
zkSync采用Plonk零知识证明的算法,证明L2的交易和状态的正确性。Plonk零知识证明的算法,和Groth16算法不一样,是Universal的零知识算法。Plonk算法的技术细节后面会单独写文章详细介绍。zkSync设计了相对独立的Plonk证明系统,示意如下:

Plonk证明系统涉及到三个功能模块:Block Committer,Prover Server以及Prover。这三个模块之间的数据传输通过Storage存储模块。Block Committer将Block信息存入Storage,Prover Server从Storage提取Block并生成证明需要的信息,并再次存入Storage,Prover从存储中查看需要证明的信息,生成零知识证明。
Prover Server和Prover之间的调用关系,示意如下:

详细逻辑可以查看core/server/src/prover_server以及core/prover目录。
7. zkSync电路实现
zkSync在bellman的基础上增加了Plonk零知识证明系统。zkSync的电路设计非常有趣,由于篇幅原因,后面会单独介绍zkSync的电路实现原理。简单的说,zkSync采用Chunk设计,将交易切分为多个Chunk,支持不同的区块大小。看zkSync源代码的小伙伴,在理解电路逻辑前,先理解好Chunk的逻辑。
8. 状态确定以及L1智能合约
zkSync的交易可以由Layer1或者Layer2的操作发起。简单的说,zkSync的交易经历三个状态:1/Request 2/ Committed 3/ Verified。只有Verified的操作才是确定性状态。从状态的角度看,交易的流程如下图所示:

某个交易在打包后,会将交易对应的Pub Data信息提交到链上,交易进入Committed状态。在交易对应的区块提交证明后,改交易进入Verified的状态,也就是确定性状态。
L1智能合约提供了commitBlock和verifyBlock接口实现Pub Data信息以及证明信息的提交:
    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
具体的逻辑,感兴趣的小伙伴可以查看contracts/contracts/ZkSync.sol源代码。
总结:
zkSync通过zk Rollup协议,实现了L2的转账。zkSync项目非常完整,是学习L2非常好的参考项目。zkSync采用Plonk零知识证明算法向L1证明状态的正确性。Plonk算法是Universal的零知识证明算法,只需要一次可信设置。zkSync电路设计采用Chunk设计,支持不同的区块大小。