Zk Rollup

admin
admin 2022年08月11日
  • 在其它设备中阅读本文章

引言

区块链公链自诞生以来,虽然大大降低了信任的门槛,但一直面临着一个效率问题:即 TPS 不高,低的 TPS 很难支持大型应用。因此业界很多技术人员尝试为区块链扩容。目前扩容方案主要有两类:Layer1,即直接增加链上的交易处理能力,这种方式也被称为链上扩容,常见的技术有:Sharding 和 DAG;Layer2,即将链上的相当一部分工作量转移到链下来完成。常见的技术有:State Channel, Plasma 和 Rollup

  • State Channel:状态通道,需要在链上锁定资金、需要实时监测链上状态防止对方作弊
  • Plasma:需要信任操作者,并存在大规模退出问题
  • Rollup:取决于状态变更在链上验证方式的不同,可分为:

    • Optimistic Rollup:通过欺诈证明来验证状态变更
    • ZK Rollup:通过零知识证明来验证状态变更

在 Optimistic Rollup 中,有一个或者几个验证者,他们通过签名的方式担保“状态 A 在经过这些交易 TX 之后会变成状态 B,我验证了”。然后,在一段时间的挑战期(通常是一周或者两周)之内,如果有人提出“你担保的结果和实际交易执行的结果不符”并提供证据,那么,错误的结果将会被回滚,挑战者可以获得担保人在链上抵押的一部分押金
在 ZK Rollup 中,验证节点在链下通过零知识证明算法,生成一个证明 P 并与状态 A,B 与交易 TX 一起上传上链,相当于是表示“状态 A 在经过这些交易 TX 之后会变成状态 B,你可以通过验证 P 来证实这一点”
在 ZK Rollup 中,不需要挑战期,因为密码学可以保证验算 P 就等价于验证了状态 B 是经由交易 TX 得出的

原理

ZK Rollup 的本质是将用户状态压缩存储在一棵 Merkle 树中,并将用户状态的变更转移到链下来,同时通过零知识证明来保证该用户链下状态变更过程的正确性。在链上直接处理用户状态的变更成本是比较高的,但是仅仅利用链上的智能合约来验证一个零知识证明是否正确,成本是相对低很多的。

transition.png

  • 当前链上合约里所有用户是状态 1
  • 链下收集多个用户的交易,执行这些交易,得到新的状态 2
  • 由这些交易生成一个零知识证明
  • 将旧的状态 1、新的状态 2、零知识证明作为参数,向合约提交新的状态变更
  • 合约验证证明的有效性,并直接应用新的状态 2

账户状态模型

Merkle.png
ZK Rollup 使用 Merkle 树来存储管理所有帐户,这棵树是存储在合约中的,代表了 Layer2 中所有用户及其资产,和区块链自身的世界树没有关系。叶子节点保存帐户状态,包括 public key,nonce 和 balance,叶子节点在 Merkle 树中是有唯一位置的,因此位置的索引信息可间接引用这个账户。
如果用 3 个字节来表示这个索引信息的话,那么这棵 Merkle 树总共可以支持 2^24 = 16,777,216 个账户。这对于一般的系统来说已经足够。因此,以太坊为例,账户地址可以由 20 个字节转为 Merkle 树的叶子节点索引 3 个字节。这样账户地址就被“压缩”了。
除了对账户地址进行压缩,也可以对转账金额数据进行压缩。例如,在以太坊上金额用 256 位的大整型来表示,但是实际使用过程中几乎很少用到超大金额和超小的金额。因此如果就假设系统中转账的最小单位是 0.001ETH,并且用 4 个字节来表示转账金额的话,就可以支持 0.001 ~ 4,294,967.295ETH 的转账,这对于一般的系统来说也已经够了。
手续费与转账金额类似,实际手续费会在一定的范围之内浮动,因此也可以为手续费设定一个最小单位,例如:0.001ETH。然后用 1 个字节来表示 0.001 ~ 0.255ETH 的手续费。这里的手续费就是用户向 Layer2 交易打包者支付的交易费用

交易流程

当向链上提交一个交易批次时,会提议新的 Merkle root(以反映更新后的 Merkle 树),同时也在该批次中包含了所有转账账户的新状态。伴随批次提交的还有一个零知识证明,合约会验证该证明,若通过则接受新的 Merkle root,应用新的账户状态。
stateRoot.png

一个简单的 rollup 主要包含 3 类主要的活动:存款、转账、取款

存款
deposits.png
存款是指:向 rollup 合约发送指定资产 token。rollup 合约会将这些 token 添加到存款队列。某个时刻,coordinator 会取一定数量的存款,将其添加到 rollup。
向 rollup 合约存款,此时存款队列中有 1 个存款:

Hash(pubkey, amount, token type) = 0x1234abcd…

再存入一笔:

Hash(pubkey, amount, token type) = 0x9876fedc…

此时存款队列为:

[0x1234abcd, 0x9876fedc]

此时有偶数个存款,会对队列中的最后 2 个进行哈希:

Hash([0x1234abcd, 0x9876fedc]) = 0x6663333

此时存款队列为:

[0x6663333]

该单一哈希值表示了向合约存入的 2 笔存款,但是,coordinator 如何知道该哈希值代表的具体内容呢?我们如何得知该哈希值是对应一笔存款,还是 2 笔,甚至是 4 笔?有 2 种方式来解决该问题:

  1. 每笔存款会释放一个 event,coordinator 可订阅该事件,从而可跟踪每笔存款的详细内容。
  2. 合约会维护待定存款队列的大小计数以及待定存款子树高度。

