映月读书网 > 区块链:技术驱动金融 > 3.2 比特币的脚本 >

3.2 比特币的脚本

每个交易输出不仅确定了一个公钥,其实同时指定了一个脚本。那脚本是什么?为什么我们要用一个脚本?在这一节我们要学习比特币的工作控制语言,也叫脚本。之后,我们就会懂得为什么要用一个脚本,而不是简单地分配一个公钥。

最常见的比特币交易,就是通过某人的签名去取得他在前一笔交易中获得的资金。这种情况下,我们希望交易的输出包含这样的信息:“凭借地址X的所有者的签名,才可以获得这笔资金。”我们知道地址其实就是一个公钥的哈希值,所以仅仅说地址X并没有告诉我们公钥在哪里,也没有给我们一个检查签名的方法。所以,交易输出必须这样描述:“凭借哈希值为X的公钥,以及这个公钥所有者的签名,才可以获得这笔资金。”这实际上就是最常见的比特币脚本,如图3.4所示。

图3.4 P2PH脚本范例

注:一个常见的比特币输出脚本范例。

那么谁执行这个脚本?这一系列指令是如何完成的呢?秘密在于,交易的输入包括了脚本(而不是签名)。为了确认一笔交易正确地获取了上一笔交易所输出的资金,我们把交易的输入脚本和上一笔交易的输出脚本串联起来,这个串联脚本必须被成功地执行后才可以获取资金。这两个脚本,一个是输出脚本(scriptPubKey),另一个是输入脚本(scriptSig)。输出脚本只是指定了一个公钥(或是公钥哈希值的地址),输入脚本指定了一个对应公钥的签名。图3.5就是两个脚本结合的案例。

比特币脚本语言

这个脚本语言是为比特币开发的。在比特币里只叫作“脚本”。它和另一种Forth语言有很多相似的地方,Forth是一种简单的堆栈式编程语言(stack-based programming language),但你并不需要先学习Forth语言才会使用比特币的脚本语言。比特币的脚本语言设计原则就是简明扼要,并内生地支持加密操作。比如,脚本里面有目的性的指令用来计算哈希值和检验签名。

这种脚本语言是堆栈式的,意味着每个指令只被执行一次,是线性的,无法循环执行。所以指令的数目给了我们一个执行时间与内存使用的上限。这个语言不是图灵完备的,意味着不能随意运行强大函数功能。[1]但这是有意设计的,因为矿工需要去执行这些网络上任意交易提交者所递交的脚本,设计者并不希望让他们提交可能无限循环的脚本。

图3.5 结合输入脚本和输出脚本范例

注:为了确认当前交易是否正确地获取了前一笔交易输出的资金,我们把两个脚本链接起来,把上一笔交易的输出脚本(图中虚线下方)添加到当前交易的输入脚本(虚线上方)之后,形成一个新的脚本。请注意<pubKeyHash?>里面有一个“?”,用作标识——我们后面会来确认它是否与当前交易提供的公钥的哈希值一致。

执行比特币脚本只能产生两个结果:要么被成功执行,这种情况下,交易有效;要么脚本执行出现错误,这种情况下,整个交易无效,拒绝记入区块链。

这个脚本语言十分简单。只有256个指令,每个只用一个字节。256个指令中,有15个目前不可用,有75个被保留还没有具体定义(以后或许可以被用来扩展),剩下的才是可用的。

许多在其他语言里常见的基本指令这里面都有。例如,基本的算数、逻辑语句(如If-then)、抛出错误、过早返回等。而且,还有密码指令,比如哈希函数语句、签名验证语句,还有一个重要的特殊指令是“CHECKMULTISIG”——可以查证多个签名。表3.1列举了一些比特币工作控制语言里的常用语句。

CHECKMULTISIG指令要求指定n个公钥和一个参数t(作为一个临界值)。这个指令正确执行的条件是:在n个公钥中,至少可以选出t个现时有效的签名。我们在本章3.3节会示范这个指令的用法,但现在我们需要认识到这个原生指令是非常强大的,它以一种极其精练的方式协助我们查验交易中的多方签名。

