撰文:Nickqiao、Faust、Shew Wang,极客 web3

顾问:Bitlayer 研究团队

摘要:近期 Delphi Digital 发布了题为《The Dawn of Bitcoin Programmability: Paving the Way for Rollups 》的比特币二层相关技术研报,系统的梳理了和比特币 Rollup 有关的核心概念,如 BitVM 全家桶、OP_CAT 和 Covenant 限制条款、比特币生态 DA 层、桥以及 Bitlayer、Citrea、Yona、Bob 等四大采用 BitVM 的比特币二层。

该研报虽然大体展示了比特币二层技术的大致图景,但整体比较泛泛而缺乏细节描述,让人似懂非懂。极客 web3 在 Delphi 研报基础上进行了展开式的深入挖掘,尝试让更多人系统的理解 BitVM 等技术。

我们将与 Bitlayer 研究团队及 BitVM 中文社区共同开展一个名为「走近 BTC」的系列专栏,长期围绕 BitVM、OP_CAT 和比特币跨链桥等重点话题进行科普,致力于为更多人祛魅比特币二层相关技术,帮更多爱好者铺平道路。

几个月前,ZeroSync 负责人 Robin Linus 发布了名为《BitVM: Compute Anything on Bitcoin》的文章,正式提出了 BitVM 的概念,推动了比特币二层技术的进展。可以说这是比特币生态最具革命性的创新之一,引爆了整个比特币二层生态,吸引了如 Bitlayer、Citrea、BOB 等明星项目的参与,为整个市场带来了生机。

之后,更多研究人员参与改进了 BitVM,先后推出了 BitVM1、BitVM2、BitVMX、BitSNARK 等不同的迭代版本。其大致情况如下所示:

  1. Robin Linus 于去年最先提出的 BitVM 实现白皮书,就是基于虚构逻辑门电路的 BitVM 实现方案,被称为 BitVM0;

  2. Robin Linus 在后面几次演讲和采访中,又非正式的介绍了基于虚构 CPU 的 BitVM 方案(称为 BitVM1),类似于 Optimism 的欺诈证明系统 Cannon,可以用比特币脚本在链下模拟出一个通用 CPU 的效果。

  3. Robin Linus 还提出了 BitVM2,一个 Permissionless 的单步非交互式欺诈证明协议。

  4. Rootstock Labs 和 Fairgate Labs 的成员发布了 BitVMX 白皮书,与 BitVM1 类似,他们希望通过比特币脚本模拟出通用 CPU 的效果(在链下)。

目前 BitVM 相关开发者生态的建设日渐明朗,周边工具的迭代完善也已肉眼可见,相比于去年,如今的 BitVM 生态已经从最初的「空中楼阁」变得「依稀可见」,这也吸引了越来越多的开发者和 VC 争相涌入比特币生态。

但对于大多数人而言,要理解 BitVM 和比特币二层相关的技术名词绝非易事,因为你要先对其周边的基础知识有系统性的理解,尤其是比特币脚本和 Taproot 等背景知识。目前网上已有的参考资料要么篇幅太长废话连篇,要么解释的不够透彻让人似懂非懂。我们致力于解决上述问题,力求以尽可能清晰的语言,帮助更多人理解比特币二层的周边知识,对 BitVM 体系建立起系统性认知。

MATT 和承诺:BitVM 的基础思想

首先我们要强调,BitVM 的基础思想是 MATT,含义是 Merkleize All The Things,主要指通过 Merkle Tree 这种树状的数据存储结构来展示复杂的程序执行过程,设法让比特币 Native 的验证欺诈证明。

MATT 虽然可以表达出一段复杂程序及其数据处理痕迹,但不会直接在 BTC 链上发布这些数据,因为这些数据的总体规模非常庞大。采用 MATT 的方案只在链下的 Merkle 树中存储数据,只把 Merkle 树最顶部的摘要(Merkle Root)发布到链上。这棵 Merkle 树主要包含三大核心内容:

  • 智能合约脚本代码

  • 合约所需的数据

  • 合约执行中留下的痕迹(智能合约在 EVM 等虚拟机中执行时对内存、CPU 寄存器产生的变更记录)

(一个简单的 Merkle Tree 默克尔树示意图 其 Merkle Root 是由图中底部的 8 个数据片段经过多层 hash 计算得到的)

MATT 方案下,只有尺寸极小的 Merkle Root 存储在链上,Merkle Tree 包含的完整数据集存储在链下,这用到了一种被称为「承诺」的思路。这里解释下什么是「承诺」(Commitment)。

承诺类似于一种简洁化的声明,我们可以把它理解为一大批数据压缩后得到的「指纹」。一般而言,在链上发布「承诺」的人会声称,某些存放在链下的数据是准确无误的,这些链下数据要对应一个简洁化的声明,这个声明就是「承诺」。

