背景
区块链公链自诞生以来,一直面临着TPS (Transactions Per Second)不高的问题(例如比特币每秒仅支持7笔交易,以太坊的TPS只有15也就是每秒只能处理15笔交易)。这严重限制了区块链应用的大规模落地。因此业界很多技术人员尝试为区块链扩容。但区块链扩容受到Vitalik提出的不可能三角的限制,不可能三角是指区块链系统设计无法同时兼顾可扩展性,去中心化和安全性,三者只能取其二。这是一个很让人失望的结论,但我们必须知道,一切事物都有自己的边界,公链不应该做所有的事情,公链应该做它该做的事情:“公链是以最高效率达成共识的工具,能够以最低成本来构建信任”。作为共识的工具,信任的引擎,公链不应该为了可扩展性放弃去中心化与安全性。那么公链的TPS这么低,该怎么使用呢?我们是否可以将大量的工作放到链下去解决,仅仅将最重要的数据提交到区块链主链上,让所有节点都能够验证这些链下的工作都是准确可靠的呢?
目前扩容方案主要有两类:
Layer 1 扩容方案,即直接增加链上的交易处理能力,这种方式也被称为链上扩容。常见的技术方案有:Sharding 和 DAG;
Layer 2 扩容方案,即将链上的相当一部分工作量转移到链下来完成。常见的技术有:State Channel, Plasma, Truebit 和 最近比较火的 ZK Rollup。
ZK Rollup是什么?
ZK Rollup就是基于零知识证明的二层扩容方案(Layer2), ZK Rollup方案起源于18年下半年,由Barry Whitehat和Vitalik先后提出。Rollup顾名思义有“卷起”和“汇总”的意思,将大量的交易“卷起/汇总”打包成一个交易,ZK Rollup的原理一句话就可以讲清楚:链下进行复杂的计算和证明的生成,链上进行证明的校验并存储部分数据保证数据可用性。Layer1来保证安全和去中心化,绝对可靠、可信,Layer2追求极致的性能。
原理
ZK Rollup 的本质是将链上的用户状态压缩存储在一棵 Merkle 树中,并将用户状态的变更转移到链下来,同时通过 ZK-SNARK(零知识简洁非交互式知识论证) 的证明来保证该链下用户状态变更过程的正确性。在链上直接处理用户状态的变更成本是比较高的,但是仅仅利用链上的智能合约来验证一个零知识证明的 Proof 是否正确,成本是相对低很多的。另外必要的转账信息也会被和证明一起提交到合约,方便用户查账。
交易过程
ZK Rollup 系统中包含两类角色:transactor 和 relayer:
transactor,即普通用户,对应以太坊上的外部账户。用户构建转账交易并用私钥签名,然后将交易发送给 relayer。
relayer 负责收集并验证用户的 transaction,之后将 transaction 批量打包,并生成 ZK-SNARK 的 Proof,最终将用户 transaction 中的核心数据和 ZK-SNARK 的 Proof 以及新的全局用户状态的 Merkle 根提交到链上的智能合约。
当 relyer 收到 transaction 后,必须 “执行” 它。transaction 的执行,本质上是改变相关账户的状态,而 STF 就是改变账户状态的函数。STF 是状态迁移函数(state transition function)的缩写。
状态是针对状态机而言的,每个状态机在某一时刻都有一个状态。假设当前状态机的状态是 PREV_STATE,然后有 n 个 Action T[1], T[2], …, T[n] 依次作用到状态机上,之后状态机的状态是 POST_STATE,此可以表示为:
POST_STATE = STF(PREV_STATE, T[1], T[2], …, T[n])
如果将以上 Action 换成转账交易 transaction,把 系统中的账户集合看作是一个状态机,那么整个过程也就是链上交易执行的过程了。交易的执行,使得整个链上的全局状态发生变化。链上的全局状态也就是各个账户的状态集合,将所有账户的状态组成一棵 Merkle 树,树的叶子节点是账户状态,树的根可以直接用来表示状态集合。因此,上述的 PREV_STATE 和 POST_STATE 也就是全局账户状态树的根。
下图表示此工作过程,黄色的表示transactor发送的交易,绿色的表示relayer维护的merkle tree,relayer执行交易后本地的merkle tree root会由prev state root转换成post state root,图中蓝色的表示relayer生成的证明账户状态转移有效的零知识证明。
Relayer把prev state root,post state root,交易数据和proof证明提交至链上合约,合约校验proof通过后会将来新的状态写入到链上,合约不需要单独校验每笔交易的合法性,只需要校验proof是否有效,降低了链上gas消耗,其中交易数据是存储在较便宜的位置CALLDATA上。链下每一次的状态转变都需要提供零知识证明,由主链上的合约进行验证,只有验证通过才能更改状态。即每一次状态转变都严格依赖密码学证明。
当然 relayer 不会免费为 transactor 提供服务,毕竟 relayer 向链上提交证明和数据是需要消耗 gas 的。因此,transactor 向 relayer 发送的交易里,也必须包含相应的手续费。
交易压缩
ZK Rollup生成的证明大小(很小),验证时间(很快基本上是常数),不会随着交易数量的增长而变大,所以ZK Rollup可以极大地提高TPS。影响ZK Rollup链上性能的只有链上CALLDATA存储数据的成本,随着以太坊升级,CALLDATA使用成本降为原来的1/4,ZK Rollup的性能则获得4倍提升,TPS可达到近2000左右。
上链的数据中prev-state root,post state root与proof基本上是不会随着交易增长变化的,只有交易数据会随着交易增长变大,所以为了能在一个区块链中容纳更多的交易,需要对上链的交易进行压缩。
最简单的账户状态可以包含:账户的 public key,nonce 和 balance。而叶子节点在Merkle 树中是有唯一位置的,因此位置的索引信息可间接引用这个账户信息。ZK Rollup使用merkle tree来记录地址,这样账户地址就可以表示成merkle tree的索引值,地址数据的大小就从原本的20 bytes减少到3 bytes。
除了对账户地址进行压缩,我们也可以对转账金额数据进行压缩。例如,在以太坊上金额用256位的大整型来表示,但是实际使用过程中几乎很少用到超大金额和超小的金额。因此如果我们就假设系统中转账的最小单位是 0.001 ETH,并且用 4 个字节来表示转账金额的话,我们就可以支持 0.001 ~ 4,294,967.295 ETH 的转账,这对于一般的系统来说也已经够了。如果还不够可以适当再增加字节来表示金额,或者引入浮点数表示方式。
手续费与转账金额类似,实际手续费会在一定的范围之内浮动,因此我们也可以为手续费设定一个最小单位,例如:0.001 ETH。然后用 1 个字节来表示 0.001 ~ 0.255 ETH 的手续费。这里的手续费也就是 transactor 向 relayer 支付的手续费。
同样,我们假设在正常使用环境下一个账户的转账次数不会达到上万次,因此用2个字节来表示账户的 nonce 也差不多够了,因为 2 个字节 可以表示的范围达到 0~65535。
最后签名字段不能压缩,以以太坊为例,签名 (r, s, v) 总共需要 65 个字节。
因此,一个 transaction 的格式大体如下:
以上 transaction 各字段的长度仅作参考,在实现具体系统的时候需要根据实际情况调整字段长度,以防止发生字段溢出的情况,但原则上还是能省则省。因为交易数据越少,在相同存储容量的前提下,所能容纳的交易数也就越多。实际中提交上链的交易数据是 transaction 的简化信息,不包含 nonce 和 signature部分,在本例中即为11个字节,上链的数据将会变得更小。
证明验证
前面提到,在ZK Rollup 的系统里,所有用户的账户信息由一棵 Merkle 树管理。而 Merkle 树的根被记录在了链上的智能合约里,这个根的值也代表着整个系统当前所有账户的状态。当有用户发起转账 transaction 时,这个状态就要发生改变,但改变必须依照规则进行。在此过程中relayer所做的工作有:
1.首先,必须要确保 transaction 的合法性
2.转出账户是否有足够的钱支付转账金额和手续费
3.转出账户的 nonce 是否正确
4.转账 transaction 的签名是否正确
5.接着,relayer 执行该转账 transaction,修改 Merkle 树中的转出账户和转入账户的叶子节点,然后重新计算新的 Merkle 树的根。
6.重复第二步,relayer 会按照先后顺序一次性处理多个 transaction,然后将最后计算得到的 Merkle 树的根作为新的状态提交到链上合约中。
但上述流程存在问题:如果仅提交状态树的根到合约,那么用户如何相信新的状态根是如实地根据上述逻辑计算出来的。万一 relayer 作恶,故意调大 transaction 的手续费呢?
解决这个问题的一个方法是,要求 relayer 提交状态树的根到合约时,同时也将所有 transaction 提交到合约,这样任何人都可以根据这些 transaction 来验证 relayer 在计算新的状态树时,有没有作弊。但这等于是将所有链下的数据搬回了链上,没有实现 layer 2 扩容的目的。
利用零知识证明就可以很好地解决这个问题。ZK Rollup 中的 ZK 也就是Zero-Knowledge的缩写。relayer 在收集了一系列的 transactions 后,需要用预先定义好的 ZK circuits 来生成一个 Proof:
确保每个交易 T[1], T[2], …, T[n] 中的 nonce, value, fee 等值都没有问题,且 signature 正确。
确保状态迁移过程没有问题,即 STF(PREV_STATE, T[1], T[2], …, T[n]) = POST_STATE
然后将这个 Proof 与 POST_STATE, t[1], t[2], …, t[n] 一起提交到链上合约。其中 t[1], t[2], …, t[n]是 transaction 的简化信息,不包含 nonce 和 signature。所以 t[i] 比 T[i] 更小。
然后智能合约只需要验证这个 Proof 是否正确。如果 Proof 正确且合约中保存的状态与 PREV_STATE 相等,那么新的状态 POST_STATE 将会被记录到合约中,替换原有状态。
由于 relayer 必须生成 ZK-SNARK 的 Proof,然后才能向合约提交,因此如果 relayer 作恶修改用户的 transaction,那么 Proof 将无法被验证通过。
另外,由于提交到链上的交易 t[1], t[2], …, t[n] 是不包含 nonce 和 signature 的,因此上链的数据将会变得更小(上例中每个 transaction 仅会有11个字节上链)。
此时,relayer 由于证明的限制,已经无法修改用户的 transaction。但是恶意的 relayer 依然可以拒绝为某个 transactor 服务,不搜集该 transactor 的 tranaction。为了防止这种行为,合约上必须支持 on-chain withdrawal,也就是任何一个 transactor 都可以从链上将自己的 token 提走。
(有关ZK SNARK生成Proof过程的阐述)
该过程中使用的零知识证明为ZK-SNARK,为了便于理解其工作过程,下面先通过一个例子引入ZK SNARK的相关知识
假定A需要向B证明他知道c1,c2,c3,使(c1⋅c2)⋅(c1+c3)=7,按照惯例,c1,c2,c3需要对B保密。
第一步就是把计算“拍平”,通过基本的运算符把原计算画成这样的“计算门电路”。
S1=C1C2
S2=C1+C3
S3=S1S2
通过增加中间变量,把复杂的计算拍平,使用最简单的门电路表达。新的门电路跟原计算是等价的。
第二步就是把每一个门电路表示为等价的向量点积形式,这个过程成为R1CS。
对每个门电路,我们定义一组向量(a,b,c),使得s . a * s . b - s . c = 0。其中s代表全部输入的向量,也就是[C1,C2,C3,S1,S2,S3],为了让加法门也能用同样的方式表达,我们增加一个虚拟的变量成为one,s向量变成[one,C1,C2,C3,S1,S2,S3]。
对应到第一个门计算出a=[0,1,0,0,0,0,0]b=[0,0,1,0,0,0,0]c=[0,0,0,0,1,0,0]把s,a,b和c代入s . a * s . b - s . c = 0,得到C1*C2-S1=0,即这个向量表达跟第一个门是完全等价的。同理我们可以计算
第二个门a=[1,0,0,0,0,0,0]b=[0,1,0,1,0,0,0]c=[0,0,0,0,0,1,0]
第三个门a=[0,0,0,0,1,0,0]b=[0,0,0,0,0,1,0]c=[0,0,0,0,0,0,1]
接下来是最重要的一步,把向量表达式表示为多项式,从而把向量的验证转化为多项式的验证,这个过程称为QAP(Quadratic Arithmetic Programs)。
我们选定1,2,3,寻找一组多项式
使得多项式在x取值1,2,3的时候a,b,c数组的取值分别对应到前述三个门电路的向量。
取多项式P(x)=s . a(x) * s . b(x) - s . c(x),根据原来的定义,在x取值为1,2或3的时候,P(x)=0。根据多项式特性,P(a)=0等价于P可以被(x-a)整除,P(x)一定能被(x-1)(x-2)(x-3)整除,也就是说存在H(X),使P(x)=T(x)*H(x),其中T(x)=(x-1)(x-2)(x-3)。注意QAP这个过程把原来三个点的取值转化成为一个多项式,相当于中间插入了很多没有意义的值,这些值的取值与原公式是无关的。也就是说多项式的验证与原计算的验证本质并不等价,但验证了多项式也就验证了原计算。最终我们把原算式的证明转化成为多项式的证明,只要证明P(x)=T(x)*H(x),即可验证原算式。
对于ZK Rollup系统来说,整个证明和验证过程为
证明:
1.每一个具体交易问题都可以抽象成数学表达式,ZK Rollup将多个交易打包,生成一个数学表达式的形式表示,再将其拍平表达成一个算数电路,也就是生成电路表达。
2.有了这个电路表达之后,再构造一个约束系统,最常用的是R1CS(rank-1 constraint system,一阶约束系统)
3.有了这套约束系统之后,再把这个问题转化成多项式可满足的一个问题,叫QAP问题。
4.然后QAP 问题得到之后,再用ZK-SNARK算法体系去给出生成最终的证明。
验证:
1.智能合约根据构造的ZK-SNARK算法对于Proof正确性进行验证。
2.如果 Proof 正确且合约中保存的状态与 PREV_STATE 相等,那么新的状态 POST_STATE 将会被记录到合约中,替换原有状态。
总结
ZK Rollup 是一种新型的 Layer 2 扩容方案,该技术的核心思想是:
1.将主链作为存储媒介,而非共识引擎 ;
2.将交易压缩,并在链下达成状态共识 ;
3.用零知识证明保证链下状态共识的安全性。
目前,ZK Rollup 最典型的应用场景是去中心化的交易所。
参考资料:
【1】https://blog.csdn.net/PPIO_Official/article/details/102942680
【2】http://jizhid.com/number/blockchain/20401.html
【3】https://mp.weixin.qq.com/s/JvIKFsFfc_Uk8kHnmLxVtw
【4】https://mp.weixin.qq.com/s/RVlhPFJaIes0qx4AyzlzkQ
【5】https://mp.weixin.qq.com/s/XOv5LG4woGEYC55PMMm3cQ
【6】https://mp.weixin.qq.com/s/8nzDueT8-LVv-XDc87x30Q