不过,目前比特币多方签名功能实现过程中有一个缺陷,CHECKMULTISIG指令在执行的时候会返回一个没用的值,而且系统还必须要安排一个堆栈中的变量去储存它,然后再忽略掉。由于修复这个缺陷成本很高,两害相权取其轻,这个缺陷就一直没被修复,我们在第3章3.6节会再做讨论。但目前,这个程序缺陷也算是比特币的一个特性。

表3.1 一些比特币脚本工作语言中的指令及其功能

执行一个脚本

在堆栈语言里执行一个脚本,我们只需要一个堆栈来垒积数据,不需要分配任何内存与变量。因此,堆栈语言中计算相当容易。总共有两类指令:数据指令和工作码指令。数据指令的作用是把数据推到堆栈的最上面;工作码指令则通常是用堆栈顶部的数据作为输入值,用来计算一个函数。

我们现在来一起看一下,图3.5这段脚本是怎么执行的。图3.6给我们展示了每一条指令执行后的堆栈状态。脚本中的前两条指令属于数据指令,分别是输入脚本(包含在交易的输入项)中的签名和用来验证签名的公钥。我们前面提到过,一看到数据指令,系统就把它堆到堆栈最上面。后面几个指令是输出脚本(包含在上一交易的输出项中)里的指令。

首先,我们复制指令OP_DUP,这一步仅仅是将堆栈最上层的公钥复制,并置于堆栈最上层;下一个指令是OP_HASH160,该指令取得堆栈最上层的数据,并计算其哈希值,然后将结果再堆到堆栈最上层。当指令执行完成后,我们将堆栈最上层的公钥替换成了公钥的哈希值。

图3.6 比特币脚本的执行堆栈状态图

注:图中底部列出了相对应的指令:尖括号里的是数据指令,以OP开头的是工作码指令,指令上方对应的是指令执行之后的堆栈状态。

接下来,我们还要在堆栈顶层再推送一些数据:此笔交易发送者指定的公钥的哈希值,以及对应的私钥,这样才可完成签名,取得资金。此时,堆栈顶部有两个数值,一个是发送者指定的公钥的哈希值,另一个是接收者想要取得资金时提交的公钥的哈希值。

这个时候,我们就要执行EQUALVERIFY命令了,这个命令是用来检查堆栈顶部两个数值是否相等的。如果不相等,就会抛出一个失败信号,并且停止执行脚本。不过现在我们假设其相等,也就代表着接收者使用的是正确的公钥。这条指令会移除堆栈顶部的两条数据,这时,堆栈还剩下两个数据:公钥以及签名。

我们已经证实接收者使用的公钥确实就是交易里指定的公钥,但现在我们必须证实这个签名是真的。这时,使用OP_CHECKSIG指令即可。这里我们可以看出比特币的脚本语言虽然简单,但很强大。它只用“OP_CHECKSIG”就能实现一个很复杂的事情:移除堆栈里两个数值,然后用公钥来证实整个交易的签名是真的。

但这里的签名究竟是对什么的签名?签名函数的输入是什么?实际上,在比特币中,我们只可以对一个事情进行签名——就是整个交易。所以,CHECKSIG指令从堆栈中取出两个数据(公钥以及签名),并验证签名对于整个交易(使用对应公钥发起的交易)来说是有效的。现在我们完成了所有的指令,堆栈里面什么也不剩。假设没有碰到任何差错的话,这个脚本的输出就是一个“真”表示这个交易是正当有效的。

实际情况

理论上来讲,通过脚本,我们可以随意地为比特币支付设定条件。当然,从2015年的情况看,这些特性也并不太常用到。如果我们回顾比特币历史中曾经实际用到的脚本,绝大多数的比特币使用的脚本都非常基础,像前文的例子一样:指定一个公钥,然后通过验证签名来使用这个币。