在某些时候,数据的 hash 可以作为对数据本身的「承诺」,其他的承诺方案还有 KZG 承诺或 Merkle Tree 等。在 Layer2 惯用的欺诈证明协议中,数据发布者会在链下发布完整数据集,在链上发布数据集的承诺。如果有人发现链下的数据集中存在无效数据,就会针对链上的数据承诺进行挑战。

通过承诺(Commitment),二层能够把大量数据压缩处理,只在比特币链上发布其「承诺」。当然,还要保证发布在链下的完整数据集可以被外界观测到。

目前几大 BitVM 方案如 BitVM0、BitVM1、BitVM2 和 BitVMX,基本都采用了类似的抽象结构:

1.程序分解和承诺:首先将复杂的程序分解为大量的、较基础的操作码(编译),然后把这些操作码在具体执行时产生的痕迹记录下来(说白了就是一段程序跑在 CPU 和内存中时,整个的状态变化记录,称为 Trace)。之后,我们对包括 Trace 和操作码在内的所有数据进行整理,组织成一个数据集,然后生成该数据集的承诺。

具体的承诺方案可以有多种形式,如:Merkle 树、PIOPs(各种 ZK 算法)、哈希函数

2.资产质押和预签名:数据发布者和验证者需要通过预签名的形式,把一定金额的资产锁定在链上,并且会有限制条件。这些条件会针对未来可能发生的情况而针对性的触发,如果数据发布者作恶,验证者可以提交证明把数据发布者的资产拿走

3.数据和承诺发布:数据发布者在链上发布承诺,链下发布完整的数据集,验证者检索数据集并检查是否有任何错误。链下数据集中的每个部分都与链上的承诺有关联性。

4.挑战和惩罚:一旦验证者发现数据发布者提供的数据有错误,它会把这部分数据拿到链上去直接验证(要先把这部分数据切的特别细),这就是欺诈证明的逻辑。如果验证结果显示,数据发布者的确在链下提供了无效数据,它的资产就会被挑战他的验证者拿走。

总结下就是,数据发布者 Alice 在链下公开二层交易执行过程中产生的所有痕迹,把对应的承诺发布到链上。如果你要证明某部分数据有误,先向比特币节点证明这部分数据和链上的承诺相关联,也就是证明这些数据是 Alice 本人对外公开的,然后让比特币节点确定这部分数据有错误。

现在我们大致理解了 BitVM 的整体思路,所有的 BitVM 变体基本都脱离不了上述范式。那么接下来,让我们开始学习和理解上述流程中用到的一些重要技术,先从最基础的比特币脚本和 Taproot 以及预签名开始。

什么是 Bitcoin Script 脚本

比特币相关的知识要比以太坊的更难理解,就连最基础的转账行为都涉及到一系列概念,包括 UTXO(未花费的交易输出)、锁定脚本(也称为 ScriptPubKey)和解锁脚本(也称为 ScriptSig)。我们先对这几个主要概念进行讲解。

(一段比特币脚本代码的示例 由比高级语言更底层的操作码组成 )

以太坊的资产表达方式,更像支付宝或者微信,每次转账只是对不同账户的余额做加减法,这种方法是以账户为核心,资产余额只是账户名下的一个数字;比特币的资产表达形式更像黄金,每块黄金(UTXO)都会标记出主人,转账实际上是把旧的 UTXO 销毁,把新的 UTXO 产生(主人会变更)。

比特币 UTXO 包含两个关键字段:

  • 数额,以「聪(satoshi)」为单位(一亿聪为一 BTC);

  • 锁定脚本,也称「脚本公钥(ScriptPubKey)」,会定义 UTXO 的解锁条件。

需要注意的是,比特币 UTXO 的所有权是通过锁定脚本来表达的,如果你要把自己的 UTXO 转让给 Sam,可以发起交易销毁自己的某个 UTXO,把新生成的 UTXO 的解锁条件写为「只有 Sam 可解锁」。

之后,Sam 如果要使用这些比特币,需要提交一个解锁脚本(ScriptSig),在这个解锁脚本中 Sam 要出示自己的数字签名,证明自己是 Sam 本人。如果解锁脚本和前述锁定脚本相匹配,Sam 就可以解锁并把这些比特币再转给别人。

(解锁脚本要和锁定脚本相匹配才行)

从表现形式的角度看,比特币链上的每笔交易都对应着多个 Input 和 Output,每个 Input 中要声明自己想解锁的某个 UTXO,并提交解锁脚本,解锁并销毁该 UTXO;Output 中会展示新生成的 UTXO 信息,对外公示锁定脚本的内容。

比如,在一笔交易的 Input 中,你证明自己是 Sam,把别人给你的多个 UTXO 解锁,统一销毁,再生成多个新的 UTXO 并声明让 xxx 在未来去解锁。

