本文将讨论在比特币上实现通用计算的任务及其涉及的各个步骤。
原文标题:《The path to general computation on Bitcoin》
撰文:StarkWare
本文,我们将讨论在比特币上实现通用计算的任务及其涉及的各个步骤。事实上,实现在比特币脚本上计算任意逻辑的能力是一个吸引人的目标,因为其将比特币带入了非托管应用和去中心化金融的领域。
之前,由于两个严重的限制 — 比特币脚本长度和操作码的表达能力,比特币上的通用计算几乎被认为是不可能的。前者禁止了除短脚本之外的任何交易,后者限制了脚本操作码能够计算的内容。这两个限制共同构成了比特币计算可行性的严峻局面,因为许多应用无法在这些限制条件下实现。
不过,近年来,这种状况已经有了明显改善。事实上,随着 2021 年 Taproot 的升级,脚本长度的限制取消了,使得比特币脚本可以显著加长(事实上,Taproot 脚本甚至允许有时只对脚本的一部分进行链上支付,但我们不会在本文探讨这一特性)。此外,为了解决操作码表达能力的限制,我们发现一个简单的操作码就足以有效地实现无限的表达能力。
上述操作码被称为 OP_CAT,自 2012 年起就在比特币脚本中被禁用。这是一个非常简单的操作码,允许在堆栈上连接元素,只需要 13 行代码,就可以通过软分叉在比特币上进行非侵入性激活。从这个看似不起眼的操作码中衍生出如此大的能量是非常不容易的,因此,本文的核心目标就是详细阐述这一点。
下文,我们将深入探讨比特币上的计算过程,从其实现方式以及存在的限制开始。
接下来,我们将概述两种工具 — Covenant 和 STARK 证明,并证明如果二者能在比特币脚本中运行,将能够实现比特币上的通用计算。此外,我们将在本文中论证,得益于 STARK 证明,这种计算还能被赋予更多有用的属性:
最后,我们将讨论 OP_CAT,及其如何实现上述工具。我们还将提到我们在这方面所做的一些努力,并讨论未来的发展方向。
注意:本文简化了一些技术性问题,以提供一个清晰的思维模型。当涉及实际实施所呈现的想法时,「细节决定成败」。
本节,我们将简要概述比特币的 UTXO 模型,即每个 UTXO 都被锁定在一个锁定脚本后面。如果你对此很熟悉,可以跳到下一节内容。
了解比特币交易的第一步是注意到它们是根据未花费的交易输出(UTXO)模型进行安排和处理的,如图 1 所示。比特币交易可以有多个输入和输出,其中每个输出代表了可以由另一笔交易所花费的一定数量的比特币。相应地,一个交易的输入则代表了另一个比特币交易输出的支出。当然,我们要求输入中的比特币总量与输出中的比特币总量大致相等(差额为支付给矿工的费用)。
图 1:UTXO 模型中的交易。每个交易的输入和输出都与一定数量的比特币相关联,其中输出中的累计金额不能超过输入中的金额。未花费的交易输出(UTXO)以蓝色表示。
每个交易输出都被一个锁定脚本(scriptPubKey)锁定,该脚本将接受或拒绝它所给定的输入(scriptSig)(由所谓的花费者提供)。因此,如果想要从一个未花费的交易输出(UTXO)中转移资金,你必须能够生成正确的 scriptSig,并将此 UTXO 作为输入包含在你生成的交易中。
默认情况下,锁定脚本将简单地根据硬编码的公钥验证数字签名,这定义了被锁定比特币的所有权。更笼统地说,每当存在一个 UTXO 时,我们就可以说,能够提供正确 scriptSig 的人就是对应锁定脚本背后锁定的比特币的所有者。因此,每个比特币所有者的总余额由与该所有者相关的所有 UTXO 累积决定(例如,由同一人进行数字签名的所有 UTXO)。
上一节概述了锁定脚本的一般情况,以及它们如何与比特币交易和比特币本身互动。本节,我们将重点介绍锁定脚本本身及其组成内容。
比特币上的锁定脚本是用一种类似于 Forth 的基于堆栈的语言编写,这种语言被称为「比特币脚本」。由于比特币脚本没有循环,我们通过脚本所需的操作码数量来衡量脚本的成本,因为脚本的长度与其运行时间成正比。在图 2 中,我们可以看到一个简单程序的示意图,该程序将检查两个输入的和是否为 12。
图 2*:一个检查两个输入值之和是否为 12 的简单程序。程序的执行是通过将 scriptSig 与锁定脚本本身连接起来,然后从左到右处理操作码。scriptSig 只将元素推入堆栈。(a) 计算的第一步。(b)* 计算过程中。(c) 计算的最后一步(锁定脚本接受输入)。
我们必须指出,比特币脚本有很多局限性,这使得为它开发简单的逻辑变得非常困难,甚至根本不可能。这些局限性包括:
在最后一点提到的 4 字节限制尤其成问题。实际上,这意味着你无法修改长元素,例如,加密操作所需的 20 字节或更长的元素。
例如,将数据连接在一起并应用加密操作这种非常常见的操作是不可能完成的,除非忽略加密操作码,并将其模拟为对由 4 字节元素数组表示的数据进行的操作。后者的成本极高,因为单一的哈希操作可能需要数十万个操作码来模拟。
最后,我们要指出的是,由于比特币脚本的开发难度很大,代码自然更容易出现错误和漏洞。
虽然上一节已经证明比特币脚本还有很多不足之处,但我们认为,比特币锁定脚本的最大限制在于其只能读取花费者的输入。这意味着两个严重的限制:
然后,我们将看到,通过构建 Covenant ,我们已经可以获得状态性。直观地说,这是因为你可以使用 Covenant 在交易数据本身中模拟存储的读取和写入。
事实上,以上所述还不足以概括比特币脚本及其复杂性的全部内容。但至少,我们可以肯定的是,编写比特币脚本并非易事。比特币脚本有非常严格的限制:
此外,根据上述限制编写代码容易出现错误和漏洞。
虽然支付等非常简单的应用可以满足上述限制,但任何稍微复杂的应用都会遇到障碍。下文,我们将了解 Covenant 和 STARK 证明如何使我们打破这些局限性。此外,我们还将讨论 OP_CAT 如何帮助比特币脚本实现上述功能。
本节,我们将了解比特币中的 Covenant 是如何保存逻辑状态的,并将更通俗地解释,比特币上的「智能合约」是什么样的。这里的智能合约指的是某种有状态的逻辑,它包含一系列函数,可以根据预定义的规则调用这些函数,将状态转换为不同的有效状态。
更具体地说,比特币合约需要以下功能:
如前所述,上述功能可以通过 Covenant 来实现。回想一下,默认情况下,锁定脚本只能读取直接输入的数据(scriptSig)。然而,通过 Covenant,锁定脚本可以读取花费者交易的所有数据字段,以及锁定脚本所在的交易数据字段,甚至其他交易的数据。图 3 展示了使用 Covenant 时锁定脚本可以访问的不同部分。
图 3:锁定脚本(可视化为一个锁)可以访问的一些数据字段(红色箭头所示)。除了访问花费者交易的数据字段外,锁定脚本还可以访问锁定脚本所在的交易(tx1)的数据字段,以及由花费者交易同时花费的另一笔交易(tx2)的数据字段。
我们认为,上述描述的 Covenant 足以在比特币上构建通用智能合约。实际上,通用的方法是在交易数据本身中编码状态,并检查其向新值转换的有效性。为此,我们的交易将有两个输出:
锁定脚本逻辑将执行以下规则:
图 4 是这种结构的示意图。正如 Weikeng Chen 所写的那样,已有许多人建议为这种结构起一个正式的名称,而「状态守车(state caboose)」作为一个候选名字脱颖而出。守车(caboos)指的是连接在货运列车尾部的铁路车厢,是指乘务员的专用车厢及其工作空间。
图 4:通过 Covenant 在比特币上实现的智能合约,遵循「状态守车」设计模式。锁定脚本确保花费者交易具有相同的形式和锁定脚本,并且具备有效的新状态 S’,其从上一个状态 S 的转换也是有效的。
举个简单的例子,你可以想象一个简单的计数智能合约。该合约的状态总是按花费者提供的值递增,这实际上调用了一个「累计」函数。这个计数智能合约如图 5 所示。
图 5:一个简单的智能合约,通过将输入(scriptSig)相加而将输入累加到其状态中。
最后要指出的是,为了「调用合约」并将合约转换到新的有效状态,用户需要为每次调用创建一个新的花费者交易。与以太坊智能合约不同,比特币智能合约本质上是按顺序的,要求交易同时承诺当前状态和新状态。在比特币上构建智能合约时,必须考虑到这种顺序属性。尽管也存在一些可能缓和该限制的方案,但本文将不对此展开讨论。
总的来说,我们了解到 Covenant 使比特币上实现了智能合约,理论上来说,当计算被分割到足够多的交易中时,可以在比特币上进行任意长度的计算。然而,仅仅使用 Covenant 仍然受到比特币脚本的大部分限制,即堆栈大小限制、操作码数量有限,以及一般情况下必须根据比特币脚本语言的约束进行编程。
下文,我们将探讨 STARK 证明系统如何通过减少比特币上计算的区块链占用以及允许使用完全不同的语言进行脚本编写,从而缓解上述限制。这种方法显著提高了编程效率和安全性。
本节的目的是探讨如何在比特币上进行计算,同时把通过比特币脚本进行的实际计算量降至最低水平。我们的一般方法是通过计算委托,即将计算转移至链下进行(多个实体都有可能参与),只有验证步骤在链上的比特币脚本中进行。
这就引出了一个问题:我们如何确保链下计算是正确执行的?实际上,解决这个问题的一个很自然的候选方案就是证明系统。简单来说,证明系统允许 Alice(证明者)通过提供一个「证明」,来说服 Bob(验证者)某个语句的正确性,关键在于:
被证明的语句几乎可以是任何语句,从难题的解法,到更相关的计算委托例子。更具体地说,Alice 将向 Bob 证明 f(x)=y,而不需要 Bob 自己进行 f(x) 的计算,如图 6 所示。
图 6:计算委托的证明系统。证明者计算 y=f(x) ,并让验证者相信这个语句的正确性。关键点在于,如果 y≠f(x),证明者则无法说服验证者相信这个事实。
因此,我们减少计算在区块链上的占用的方法将是在比特币脚本中实现一个证明系统的验证器。因此,为了调用智能合约,调用者将提供一个正确状态转换的证明,比特币智能合约将验证这个状态转换的证明的正确性,而不是直接检查状态转换(见图 7)。
此外,这种方法还有一个至关重要的好处,那就是比特币脚本的逻辑可以保持固定不变,而不受应用的影响,这就大大降低了出现错误的几率,并简化了审计工作。这源于一个简单的事实:同一个验证算法可以验证 y=f(x) 语句或 y=g(x) 语句,而无需事先知道要计算的函数。
图 7:通过 Covenant 在比特币上实现的智能合约,遵循图 4 中的「状态守车」设计模式,但增加了一个验证器。这一次,锁定脚本不再直接检查从 S 到 S’ 的转换是否有效,而是验证转换是否正确的证明。
虽然存在许多证明系统,但我们选择使用 STARK 证明系统(STARK),因为其具有许多吸引人的特点:
最后,与其他证明系统相比⬇️
STARK 对比特币友好,即其验证器组件更容易在比特币脚本中实现。从本质上讲,这是因为 STARK 主要基于哈希,与基于配对的证明相比,涉及的代数运算非常少。
在比特币脚本中代数运算的成本很高,这就解释了为什么 STARK 是比特币友好的。此外,Circle-STARK 变体由于字段很小,对比特币尤其友好。
因此,由于比特币验证器可以相对容易地验证任何计算,我们可以选择验证哪种类型的计算。值得注意的是,这甚至可以是某种类型的 CPU 执行。此外,我们还可以在 CPU 的基础上设计高级编程语言。为此,我们在 StarkWare 开发了 Cairo,这是一种类 Rust、人性化且安全的编程语言,专为高效证明和验证而设计。在比特币上证明 Cairo 的执行可以带来很大的优势。
总的来说,通过使用 STARK 验证器以及 Covenant ,我们能够在比特币上实现通用计算。此外,这种计算还具有以下吸引人的附加特性:
如上所述,OP_CAT 是一个简单的、目前已被禁用的操作码,其可以让比特币脚本连接堆栈上的元素。这个简单操作的重要性不可低估,因为它可以同时在比特币上实现 Covenant 和 STARK。具体操作如下:
关于最后一点,在 Pay2Taproot 输出中保存状态涉及一些技术难题。不过,可以使用其他输出类型来存储状态,如 Pay2WitnessScriptHash。这就产生了前面提到的图 4 和图 5 中的「状态守车」技术,交易中有两个输出。
图 8:*Merkle 路径验证,涉及哈希和字符串连接操作。Merkle 路径各部分(蓝色字符串)被连接起来,并进行相应的哈希处理。然后,与 Merkle 根进行相等性检查。OP_CAT 操作被写成 ||。*
在由 Weikeng Chen 和 Pinghzou Yuan 领导的开源项目中,我们正在共同努力建设 Bitcoin Wildlife Sanctuary。其中的两个主要项目包括:
通过这一努力,我们的目标是为比特币带来高效、安全、对开发者友好的 OP_CAT 智能合约。我们认为,这对比特币计算领域来说是一个充满希望且令人兴奋的前景。
总的来说,通过本文,我们了解到:
下一篇文章,我们将深入探讨比特币上的 Circle-STARK 验证器。
感谢 Weikeng Chen、Pingzhou Yuan 以及 StarkWare 团队对本文提出的宝贵意见。
Victor Kolobov
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。