当然,实际中也会使用一些其他指令,比如MULTISIG,还有一种支付给脚本的哈希值(Pay-to-script-hash,简称P2SH,我们很快会谈到)等,但除此之外,平时常用的指令真不多,因为每个节点都有一份标准脚本的白名单,它们会拒绝接受不在名单上的脚本。这倒不是说无法运行其他脚本,只是使用起来比较麻烦。事实上这样的安排也很巧妙,我们会在谈论比特币点对点网络的时候再进行描述。

销毁证明

销毁证明(proof of burn)脚本,用于销毁比特币(即防止资金被赎回)。如果交易代码的运行结果是将比特币转到“销毁证明”脚本,那么这笔比特币将被销毁。实际应用中主要是用来引导客户使用其他数字货币系统,即将比特币销毁,以便获得另一个数字货币系统发行的新币。我们会在第10章展开叙述。销毁证明脚本使用起来非常简便:使用OP_RETURN脚本来抛出错误;不论之前指令的运行结果是什么,OP_RETURN指令总会被执行,并相应抛出一个错误,脚本返回一个“错误”(false)值。

由于OP_RETURN以抛出错误的形式结束脚本,其后的所有指令都不会执行。利用这个特性,我们可以往脚本中植入任意信息,这些信息也将被存储在区块链中。假如你想通过署名或者盖时间戳的方式来证明你在某个时候知道某件事情,就可以发起一笔极小额的比特币交易,在脚本中加入上述信息,并使用销毁证明脚本将币销毁,这样就可以将信息永久地存储在区块链上。

支付给脚本的哈希值

如前文所述,比特币的工作机制要求币的发送者必须在交易时明确指定脚本。这种机制有时候不太适用:假如你在网店看中了一件商品并打算下单,你会问卖家“请把付款地址告诉我,我可以付款了”,但如果卖家使用了多重签名地址(MULTISIG),那他会说“嘿,我们用了多重签名地址,你需要支付给一个脚本地址,而不是一个简单的地址”,但你会说“我不知道怎么弄,这太复杂了,我只会支付给简单的地址”。

比特币用了一种很聪明的办法来解决这个问题,不仅可以实现多重签名地址支付,而且还可以实现复杂的资金监管规则。比特币使用的办法是:收款方告诉付款方“请把比特币支付给某个脚本地址,脚本的哈希值是××,在取款的时候,我会提供上述哈希值对应的脚本,同时,提供数据通过脚本的验证”,而不是“请把比特币支付给某个公钥,公钥的哈希值是××”。付款方通过P2SH即可实现上述交易。

需要说明的是,P2SH脚本只是对堆栈最顶层的数据进行哈希运算,核验运算结果是否与给定的哈希值一致,核验通过后,再执行一步特殊的核验:将堆栈最顶层的数据重新解读为一系列指令,然后将其作为脚本运行一次,此时,堆栈中的其他数据作为脚本的输入值。

要做到P2SH还是有点复杂的,因为P2SH不是比特币的原始设计,是后来加上去的。它解决了两个重要的问题:让付款方的支付工作简单化,收款方只需告诉付款方一个哈希值即可。在我们上面的例子中,你不再需要去关心商家到底用哪种地址,是否用了多重签名,因为这只是商家在支取这笔款项时需要考虑的事情。

P2SH还实现了效率上的提升:矿工的工作是追踪那些还没有被消费掉的输出脚本。采用P2SH的输出脚本会变得很小——它们只不过是个哈希值而已。所有的复杂性都被放在输入脚本中了。

[1] 图灵是第二次世界大战时英国数学家,密码学家。他破译了纳粹的密码机“谜”,为盟军取得第二次世界大战胜利做出重大贡献,美国好莱坞以此题材拍了一部电影《模仿游戏》。图灵完备的意思是语言有能力随意地执行强大的函数。——译者注