具体而言,在交易的 Input 数据中,你要声明自己要解锁哪些 UTXO,并指出这些 UTXO 数据的「存储位置」。这里要注意,比特币和以太坊截然不同,以太坊提供了合约账户和 EOA 账户两种账户来存储数据, 资产余额作为数字,记录在合约账户或 EOA 账户名下,统一放置在名为「世界状态」的数据库中,转账时直接从「世界状态」中对特定账户进行修改,便于定位到数据的存储位置;

比特币没有世界状态的设计,资产数据分散存储在过往的区块中(就是未解锁的 UTXO 数据,在每笔交易的 OutPut 中单独存放)。

如果你想解锁某个 UTXO,要说明该 UTXO 信息存在于过去哪笔交易的 Output 中,出示这笔交易的 ID(就是其 hash),让比特币节点去历史记录中寻找。如果要查询某个地址的比特币余额,需要从头遍历所有区块,找出和 xx 地址关联的未解锁 UTXO。

平时用比特币钱包时,可以快速检查某地址拥有的比特币余额,很多时候是因为钱包服务自身通过扫描区块,对所有地址建立了索引,方便我们快速查询。

(当你生成一笔交易声明把自己的 UTXO 送给别人时,要根据这些 UTXO 所属的交易 hash/ID 来标记出该 UTXO 在比特币历史记录中的位置)

有意思的是,比特币交易的结果是在链下计算完成的,用户在本地设备上生成交易时,就要直接把 Input 和 Output 全部创建好,相当于把交易的输出结果计算完了。交易在广播到比特币网络中,被节点验证后才上链。这种「链下计算—链上验证」的模式与以太坊是完全不同的,在以太坊上,你只需要提供交易输入参数,交易结果由以太坊节点计算并输出。

此外,UTXO 的锁定脚本(Locking Script)是可以自定义的,你可以把 UTXO 设定为「某个比特币地址的主人可解锁」,该地址的主人需要提供数字签名和公钥(P2PKH)。而在Pay-to-Script-Hash(P2SH)交易类型中,你可以在 UTXO 锁定脚本中添加一个 Script Hash,谁能提交这个 Hash 对应的脚本原像,并满足该脚本原像中预设的条件,就可以解锁 UTXO。BitVM 所依赖的 Taproot 脚本,用到了类似于 P2SH 的特性。

比特币脚本怎么触发

这里我们先以 P2PKH 为案例介绍比特币脚本的触发方式,只有理解了其触发方式才能理解更为复杂的 Taproot 和 BitVM。P2PKH 全称「Pay to Public Key Hash」,在这种方案下,UTXO 的锁定脚本中会设置一个公钥 hash,解锁时需要提交对应该 hash 的公钥,这和常规的比特币转账思路基本一致。

此时,比特币节点要确定解锁脚本中的公钥,和锁定脚本中指定的公钥 hash 能对上号,也就是说,要确定解锁人提交的「钥匙」和 UTXO 预设的「锁」彼此匹配。

进一步说,P2PKH 方案下,比特币节点收到交易后,会将用户给出的解锁脚本 ScriptSig,与要解锁的 UTXO 的锁定脚本 ScriptPubkey 拼接到一起,放在 BTC 脚本的执行环境内执行。下图给出执行前的拼接结果:

可能读者并不了解 BTC 的脚本执行环境,此处我们进行简单介绍。首先,BTC 脚本包含两种元素:

数据和操作码。这些数据和操作码会按照从左到右的顺序,依次压入栈内按照指定逻辑来执行,得到最终结果(关于什么是栈 此处不展开详述 读者可以自行 Chatgpt)。

以上图为例,左侧是某人上传的解锁脚本 ScriptSig,包含他的数字签名和公钥,而右侧的锁定脚本 ScriptPubkey 中,包含 UTXO 创建者生成该 UTXO 时设置的一段操作码和数据(此处我们不需要了解每个操作码的含义,理解个大概即可)

上图中右侧的锁定脚本中的 DUP、HASH160、EQUALVERIFY 等操作码,负责把左侧的解锁脚本中携带的 Public key 取哈希,和锁定脚本中预设的 Public key hash 做对比,若两者相等,说明解锁脚本中上传的公钥,和锁定脚本中预设的公钥哈希相匹配,这就通过了第一道验证。

但是,有个问题,UTXO 锁定脚本的内容其实是在链上公开的,任何人都能观测到其中包含的公钥哈希,谁都可以上传对应的公钥,谎称自己是那个被「钦定」的人。所以在验证完公钥和公钥 hash 后,还要验证交易发起人是否真是该公钥的实际控制者,这就要对数字签名进行核验。锁定脚本中的 CHECKSIG 操作码,就是负责验证数字签名的。

总结一下,P2PKH 方案下,交易发起人提交的解锁脚本中,包含公钥和数字签名,该公钥要和锁定脚本中指定的公钥哈希匹配,且交易的数字签名正确,满足这些条件才能顺利解锁 UTXO。