接下来,如何将队列中的存款移到 rollup 呢?
rollup 中所有的存款和帐户余额都是存储在稀疏默克尔树中的,分别称为 存款树 帐户树 。该默克尔树具有固定大小,预初始化为零值(或相应的哈希值)。

为了处理待定存款,coordinator 会从待定存款队列中取第一个元素,然后插入到 rollup 的存款树中的合适高度,这将产生新的存款树及其根, coordinator 将其发布到 rollup 合约。
为生成新的帐户树,必须确保 coordinator 将该存款子树插入帐户树的空的部分位置。为此,coordinator 需提交帐户树相应级别的空节点的默克尔证明,然后将其替换为存款子树的根。

转账
一旦存款成功,可将其资金在 rollup 账号间快速、便宜地转账。具体为,向 coordinator 发送一个转账交易,然后 coordinator 将其与其他交易打包并提交到 rollup 合约,同时也会提交新的帐户树以及零知识证明。

可根据批次中的交易来派生出新的帐户树。如果 Alice 提交一笔交易说“向 Bob 发送 10 个代币”,那么协调者会将 Bob 的余额增加 10,将 Alice 的余额减少 10,重新哈希账户数据以获得新的账户叶子,并重建帐户树。这会产生一个新的默克尔根,并将其发送到智能合约。
通过订阅智能合约释放的存款事件,每个 coordinator 都维护了本地存款数据库。当收到一笔交易时,coordinator 会根据其数据库做如下验证:

  • 该交易是正确的,并对应付款人的公钥。
  • 付款人的账号存在于帐户中。
  • 转账的金额未超过付款人的余额。
  • 收款人账号存在于帐户中。
  • 付款人和收款人具有相同的 token 类型。
  • 付款人的 nonce 值正确。
  • 不存在上溢或下溢情况。

一旦验证通过,该交易会被添加到队列中。一旦队列内有一定数量的交易,coordinator 将创建批次。为此,coordinator 需编译用于零知识电路的一堆输入,用于生成成证明,具体包含:

  • 为批次内的所有交易创建一棵默克尔树,可填充虚拟交易以满足电路的尺寸要求,然后为每笔交易创建证明。
  • 为所有交易的每个收款人和付款人创建一组证明,以证明收款人账号的存在性和付款人账号的存在性。
  • 每笔交易更新付款人和收款人账号引起的对帐户树的更新,基于此创建派生出的一组中间 root 值。(可向合约证明批次内的交易都正确应用。)

该电路具有 3 个公开的输入:pre-state root、post-state root、transaction root,该电路通过遍历所有交易创建证明,执行与 coordinator 相同的检查和验证。每次迭代,电路会:

  • 首先借助账号 merkle proof 检查付款方存在于当前 root 下。
  • 减少付款人的余额,增加其 nonce,对更新的帐户重新哈希,然后利用该新哈希值与账号的 merkle proof 一起,派生出新的 merkle root。
    circuit.png

提交和取款

一旦电路为所有状态更新完成了零知识证明创建,coordinator 会将该证明提交到智能合约,电路会验证该证明,并验证该证明中的 3 个公开的输入:pre-state root、post-state root、transaction root。
若证明中的 pre-state root 与记录在合约内的当前存储树匹配,且证明有效,则合约会从证明中提取 post-state root,更新当前存储树以与其匹配。
此时,任何存款人都可向合约请求验证其余额:对其账户进行哈希,将该哈希值提交给合约,同时提交 merkle proof 来验证当前账户。
证明中的 3 个公开输入之一为 transaction root,该 root 对应的 merkle tree 中包含了输入到电路内的所有交易。每笔交易都关联一个 merkle proof 可验证该 transaction root。当向合约提交批次时,合约会记录该 transaction root,使得任何存款人都可验证其交易包含在该批次内。
为了取款,存款人向账户中索引 0 的账号发送资金。该索引的账号保留为该用途,将资金发送到该账号并燃烧 L2 的资金。一旦该交易(向索引 0 账号转账的交易)被包含在批次中,存款可向智能合约发送区块申请,采用以上机制,提交证明,取款申请中包含了:

  • 交易详情
  • 默克尔证明
  • 交易树根
  • 一个用来接收 token 的 L1 账户
    一旦交易存在性验证通过,智能合约将检查区块是否已处理,然后向 L1 的特定收款方发送资金。

数据可用性

rollup 与 plasma 的本质区别在于,rollup 为混合 L2 协议。即将计算从 L1 中移除,而将数据仍保留在 L1 中。这就意味着,一旦数据提交到 L1,任何人都可以重建该 rollup 然后接收交易和创建批次提交等等。
为了使使用 L1 作为数据可用性层成为可能,交易被压缩并作为 calldata 发布到智能合约。这节省了大量空间,每字节 16 gas,节省空间意味着节省 gas。从而实现高交易吞吐量。
根据 Solidity 文档可知:
“Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.”
使用 calldata 的原因在于其是最便宜的可用存储形式。以 calldata 参数传输的状态修改并不会存在在以太坊 state 中,但是以太坊节点会在区块创建时存储该交易数据。通过数据压缩节约 gas,具体为:
tx.png

https://learnblockchain.cn/article/3195
https://learnblockchain.cn/2019/11/07/zk-rollup
https://blog.csdn.net/mutourend/article/details/125971262