1. 引言
前序博客有:
- 基于BitVM的乐观 BTC bridge
- BitVM:Bitcoin的链下合约
- Bitcoin Bridge:治愈还是诅咒?
- BitVM2:比特币上的无需许可验证
- 以比特币脚本来实现SNARK Verifier
- Clementine:Citrea的基于BitVM的信任最小化双向bridge
要点:
- 1)若有OP_CAT,Merkle path验证将更容易
- 2)sCrypt团队在Bitcoin SV 上的验证 BLS12-381 proof,尽管使用了OP_CAT和OP_MUL等,但交易大至26MB,通过单笔交易在比特币上验证ZK Proof是有难度的。
- 3)以比特币脚本来实现ZK Verifier时,应让proof验证算法以尽可能少的比特币脚本量来表示。
- 4)可将proof验证算法拆分为多个交易,每个交易完成proof验证的几个步骤。
- 5)基于Pairing的ZKP系统,认为其不太适于比特币的原因在于:
- 5.1)为保证椭圆曲线安全性,其素数域至少约为256位。素数域大且less structured。
- 5.2)Bitcoin SV上即使使用了OP_MUL opcodes,pairing运算仍需要大量脚本来实现。
- 6)ZKP系统的算术化表示,目前认为教科书式的Plonk门表示(2个输入,1个输出),对比特币上verifier实现更简单。
- 7)STARK证明系统基于的有限域更小,如BabyBear域和M31域,用比特币脚本表达更友好,目前认为Goldilocks域不够比特币友好。
- 8)STARK被认为是post-quantum的,而基于Pairing的ZKP系统不是,所以认为STARK可能是更合适的长期选择。
- 9)STARK证明系统要有足够的安全级别,需要一些参数。但StarkWare的grinding技术 和 STIR 技术,可使用更少的查询或对verifier更友好的编码率,来达到相同的安全级别。
- 10)不能为了让Bitcoin ZK Verifier简单而忽略了Prover的开销,可像Polygon zkEVM那样,组合多种证明系统来联合使用。
- 11)ZK技术将有可能是比特币生态的游戏规则改变者:
- 11.1)可借助ZK技术实现比特币交易自省
- 11.2)可借助ZK技术实现证明聚合层,将多个比特币生态项目的proof聚合为一个后,再提交到比特币上验证,从而缓解比特币网络堵塞。
- 12)BitVM挑战技术中,还要确保多个挑战者能够独立、并行地工作。这可以防止拖拉的挑战者阻止其他严重的挑战者,并且避免证明者在同时处理多个挑战者时需要提交额外的保证金。
- 13)当前存储库中的 BitVM 设计与许多人熟悉的白皮书中的版本非常不同。
近年来最雄心勃勃的尝试是由sCrypt团队在Bitcoin SV 上的验证 BLS12-381 proof,但这种尝试对比特币不起作用——Bitcoin SV (BSV) 是比特币的硬分叉,是一种完全独立的比特币。
如今的BSV有很多不同之处:
- BSV 支持比特币不支持的新操作码:如OP_CAT、OP_MUL等等。
- 且具有更高的脚本大小限制。
sCrypt 用于验证 BLS12-381 proof的交易见:
- https://test.whatsonchain.com/tx/eba34263bbede27fd1e08a84459066fba7eb10510a3bb1d92d735c067b8309dd?tab=details
其大量使用了这些新操作码,如 OP_NUM2BIN、OP_SPLIT、OP_CAT。这笔交易相当大——26MB——这在比特币中是不可能的,因为Bitcoin上最大可能的区块大小是4MB,而比特币的区块间隔约为10分钟——还需为其他交易的结算留出空间。
在这一雄心勃勃的尝试之前,比特币开发人员实际上是第一批研究零知识证明的人之一,甚至在以太坊出现之前。早在2011年,前比特币核心开发者、Blockstream前CTO Gregory Maxwell就提出了“Zero Knowledge Contingent Payment 零知识或有支付”。几年后,零知识证明变得足够实用,ZKCP 首次在比特币网络上实现(详情见2016年2月博客The first successful Zero-Knowledge Contingent Payment)。然而,这并没有在比特币网络上验证 ZK 证明,而是在链下验证该证明。这对于 ZKCP 应用程序来说已经足够了,但对于其他一些需要链上 ZK 证明验证的应用程序来说,它不起作用。
在Bitcoin StackExchange上关于比特币验证 ZK 证明的最新讨论:
- 来自QED Protocol的 Carter Feldman发现缺乏 OP_MUL 使得验证现代 ZK 证明变得困难。
- 活跃的比特币核心开发者、Blockstream 前联合创始人兼核心工程师 Pieter Wuille 的回答如下。
“在我看来,即使存在 OP_MUL,在比特币脚本中实现 SNARK Verifier也是完全不可行的。如果没有循环、椭圆曲线运算,甚至模幂运算,您将被迫将计算完全展开为单个乘法。我无法想象在不使交易超出大小限制的情况下,可以在比特币脚本中实际完成这一点。即使它在某种程度上是可行的,它的效率也会低得可笑,我无法想象你会想用它来做除了实验之外的任何事情。”
Pieter Wuille 还补充道:
“如果您的目标是在比特币脚本中进行实用的 SNARK 验证,也许您应该制定一项提案来实际添加 SNARK 验证操作码?”
这是一个很好的起点。要知道为什么在比特币网络上验证 ZK 证明会很困难,需要重新审视比特币脚本是什么——它与 Solidity、EVM 非常不同。
2. 比特币有一组有限的操作码
与以太坊的智能合约及其被Solidity、OpenZeppelin和Reth包围的生态系统不同,比特币中的一切都更加简单。比特币有自己的脚本语言和简单的stack machine。之所以不称其为“VM”,因为比特币stack machine:
- 缺乏简单的控制流原语(如循环和条件跳转)
- 且只是逐一执行操作码。
在比特币中验证 ZK 证明的主要挑战是:
- 比特币脚本支持一小部分操作码,这些操作码只能实现非常有限的计算形式。
这并非总是如此。在 2010 年 8 月之前,比特币支持更多的操作码,包括 OP_CAT 和 OP_SUBSTR,Bitcoin SV 后来又重新添加了这些操作码。然而,由于担心这些操作码中可能存在错误,中本聪禁用了这些操作码。
这些操作码尚未启用。因此,人们一直试图用现有的操作码来“模拟”消失的操作码,但很少有成功。
现在使用 OP_MUL 和 OP_CAT 作为示例来展示它变得多么不方便。
- 1)以前,可以使用 OP_MUL 将堆栈中的两个数字(最多 4 个字节)相乘,并将结果存储回堆栈。删除 OP_MUL 后,必须使用Double-and-add算法算法通过 OP_ADD 和 OP_GREATERTHANOREQUAL 来模拟数字乘法,这将需要数百条指令。
- 2)另一个被禁用并成为 Twitter 上热门话题的操作码是 OP_CAT。可能会看到一些比特币爱好者在其 Twitter 个人资料中包含 OP_CAT,以展示他们对恢复 OP_CAT 的支持。Blockchain Beach 2024年1月文章Satoshi Thought It Was Too Powerful. Why are the Taproot Wizards Reviving OP_CAT? 中的下图最好地总结了OP_CAT的历史。
但是,OP_CAT 是什么?网络memes似乎到处都放着一只猫(OP_CAT),但CAT的意思是“串联”。它从堆栈中获取两个字符串并将它们组合成一个字符串。这是一个非常简单的功能,而 OP_CAT 仍然被禁用的事实造成了很多不便。- 2.1)首先,还没有找到规避或模拟 OP_CAT 的方法。 Robin Linus 在https://github.com/coins/bitcoin-scripts 中收集了一些奇特的比特币脚本,其中讨论了所有比特币脚本技巧,但即使对于特殊情况,它也没有任何神奇的解决方案来模拟 OP_CAT。现有的操作码在这里无能为力。 OP_ADD 仅适用于短输入,无法处理稍长的字符串。结果,在Robin的Merkle Path Verification with OP_CAT脚本中,他不得不提出并承认“这个实现只需要OP_CAT”。 2021 年,来自 Blockstream 的 Andrew Poelstra 分享了他关于实现“covenants契约”但需要 OP_CAT 的想法(见博客CAT and Schnorr Tricks II),OP_CAT 可以在 Blockstream 的 Liquid Network 上找到。Andrew Poelstra也没有一种便捷的方法来绕过比特币网络上 OP_CAT 的要求。
- 2.2)其次,OP_CAT 对于一些关键和基本的密码原语来说恰好是不可或缺的,包括高效的 Merkle 树。为了使 Merkle 树高效,应该在比特币脚本中利用哈希操作码,如 OP_SHA256。现在的挑战出现在 Merkle 树的压缩步骤中,它通过对子节点的哈希值串联进行哈希计算来计算父节点的哈希值,如果没有 OP_CAT,这似乎完全不可行——所有算术操作码在这里都没有帮助,因为它们无法处理像 SHA256 哈希这样的较长输入,并且可以轻松确认其余操作码无法提供帮助。然而,有了 OP_CAT,这一切就变得微不足道了,具体见Robin的Merkle Path Verification with OP_CAT脚本。
碰巧大多数基于哈希函数的现代零知识证明都依赖于 Merkle 树。没有OP_CAT,是非常痛苦的。
3. OP_CAT的需求及其进展
和许多其他比特币开发者一样,一直在尝试寻找创造性的方法来使用现有的操作码来验证 ZK 证明,但没有找到好的解决方案。感觉重新启用 OP_CAT 可能是必要的。
对于重新启用 OP_CAT 有很多肯定的意见,包括来自 Blockstream 的意见,并且许多比特币分叉已经启用了它。人们越来越习惯 OP_CAT,原因有四个:
- 1)OP_CAT 的 C/C++ 代码相当简单,因为它基本上只是调用 C++ 标准库的向量实现,如比特币代码存储库历史代码记录中所示。
- 2)OP_CAT 与 OP_LSHIFT 崩溃并没有真正的关系,OP_LSHIFT 崩溃是这些操作码在 2010 年被禁用的原因。
- 3)通过限制 OP_CAT 的输出长度,可以有效防止攻击者使用 OP_CAT 创建非常长的字符串(每次将其长度加倍)的安全问题。早在 2010 年,上面显示的 C++ 代码就已经强制实施了 520 字节的限制。
- 4)重新启用 OP_CAT 只需要软分叉。
传统上,人们往往不相信比特币会启用 OP_CAT,因为在过去十年中,到目前为止,还没有恢复禁用的操作码。但实际上,重新启用 OP_CAT 的进展似乎非常积极。bitcoin-dev邮件列表是进行大量比特币核心开发讨论的地方。 Ethan Heilman 于 2023 年 10 月启动了一项提案。
这一提议得到了积极的回应。比特币主要开发者Andrew Poelstra表示:
“ ……当谈论如何使用比特币脚本做很酷的事情时,我会说大约 90% 的情况都是以“如果我们有 CAT,我们就能做到这一点”结尾。剩下的 10% 通常不需要更多。”
“没有任何一个操作码可以媲美 CAT 的功能(除了像 OP_ZKP_VERIFY 这样的超级通用操作码:))”
2023 年 12 月,创建了 BIP 草案(https://github.com/bitcoin/bips/pull/1525)。尽管尚未分配 BIP 编号,但一些比特币开发人员已对其进行了审查,并已完成了所请求修改的一些迭代。
将其实施到Bitcoin Core(参考比特币客户端实施)中的努力已经开始。目前该讨论正在Bitcoin Core PR Review Club比特币核心公关评论俱乐部中进行。过去几周,重新启用 OP_CAT 的拉取请求 ( https://github.com/bitcoin-inquisition/bitcoin/pull/39 ) 已得到积极更新和讨论。 2024 年 3 月 6 日,比特币核心公关评论俱乐部举办了一次会议讨论 OP_CAT。
如果拉取请求被合并,讨论将转移到 Bitcoin Core 主存储库(https://github.com/bitcoin/bitcoin/pull/29247),如下所示。 OP_CAT,如果通过了所有必要的流程,就会将此 PR 合并并发布到新版本的 Bitcoin Core 客户端中,并且软分叉将在比特币上启用此功能。
很难预测整个过程需要多长时间。这是 2010 年禁用的操作码第一次被重新启用,也是第二次将主要操作码添加到比特币中(上一次是 Tapscript 的 OP_CHECKSIGADD)。
然而,仍然相信比特币社区将很快恢复 OP_CAT。
- 首先,比特币目前的路线图没有其他重大变化,OP_CAT 一直是当前的焦点。
- 其次,OP_CAT已经讨论了很多年,已经在比特币网络的变体中实现,并且得到了很好的研究。这可以从比特币核心开发人员之间的对话中观察到,而每个人似乎都在同一立场上。
总而言之,看好 OP_CAT 将重返比特币网络,相信这将是比特币生态系统的重要时刻。
在本文的其余部分中,将分享如何在比特币网络中验证零知识证明(假设启用了 OP_CAT)的想法。
4. 定义比特币友好的证明系统
sCrypt 之前的尝试在Bitcoin SV 上验证了 BLS12-381 Groth16 proof,但仍然不适用于比特币网络——sCrypt 的实现已经假设了 OP_CAT 的存在。 sCrypt 的实现不起作用的主要原因是脚本太长,并且26MB的交易无法放入一个比特币区块中。比特币区块脚本大小必须小于4MB,否则矿工无法将其包含在任何区块中。
这使得可精确定义ZKP系统是“Bitcoin-friendly比特币友好”的含义。自2017年SegWit升级以来,比特币网络通过“weight units”来衡量区块的大小,一个区块的weight units上限为400万个。对比特币区块浏览器的观察表明,许多区块已经用完了 400 万weight units配额,每个区块的大小在 1MB 到 2MB 之间。
在 pay-to-taproot (P2TR) 交易中,一笔交易消耗的weight units数量是通过一组规则(具体见比特币Weight units规则说明)计算的,因此:
- 一笔交易中的某些字节将花费 4 个weight units,而其他一些字节只需 1 个weight unit。
- 给予某些字节较低的weight units成本(又名“折扣”)的基本原理是,这些字节用于确定交易是否有效,并且一旦交易被验证,这些字节就不需要由所有全节点永久存储,因此它们对比特币网络的存储开销的贡献较小。
幸运的是:
- (将成为 P2TR 交易中的见证脚本)的比特币脚本享有此折扣。
- P2TR 给比特币脚本带来的另一件事是,其解除了比特币脚本的大部分大小限制。只要P2TR可以放入区块中,就没有什么可以阻止矿工将其包含在内。然而,这并不意味着普通用户可以编写400万字节的比特币脚本。根据默认策略(见https://github.com/bitcoin/bitcoin/blob/master/src/policy/policy.h#L27),许多节点只会转发小于 400k weight units(区块限制的 1/10)的交易,如果一个人真诚地想要提交更大的交易,则需要与矿工合作,如Bitcoin StackExchange 上的一些帖子(Can I send almost 1MB transaction?、What is the maximum number of inputs/outputs a transaction can have?)指出。
P2TR 在使比特币中的 ZK 证明验证成为可能方面发挥着重要作用。@therationalroot总结了P2TR的好处。
还应注意的另一个限制是堆栈大小。比特币脚本当前将堆栈大小限制为 1000。程序不能创建超过 1000 个堆栈元素。 这大致意味着比特币脚本理论上可以访问的“工作内存”是 0.5MB。应该确保程序可以在内存限制内运行。然而,没有必要进一步优化它,因为交易费用不取决于峰值堆栈大小。
现在,拥有定义比特币友好性所需的所有信息。基本上,想要一个证明系统,其验证算法可以在比特币脚本中以尽可能少的weight units表示,并且可以在堆栈限制和其他规则内执行。这转化为证明系统的愿望,其验证算法需要在比特币脚本中以尽可能少的字节来表达。
若有两个具有相似weight units的证明系统,那么比特币友好性的第二个衡量标准是验证算法是否可以方便地拆分为多个交易,其中每个交易完成证明验证的几个步骤。 若单次证明验证所需的独立比特币脚本仍然超出限制,这会有所帮助。在这种情况下,可能需要某种covenants来强化这些交易之间的关系,这将受益于 OP_CAT,但它需要更多的研究。
在比特币脚本中简洁地表达计算(即不使用太多weight units)的秘诀是尽可能利用操作码。如,比特币脚本通过五个操作码(分别用十六进制表示 0xa6-0xaa)对哈希函数提供原生支持:
- OP_RIPEMD160
- OP_SHA1
- OP_SHA256
- OP_HASH160
- OP_HASH256
一次哈希运算,无论长度如何,只占用一个权重单位。如果必须使用其余操作码来模拟哈希运算,假设 OP_CAT 可用,则可能仍然需要至少 60k weight units(来将 SHA256 作为布尔电路运行),并且还有其他成本,如访问堆栈。当前堆栈可能不足以运行该程序。人们可以看到尽可能使用现有操作码的重要性。
特别关注“哈希函数”的原因是:
- 许多现代零知识证明协议恰好依赖于哈希函数。
5. 寻找比特币友好的证明系统
生产级现代ZKP可以大致分为两个系列:
- 基于椭圆曲线配对的证明(如BN254)
- 如包括Groth16、Plonk和Marlin
- 基于哈希函数的证明
- 如包括STARK及其许多变体和实现
第一个想法是,基于配对的证明系统不太可能适用于比特币,原因为:
- 对更大、结构化更少的素数的计算。基于配对的证明系统依赖于椭圆曲线。为了保证椭圆曲线的安全,其素数域模数必须至少约为 256 位。通常,通过显式公式构建配对友好曲线,如BN曲线。这种构造性方法的缺点是不能选择小素数,并且可能需要实现诸如Montgomery reduction之类的东西来处理这些非结构化的大素数,这增加了复杂性。
- 难以模拟。为了验证基于配对的证明,验证者需要对曲线上的点执行多次标量乘法以及所谓的“配对”步骤。这些可能非常重。回想一下,sCrypt示例远远超出了标准区块限制,尽管它利用了新的操作码,如Bitcoin SV 上可用的 OP_MUL。
因此,重点是基于哈希的证明系统。Robin Linus 最近发了一条推文,询问是否有人这样做过。
核心思想是:
- 通过 OP_CAT,与 Merkle 树相关的计算部分可以像上面提到的那样,使用采用非常小的weight unit的现有哈希操作码。
- 其余的计算相对来说比基于配对的证明系统容易得多,因为它可能涉及小素数。这种灵活性是性能的关键。
已发现 STARK 中使用的某些素数对比特币友好。对于比特币友好的素数,它需要满足一个要求:素数小于 2 31 2^{31} 231。这可以确保两个数字相加不会超过 2 32 2^{32} 232。
许多素数都满足这个要求。
- M31 ( 2 31 − 1 2^{31} - 1 231−1),已在 Circle STARK 中使用。
- BabyBear ( 15 ∗ 2 27 + 1 15 * 2^{27} + 1 15∗227+1),已在 RISC Zero 中使用。
对于比特币来说,M31 和 BabyBear 彼此并没有太大区别,因为它们的算术运算将以几乎相同的方式进行模拟。
好处是,这允许:
- 使用 OP_ADD 和 OP_SUB 来执行所有加法和减法。
- 可以通过“0 OP_LESSTHAN”来检测溢出,并且可以通过OP_ADD和其他操作码快速纠正溢出。
- 为了将两个数字相乘,由于 OP_MUL 仍然被禁用,需要通过double-and-add来实现它。 BitVM 已经实现了这项技术,详情见:Add u32_mul #12。
读者可能想知道另一个素数 Goldilocks64 ( 2 64 − 2 32 + 1 2^{64} - 2^{32} + 1 264−232+1) 是否对比特币友好。由于这个素数是 64 位的,因此目前无法使用现有的比特币操作码轻松模拟其计算,所以目前认为可以排除 Goldilocks64。
现在,剩下的一个问题是,鉴于 STARK 有如此多的变体,应该选择哪一种呢?这实际上是在问在证明系统中应该使用什么算术化。
- 一种候选解决方案是 R1CS,它已在Groth16中使用。Fractal论文(EUROCRYPT 19) 展示了如何在基于哈希的证明系统中进行 R1CS 并使其verifier尽可能高效。
- 另一种候选解决方案是教科书式 Plonk,其中每个门都有三根线,两根用于输入,一根用于输出,并且每个门仅执行两个输入的一个简单线性组合。目前认为,就最终Verifier成本而言,这可能比 R1CS 更简单。
需要做一些工作来试验以上提案,但基于目前对 ZKP 格局的理解,以上讨论应该足够全面。
可能对从业者有所帮助的另一句话是,为了在基于哈希的证明系统中实现足够的安全级别,需要使用一些参数。
- Starkware 使用一种称为“grinding磨削”的技术,该技术添加了工作量证明组件以提高安全性,通过这样做,人们可以进行更少的查询或使用对verifier更友好的编码率,同时仍然具有相同的级别的安全性。由于在比特币中验证grinding对于verifier来说很容易,因此特别喜欢这个想法。
- 最近的一项名为STIR 的工作与grinding非常相关,因为它提供了一种可以进一步降低验证成本的技术。
最后但并非最不重要的一点是,与基于配对的证明系统相比,STARK 的一个好处是:
- STARK 被认为是后量子安全的,而众所周知,基于配对的证明系统不是后量子的。换句话说,STARK可能是更合适的长期选择。
要弄清楚verifier的具体成本还有很多工作要做,相信人们实际上可能需要通过实际实现来学习。但L2 Iterative团队认为总体路线图是明确的,而且只是时间问题。请注意,目前仍然担心,最终这个对比特币更友好的证明系统可能不适合交易,在这种情况下需要做更多的优化。 本文将讨论其中的一些。
现在,人们可能想知道,专注于“verifier”的比特币友好性是否会让“prover”付出高昂的代价。这是上面提到的教科书式 Plonk 证明系统的一个潜在问题:
- 由于其简单的结构,相同的计算可能会在如何表示程序方面产生更多的冗余并且速度会变慢(想想具有更简单指令集的 CPU,人们可能需要使用更多的指令来实现相同的功能)。教科书式prover和高度优化的prover(如 Scroll、Polygon Miden、RISC Zero 中使用的 Halo2)之间的prover效率差距可能很大。
这可以通过 Mariana Raykova、Eran Tromer 和 Madars Virza 在 2019 年提出的一种名为“SHARP ”的设计理念来解决。这个想法是,人们可以通过使用两个(或多个)证明系统,在prover效率和verifier效率之间获得最佳权衡:
- 一些证明系统的prover效率良好,但verifier效率可能不是最优的(通常是由于算术复杂但因此具有表现力)。
- 一种最终证明系统,其prover效率可能平庸,但verifier效率良好。
将这些不同的证明系统组合在一起的方法是通过递归验证Scalable Zero Knowledge via Cycles of Elliptic Curves。为了证明某件事是正确的,证明系统可以验证其计算,或者对断言计算正确的现有证明进行验证。对于足够大的计算,后者会更有效,一般来说,验证proof比验证相应的计算渐近更容易。
这已在生产中使用。下图展示了Polygon zkEVM如何使用不同的证明系统来生成验证大量计算的单一证明。图中所示的几个递归prover并不相同。如,最后阶段使用切换到不同有限域的递归prover,以便使 SNARK 阶段的最终prover的proof 是verifier高效的。
因此,要做的是:
- 使用prover高效的证明系统运行实际的proof验证,
- 并使用递归验证和prover高效的证明系统将这些证明聚合成单个证明。
- 最后,使用verifier高效(更具体地说,比特币友好)的证明系统将其转换为比特币友好的证明。
这使得能够避免因比特币友好性而对prover效率造成的损失。
6. 使用 OP_CHECKSIG 进行交易自省
如果解决方案成功,人们可能能够在 ZK 证明的帮助下实现非常复杂的covenants,因为它支持通用交易自省。
比特币中的Covenants 契约指的是:
- script pubkey脚本公钥对于想要在该脚本公钥下花费 UTXO 的交易强制执行一些规则的能力。
- 这很大程度上依赖于 OP_CAT。这个想法是,脚本 pubkey 可以使用 OP_CHECKSIG 来验证当前交易的签名,其中包括outputs列表等信息,如果脚本不想允许这样的outputs,脚本 pubkey 可以决定是否使该交易无效。
Rijndael (@rot13maxi) 的推文中提供了一个简单的示例,展示了 OP_CAT 和 OP_CHECKSIG 的强大功能。
@rot13maxi 基本上创建了一个 UTXO,要求支出交易遵循给定的格式。这个例子很棒,因为它只依赖于现有的操作码和 OP_CAT,并且脚本足够短。局限性在于它可能无法支持更复杂的规则或更灵活的交易。
sCrypt一直在努力使用契约将状态从一笔交易传递到另一笔交易,从而使Bitcoin SV 上的脚本有状态。其思想与使用 OP_CAT 和 OP_CHECKSIG 以及Andrew Poelstra 讨论的CAT and Schnorr Tricks类似。
如果能够在比特币中实际验证 ZK 证明,并且这种验证变得足够高效,就可以进行通用交易自省,如下所示。
- 计算交易的哈希值。
- 通过 Schnorr trick,使用 OP_CHECKSIG 根据交易验证此哈希值。
- 将哈希作为输入生成零知识证明,断言:
- 令b为该签名下的交易主体。
- 验证签名是对消息b的有效签名
- 检查b是否满足某些规则
ZK带来的好处就在于:
- “满足一定的规则”。
因为当今的零知识证明非常通用,所以它基本上可以执行普通计算机可以执行的任何规则。如,RISC Zero是运行 RISC-V 指令的通用虚拟机。只要程序可以编译成 RISC-V,它就可以计算其执行的证明。如今,人们可以轻松地将 Rust 和 C++ 代码交叉编译为 RISC-V(看看游戏《Doom》是如何在 ZK 中验证的)。下面是 RISC Zero 的证明生成性能表。 M2 MacBook 可以证明 RISC-V CPU 以 100kHz 的速度执行。通过集群计算,这可以线性扩展。使得能够处理许多以前在比特币生态系统中没有考虑到的复杂程序。
通用ZK可以做得更多。如,通过交易自省,ZK 证明可以知道输入的 txid,并且由于 txid 是前一笔交易中信息的哈希值,这使得 ZK 可以读取前一笔交易。
- 令prev[0]为创建第一个输入的上一个交易。
- 验证prev[0]是否对应于新交易主体中出现的txid h[0] 。
- 使用prev[0]中的数据作为先前交易的可靠参考。
由于每笔交易可以有多个来自不同交易的输入,因此可以有prev[1]、prev[2]、prev[3] ……代表其他输入的交易。
然而,这还不止于此。现在已经可以访问上一笔交易了,就可以访问上一笔交易的上一笔交易了。这可以追溯到起源。使得能够做的是自省部分比特币历史数据,这些数据是当前交易的祖先。
这种设计可以实现许多可能的应用。游戏规则的改变者就是零知识证明,它提供了一些有用的功能。
- 通用计算。如果验证零知识证明变得可行,交易自省将不再受到比特币脚本力量的限制。现代生产中使用的零知识证明已经可以实际验证由Rust和C++等高级语言编译的程序,并且该程序具有程序计数器,可以具有动态控制流,并且具有大的可访问随机存取存储器。这基本上消除了使比特币脚本图灵完备或通用的必要性,因为这种计算可以委托给 ZK,这进一步减少了网络开销。
- 增量计算。如今,零知识证明可以以一种非常可组合的方式生成,因为实际上可以在另一个零知识证明中验证一个零知识证明。这意味着,如果考虑一个状态机,证明其从 A 到 Z 的状态转换可以通过验证已经验证从 A 到 Y 的状态转换的证明并继续从 Y 到 Z 的计算来完成。这里,验证来自A 到 Y 比从 A 到 Y 重新计算要快得多。比特币区块链的轻客户端可以被视为一个状态机,这意味着人们可以生成比特币历史的 ZK 证明并使用以下命令刷新该证明:通过增量更新生成新区块。
- 简洁验证。通过递归组合,零知识证明的一个关键特性是,无论证明中确实验证了多少计算,都可以使verifier开销保持不变。因此,即使一个证明可能会回顾多年的比特币历史,其proof验证成本实际上与另一个执行非常简单操作的proof相同。这也是零知识证明在以太坊生态系统中用于构建 ZK-Rollups 的原因。
通过使用ZK bridge,交易自省的能力也可以超越比特币网络。 ZK bridge的想法是,人们可以有效地为另一条链运行轻客户端,查询状态,并验证 ZK 中的所有执行。许多L1区块链都是基于PoW或PoS,因此,ZK bridge可以验证PoW和PoS来检测和拒绝错误的分叉,并且在选择正确的链后,它可以使用ZK以可验证的方式读取区块链,就像比特币的简化支付验证(SPV)一样。
然而,正如之前提到的:
- 只有当零知识证明足够有效,可以将其适应所有比特币脚本限制(堆栈大小、weight units)时,这才有可能。
- 而这只有在比特币网络上 ZK 证明验证的成本合理的情况下才有可能。
无论是否实用,都无法得出结论性的答案。 Robin Linus 认为 400 万个weight units足以进行大量计算。但是,L2 Iterative团队认为需要首先投资于实际的工程工作。如果成本太高,可能需要进行更多的优化。
7. 将证明验证拆分到多个交易中
假设ZK证明验证无法在单个交易中完成,需要找到创造性的方法将一堆交易链接在一起,让它们各自执行部分ZK证明验证。
sCrypt 的几篇文章(Turing Machines on Bitcoin、OP_PUSH_TX: Bitcoin Covenant without Breaking Changes、Stateful Smart Contracts on Bitcoin SV)虽然依赖于Bitcoin SV,但提供了如何做到这一点的总体思路。使用 OP_CHECKSIG,当然还有 OP_CAT,其input的公钥是脚本可以强制花费该input时其他输入和输出的方式。
想法如下。将自己视为prover。prover的目标是让verifier相信已经完成的计算。
-
首先,证明者可以将计算分成N个脚本。每个脚本执行部分验证。
-
然后,prover创建其第一个交易,如下所示。
[input] => out[1], out[2], …, out[N], out[N+1]
这里,out[1] 到 out[N] 将是执行计算的 pay-to-taproot (P2TR)。 out[N+1] 是记录当前prover公钥的output。因此,它可以是一个简单的脚本,如下所示:
OP_RETURN {prover public key}
这笔交易不会很昂贵,因为在这笔交易中,无论out[1]到out[N]中的脚本有多复杂,这笔交易包含的只是脚本的哈希值。因此,本次交易规模较小。
-
现在,对于每个 out[1] 到 out[N],它们将根据需要执行计算,并使用 OP_CHECKSIG 和 OP_CAT 强制执行以下条件。
- 一笔交易只有一个输入,也只有一个输出。
- 输入的 txid 会根据第一笔交易重新计算:[input] => out[1], out[2], …, out[N], out[N+1]。
- 需要来自prover关于此交易的公钥的签名(如 out[N+1] 中给出)并由 OP_CHECKSIG 进行检查。这是为了防止比特币网络上的另一个人从 out[1] 支出到 out[N]。
- output将是一个非常简单的输出,只有prover公钥下的有效签名才可使用(这将使用 OP_CHECKSIG 来避免延展性),并且该脚本包含两个附加数据元素:一个表示应该是的共享数据的哈希值所有 out[1]、out[2]、…、out[N] 和未使用的随机堆栈元素都是相同的,这是为了促进Andrew Poelstra 讨论的Schnorr tricks,因为需要对原像进行哈希处理OP_CHECKSIG 验证以 0x01 结尾。
-
因此,将创建N 个交易,每个交易解析其中一个 out[i] 并生成一个输出,可以将其称为 cert[i]。请注意,cert[i] 包含共享数据的哈希值,并且该哈希值在所有这些交易中应该相同,即使它们可能位于不同的块中。这N 笔交易如下所示:
out[1] => cert[1] out[2] => cert[2] … out[N] => cert[N]
-
现在,假设prover需要说服verifier(这是一个 pay-to-taproot UTXO,证明者无法控制它),prover通过创建以下内容向verifier呈现所有 cert[i]交易。
verifier-in, cert[1], cert[2], …, cert[N] => verifier-out
将进行一些小的调整以使 Schnorr trick能够帮助自省。
整体交易流程如下所示。
verifier-in 将执行以下操作:
- 通过OP_CHECKSIG和OP_CAT,获取cert[1]到cert[N]的txid,得到两个输出。
- 检查verifier-out是否符合预期。
- 使用 cert[1], cert[2], …, cert[N] 的 txid,恢复交易 out[1] => cert[1], out[2] => cert[2], …, out[ N] => cert[N],从而获得out[1],out[2],…,out[N]的txid。
- 检查 out[1]、out[2]、…、out[N] 是否具有相同的 txid。
- 使用out[1]的txid到out[N],恢复交易[input] => out[1],out[2],…,out[N],out[N+1]。
- 检查 out[1]、out[2]、…、out[N] 中是否使用了与接受的 ZK 证明验证程序相对应的正确的 pay-to-taproot 哈希值。
- 检查 cert[1]、cert[2]、…、cert[N] 是否包含正确的共享数据。请注意,共享数据是必要的,因为如果verifier-in不检查共享数据,那么prover可以提供任何证明,并且被证明的事物可能与verifier正在寻找的内容无关。
使用 Taproot 很重要,因为 OP_CHECKSIG 在 Taproot 和非 Taproot 交易中的行为非常不同 - 如,它现在使用 Schnorr 签名,这对于启用交易自省的 Schnorr trick以及交易哈希的方式至关重要计算有所不同,因为它不再包含脚本代码。
需要声明的是,目前还没有真正实现上述想法,需要完整的实现来看看所有的脚本是否都能在其限制内完成(当然,这需要完整实现 STARK prover)。特别是,想看看是否可以确保这里的所有交易都是“标准”的(遵循Bitcoin Core的policy.h设置的软限制),这样就不需要与矿工协调来提交交易。与矿工协调也不是不可能。 Marathon Digital 的Slipstream就是这样的一项服务:
- “Slipstream 是一项直接向 Marathon 提交大额或非标准比特币交易的服务……如果您的交易符合 Marathon 的最低费用门槛并符合比特币的共识规则,Marathon 会将其添加到其内存池中用于开采。”
另一个考虑因素是希望减少证明结算的等待时间。上面的设计确实试图促进这一点,允许所有证明生成交易同时提交到 P2P 网络。
读者可能会想,一次性提交所有交易是否可以,因为有些交易依赖于其他交易,如果前面的交易尚未完成,这样的交易就会失败。读者可能想知道矿工是否确实可以将这些交易放在同一个区块中,或者比特币网络中的交易只能使用前一个区块中的前一笔交易。
然而,这对于比特币网络来说并不是一个主要问题,因为比特币允许这样的灵活性。事实上,比特币有一种名为“子为父付款”(CPFP)的技术,该技术已经利用了具有依赖性的交易可以一起提交到内存池的事实。一笔交易可以使用前一笔交易的输出,只要它满足共识规则:
比特币共识规则要求,创建输出的交易必须早于花费该输出的交易出现在区块链中,包括让父交易早于子交易(如果两者都包含在同一个区块中)出现在同一个区块中。
因此,人们可以一次性提交所有这些交易,并等待 P2P 网络解决它们。请注意,交易的设计目的是使网络更容易解决。
- 第一个交易,[input] => out[1], out[2], …, out[N], out[N+1],仅取决于证明系统以及prover公钥,因此prover确实可以提前进行这笔交易。如果没有,prover也可以将该交易与其他交易一起提交。
- 接下来的N个交易,out[i] => cert[i],仅依赖于第一个交易,并且不会相互干扰。比特币网络中的矿工可以以任意顺序接受这些交易,通过合理的费用,鼓励矿工尽快纳入这些交易。
- 最后一笔交易 verifier-in, cert[1], cert[2], …, cert[N] => verifier-out 可以与其他交易一起提交到 P2P 网络,并且可以预期执行当所有N笔交易都被确认时。
然而,总是可以选择与矿工合作来让交易被接受,想象最终可能会有一个区块空间预留系统,以获得更可预测的延迟和结果,并且它可能会帮助免于进行更多的黑客攻击。
8. 分割验证的欺诈证明
另一个想法是借用optimistic rollup的想法,使其成为欺诈证明,类似于BitVM的做法,这可以从上一节介绍的分割验证实现开始。欺诈证明避免了实际进行计算的需要,并且它依赖于第三方(公众中的挑战者)来使比特币网络上欺诈证明的任何主张无效。
一个重要的微妙之处是欺诈证明引入了另一个要求——数据发布。为了让verifier相信某个声明是真实的,因为公众没有提供任何欺诈证据,verifier需要确信已经提供了公众质疑该声明所需的任何数据——特别是在比特币网络上发布的数据。
L2 Iterative团队提出了高层次的想法,并承认需要详细的实施来验证这个想法是否可行。prover在第一步中创建交易,如下所示。
[input] => out[1], out[2], …, out[N], out[N+1], da[1], da[2], …, da[N]
其中 out[1] 到 out[N] 中的每一个都是pay-to-taproot的,有2种花费方式:
- 对于 out[i],提供 da[i] 中描述的witness和 ZK proof的公共输入,并表明该witness不起作用,或者 da[i] 中的数据与中指定的哈希值不一致出[N+1]。如果满足这一点,它将使用 OP_CHECKSIG 和 OP_CAT 以及 Schnorr trick来确保输出是对挑战者的简单支付。它使用相同的交易自省技术来读取父交易中的out[N+1]。这将花费输出,从而防止再次花费相同的输出。
- 或者,需要prover公钥下的签名(可以在 out[i] 的 Tapleaf 中进行编码)。这个 Tapleaf 是happy path,并且它不会对花费此输出的交易提出额外的要求。
特殊输出 out[N+1] 将编码以下信息:
OP_RETURN {public input} {hash of da[1]} {hash of da[2]} … {hash of da[N]}
da[1] 到 da[N] 中的每一个都是pay-to-taproot的,只能通过向 out[N+1] 中给出的哈希提供原像来花费。
- 它使用交易自省技术返回到父交易并读取 out[N+1] 中的哈希值。
- 它检查初始堆栈(与 OP_CAT 正确连接后)是否包含可被哈希到 out[N+1] 中概述的哈希中的数据。请注意,由于堆栈大小限制,需要自定义此哈希的计算,以适应 OP_SHA256 最多只能哈希 520 字节数据的事实,如将数据拆分为多个块,单独对其进行哈希,然后进行哈希以 Merkle 树风格迭代生成哈希值。
- 它产生一个非常简单的输出,只有prover公钥下的有效签名才可以使用——这将使用 OP_CHECKSIG 来避免可延展性。将此输出称为 timelock[i]。然而,该输出将具有时间锁定,如只有在交易被确认足够长的时间后才能使用。时间锁将允许一定的随机性以促进 Schnorr 技术。
- 因此,da[i] 的支出会在比特币区块链上发布必要的数据。
因此,人们可以执行以下形式的交易:
da[i] => timelock[i]
现在,prover被要求将以下交易提交到比特币区块链:
[input] => out[1], out[2], …, out[N], out[N+1], da[1], da[2], …, da[N]
da[1] => timelock[1]
da[2] => timelock[2]
......
da[N] => timelock[N]
可能有一种替代设计,试图通过将 da[i] 推迟给verifier来避免交易自省的需要,这可能有助于降低成本,因为 da[1], …, da[N] 都具有相同的txid。这里将其留待将来的研究。
现在,如果出现任何问题,挑战者可以提交欺诈证明。如,如果挑战者从比特币网络获取数据后认为 out[i] 不匹配,那么根据 out[i] 的定义,挑战者可以使用第一个 Tapleaf 来花费 out[i],这将防止证明者稍后使用第二个 Tapleaf 花费 out[i]。如,如果挑战者发现out[j]是可挑战的,则可以提交如下交易:
out[j] => $
这里$代表挑战者可以扣押的押金作为挑战奖励。总而言之,挑战流程如下:
如果已经过去了足够的时间,但没有人可以挑战任何输出 out[1] 到 out[N],prover现在可以将proof提交给verifier。
verifier-in, out[1], …, out[N] timelock[1], timelock[2], …, timelock[N],
=> verifier-out
该流程的happy path如下所示:
verifier-in 将执行类似的检查:
- 通过OP_CHECKSIG和OP_CAT,获取timelock[1]到timelock[N]的txid,得到两个输出。
- 检查verifier-out是否符合预期。
- 使用 timelock[1], timelock[2], …, timelock[N] 的 txid,恢复交易 da[1] => timelock[1], da[2] => timelock[2], …, da[ N] => timelock[N],从而获得da[1], da[2], …, da[N]的txid。
- 使用out[1]的txid到out[N],恢复交易[input] => out[1], out[2], …, out[N], out[N+1], da[1] ,da[2],…,da[N]。
- 检查 out[1]、out[2]、…、out[N]、da[1]、da[2]、…、da[N] 是否具有相同的 txid。
- 检查 out[1]、out[2]、…、out[N] 中是否使用了与已接受的 ZK proof验证过程相对应的正确的 pay-to-taproot 哈希值,以及 da[1] 中是否使用了已接受的数据发布过程,da[2],…,da[N]。
- 检查out[N+1]中的公共输入数据是否与verifier相关。
- 检查 da[1], …, da[N] 是否具有正确的相对时间锁定,这意味着 out[1], …, out[N] 可用,并且 da[1], …, da[ N]已发表。附加规则可能会控制相对时间锁定需要多长时间,具体取决于用例并考虑到比特币潜在的网络拥塞。
9. 使用 BitVM 进行欺诈证明
另一种解决方案可能更通用并且可以用更高级的语言进行编程,那就是使用BitVM。
根据L2 Iterative团队与 Robin Linus 的对话,想强调的是:
- 当前存储库中的 BitVM 设计与许多人熟悉的白皮书中的版本非常不同。
鼓励读者从 GitHub 存储库中查找最新的 BitVM 设计。https://raw.githubusercontent.com/BitVM/BitVM/24bf29304d0a951106d907e7bcd43f4a37a8feb7/docs/bitVM_graph_v2.svg图 总结了 BitVM 的新设计,这应该是不言自明的。
如果访问 BitVM 存储库,会发现今天的 BitVM 类似于尝试模拟 riscv32i 标准的RISC-V VM。 BitVM 支持 20 条指令: ADD、SUB、MUL、AND、OR、XOR、ADDI、SUBI、ANDI、ORI、XORI、JMP、BEQ、BNE、RSHIFT1、SLTU、SLT、SYSCALL、LOAD 和 STORE。
该指令集确实足以运行图灵完备机。
- 这包括逻辑和流程控制,如 JMP、BEQ、BNE。具体来说,请记住比特币脚本运行的是stack machine。人们可以使用它来进行函数调用。
- u32 上的基本算术运算已使用比特币脚本进行了模拟。请注意,使用 SLTU,可以模拟 u64 整数运算,如 RISC Zero 的 VM 中一样。
- 可以使用可寻址内存。
每个 RISC-V CPU 周期都可以在比特币脚本中重新运行/挑战。
挑战者可以通过证明在执行的特定 RISC-V CPU 周期中发生以下六个事件之一(即“欺诈”)来挑战 BitVM 中的执行:
- faulty current program counter
- faulty next program counter
- faulty instruction
- faulty read of operand 1
- faulty read of operand 2
- faulty write
如果挑战者无法挑战任何循环,那么根据欺诈证明的定义,执行必须是正确的。挑战者可以通过在给定程序和一些输入数据的情况下在本地运行相同的 BitVM 来搜索差异,然后进行挑战-响应游戏,指出比特币网络上的差异。
因此,提供了一个非常简单的策略,如下所示:
- 将验证器编译为 BitVM 的 RISC-V 方言,这可以复用定制 RISC-V Valida LLVM 编译器的现有成果。
- 创建 BitVM 执行声明作为taproot并将其发布到链上。
- taproot现在已准备好接受挑战。
请注意,对于 BitVM,比特币友好性的定义现在完全不同了。之前,希望确保verifier足够小并且足够高效,以便在比特币脚本中进行验证,特别是它需要符合所有堆栈限制和weight units限制。
现在这些对于 BitVM 来说都不重要了。事实上,由于trace的证明生成可以相当高效且并行化,因此证明生成成本不太值得关注。然而,提交挑战的成本几乎是恒定的。 BitVM 不必使用 OP_CAT,但如果没有 OP_CAT,开销将非常高,因为与哈希函数相关的开销将显著增加。
然而,BitVM 并非没有开销。与乐观rollup一样,证明需要一个退出期以允许挑战者加入。请注意,完全链上的挑战响应可能需要prover(在 BitVM 中称为 Paul)和挑战者(称为 Vicky)之间进行数十次往返,并且由于比特币的出块时间为 10 分钟,因此这可能是相当长的时间。也有点不确定如果很多挑战者同时挑战会发生什么,是否会影响延迟和最终结果。
然而,以太坊中的许多乐观rollup都有 7 天的退出期,因此仍然有足够的时间来发生挑战-响应,但对于退出期必须很短的用例来说,它不起作用。还需要确保多个挑战者能够独立、并行地工作。这可以防止拖拉的挑战者阻止其他严重的挑战者,并且避免证明者在同时处理多个挑战者时需要提交额外的保证金。
最近一个降低提现期影响的解决方案是使用EigenLayer和Babylon等重质押协议来招募validators来预先验证证明并公布结果。AltLayer一直致力于提供捆绑解决方案。这在实践中是有效的,但它确实带来了额外的假设。
L2 Iterative团队正在密切关注 BitVM 的发展。作为通用 RISC-V VM,BitVM 可能会促进比特币网络可编程性的发展。 BitVM 仍在开发中,但其总体想法是有意义的——它的非交互式且密切的对应物RISC Zero或Valida已在实践中部署。
10. 证明聚合层
人们(尤其是比特币核心用户)担心的是,越来越多的比特币应用程序将超额订阅比特币资源,尤其是在 Taproot 升级显著放宽了脚本大小限制之后。
比特币社区一直在努力限制对比特币网络资源的不当使用。一个名为““WTF happened in February 2023?” 2023 年 2 月发生了什么?”的网站代表一个社区提案,要求节点自愿过滤掉一些交易,特别是那些由于铭文和 BRC-20 造成的垃圾邮件。
虽然在比特币上验证 ZK 证明不太可能被归类为垃圾邮件,因为它确实有用,但如果有十几个应用程序提交 ZK 证明进行验证,并且这种情况经常发生,则可能会导致网络拥塞。据报道Bitcoin network congestion eases as mempool clears in February,去年比特币内存池在巅峰时期包含 194,374 笔交易。这是不可取的,因为当交易位于内存池中时,这意味着该交易尚未能够结算到比特币区块链中。交易要么必须支付更高的费用,要么就继续等待。
因此,当在比特币上设计 ZK 证明验证时,试图控制它可能给比特币网络带来的开销是一个重要的设计考虑因素。
提出了添加证明聚合层的想法。通过这个聚合层,不同的应用程序可以将它们的证明“组合”在一起,并向比特币网络提交单个证明。这有很多好处。
- 它降低了比特币网络验证 ZK 证明的总体成本,该成本可以小到只有一个应用程序这样做。
- 它降低了不同应用程序使用比特币网络的成本,并通过在应用程序之间分摊成本,使它们能够更好地在成本和发布频率之间进行权衡。
- 如果使用欺诈证明,它会对挑战者有所帮助,因为所有挑战者只需要对所有应用程序挑战一项欺诈证明。
- 在以太坊生态中,已经有一些项目致力于证明聚合——在以太坊上,解决 ZK 证明的成本也很高。这包括Nebra和Aligned Layer。
当 ZK 准备就绪时,它可能会通过提供可验证的去中心化存储层来帮助缓解比特币的网络压力,就像Celestia和Nubit想要做的那样。 Nubit 使用以下架构,利用比特币进行质押,使存储层去中心化,而不会将数据转储到比特币网络上。 ZK 证明在这里很有用,因为它们可以强制存储节点诚实地存储数据,并可以强制执行质押和削减机制,这对于使存储层去中心化和可靠至关重要。
这可以用于欺诈证明和乐观rollup的数据可用性,其中数据可用性只需要是临时的而不是永久的,并且不应该使用比特币进行存储。这也可以帮助BRC20和其他铭文协议,使它们不需要将数据发布到比特币网络上,这解决了比特币社区的一个主要问题。
拥有一个由 ZK 的比特币共识保护的数据可用性层,可以降低创建 BRC20 的成本,使 BRC-20 更加去中心化,并避免比特币不必要的开销。
11. 工具和资源
比特币的未来依赖于开发人员在其基础上进行构建,相信还有很多事情要做,有很多主题需要探索,还有很多应用程序需要构建。
对在比特币上构建可编程性感兴趣的人可以参考 BitVM 代码库。
- https://github.com/BitVM/BitVM
BitVM 团队还在开发 Rust 编程环境和工具包,以便在 Rust 中使用比特币脚本。
Blockstream 等人已经讨论了使用 OP_CAT 实现比特币复杂功能的技术。
- 2021年1月博客 CAT and Schnorr Tricks I
- 2023年10月博客 Covenants: Examining ScriptPubkeys in Bitcoin Script
sCrypt 虽然依赖于比特币 SV 中的功能而不是比特币,但可以提供有关如何使用 OP_CAT 和其他潜在操作码来实现非常复杂的功能的见解。文章来源:https://www.toymoban.com/news/detail-845751.html
- https://scrypt.io/
参考资料
[1] L2 Iterative团队2024年3月26日hackmd How to verify ZK proofs on Bitcoin?文章来源地址https://www.toymoban.com/news/detail-845751.html
到了这里,关于如何在比特币上验证ZK Proofs的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!