(这个图是动态的:P2PKH 方案下比特币解锁脚本示意图

来源:https://learnmeabitcoin.com/technical/script )

当然,比特币网络中支持多种交易类型,不只有 Pay to public key/public key hash,还有 P2SH(Pay to Script hash)等,一切取决于 UTXO 创建时自定义的锁定脚本被设置成什么样

这里需要注意的是,P2SH 方案下,锁定脚本中可以预设一个 Script Hash,而解锁脚本需要把 Script Hash 对应的脚本内容完整提交上来。比特币节点可以执行这段脚本,如果这段脚本里定义了多签验证的逻辑,就可以在比特币链上实现多签钱包的效果。

当然,P2SH 方案下,UTXO 创建者要让未来解锁 UTXO 的人事先知道 Script Hash 对应的脚本内容,只要双方都知道这段 Script 的内容,那么我们就可以实现比多签更复杂的业务逻辑。

这里要说明一点,比特币链上(区块)并不直接记录哪些 UTXO 和哪些地址关联,它只记录 UTXO 可以被哪个公钥哈希 / 哪个脚本哈希解锁,但我们根据公钥 hash/ 脚本 hash 可以快速算出对应的地址(钱包界面显示的那一段像乱码的东西)。

我们之所以能在区块浏览器和钱包界面看到 xx 地址下有 xx 数额的比特币,是因为区块浏览器和钱包项目方帮你解析了这些数据,会扫描所有区块并根据锁定脚本中声明的公钥 hash/ 脚本 hash,计算出对应的「地址」,然后显示出 xx 地址名下有多少比特币。

隔离见证与 Witness

当我们理解了 P2SH 的思路后,便和 BitVM 所依赖的 Taproot 更近一步了。但在此之前,我们要了解一个重要的概念:Witness 和隔离见证。

复盘前面讲到的解锁脚本和锁定脚本,以及 UTXO 解锁流程,会发现一个问题:交易的数字签名包含在解锁脚本中,生成签名时不能把解锁脚本覆盖进去(生成签名用到的参数不能包含签名本身),所以数字签名只能覆盖解锁脚本之外的部分,也就是只能与交易数据的主干部分建立关联,不能完整的覆盖交易数据。

这样一来,就算交易的解锁脚本被中间人稍做手脚,也不会影响到验签结果。比如说,比特币节点或矿池可以在交易的解锁脚本中,塞入其他数据,在不影响验签和交易结果的前提下,使得交易数据发生细微变化,最后算出的交易 hash/ 交易 ID 也会改变。这被称为交易延展性问题

这带来的坏处是,如果你打算连续发起多笔交易,并且有次序上的依赖关系(比如,交易 3 引用了交易 2 的输出,交易 2 引用了交易 1 的输出),那么排后面的交易必然要引用前面交易的 ID(hash),矿池或比特币节点等任意中间人可以微调解锁脚本中的内容,使交易上链后的 hash 与你预期的不一致,那么你预先创建好的多笔有次序关联的交易会失效。

实际上,在 DLC 桥和 BitVM2 的方案中,会批量构建有先后次序关联性的交易,所以前面提到的场景并不少见。

简单来说,交易延展性问题是因为,交易的 ID/hash 在计算时,会把解锁脚本的数据包含进去,而比特币节点等中间人可以微调解锁脚本中的内容, 导致交易 ID 与用户预期的不符合。其实这是比特币在早期设计时考虑不周留下的历史包袱。

后来推出的隔离见证 /SegWit 升级,其实就是把交易 ID 和解锁脚本彻底解耦,计算交易 hash 时不需要把解锁脚本数据包含进去。遵循 SegWit 升级的 UTXO 锁定脚本,会默认在首位设置一个叫「OP_0」的操作码,充当标记;而对应的解锁脚本,从 SigScript 更名为了 Witness(见证)。

遵循隔离见证规则后,交易延展性问题会被妥善解决,你不需要担心发送给比特币节点的交易数据被微调。当然我们不需要想的太复杂,P2WSH 的功能和前面谈到的 P2SH 并无本质差异,你可以在 UTXO 锁定脚本中预设一个脚本哈希,等解锁脚本的提交者把 hash 对应的脚本内容提交到链上并执行。

如果你要实现的脚本内容特别庞大,包含特别多的代码,通过常规的方法无法把完整的脚本提交到比特币链上(每个区块有大小限制)。那怎么办?这就需要借助 Taproot,针对上链的脚本内容进行精简化处理,而 BitVM 正是基于 Taproot 构建出的复杂方案。

在「走近 BTC」的下一篇文章中,我们将对 Taproot、预签名等和 BitVM 相关的其他更复杂的技术进行详细科普,大家敬请期待!