# 第7章 高级交易和脚本

# 7.1 介绍

本章将进行大量更新。如需参考BTC原有介绍,包括P2SH,隔离验证等内容请参考MasterBitcoin2CN (opens new window) 相关章节

在上一章中,我们介绍了比特币交易的基本要素,并且了解了最常见的交易脚本类型,即P2PKH脚本。在本章中,我们将介绍更高级的脚本,以及如何使用它来构建复杂条件的交易。

首先,我们将了解多重签名multisignature脚本。接下来,我们将开启了复杂脚本的整个世界。然后,查看新的脚本操作符。

# 7.2 多重签名

多重签名脚本设置了一个条件,脚本中记录了N个公钥,必须至少提供其中的M个签名才能解锁资金。这也称为M/N方案,其中N是密钥的总数,M是验证必须的签名数。例如,2/3的多重签名是三个公钥被列为潜在签名人,其中至少两个必须用于签名才能创建有效的使用资金的交易。

设置M/N多重签名条件的锁定脚本的一般形式是:

M <Public Key 1> <Public Key 2> ... <Public Key N> N CHECKMULTISIG
1

M是花费输出所需的签名的数量的底限,N是列出的公钥的总数。 设置2/3多重签名条件的锁定脚本如下所示:

2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG
1

上述锁定脚本可被含有一对签名和公钥的解锁脚本满足:

<Signature B> <Signature C>
1

或者由3个公钥中任意2个对应的私钥产生的签名组合。

这两个脚本一起形成下面的组合验证脚本:

<Signature B> <Signature C> 2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG
1

执行时,仅当解锁脚本与锁定脚本设置的条件匹配时,此组合脚本的评估结果才为TRUE。上述例子中设置条件就是:解锁脚本是否含有3个公钥中的任意2个相对应的私钥的有效签名。

CHECKMULTISIG执行中的bug

CHECKMULTISIG的执行中出现了一个bug,需要做一些轻微的变通。 就是当CHECKMULTISIG执行时,它应该消耗堆栈上的M + N + 2个项目作为参数。 然而,由于该bug,CHECKMULTISIG会弹出一个额外的值或超出预期一个值。

我们使用前面的验证示例更详细地看一下:

<Signature B> <Signature C> 2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG
1

首先,CHECKMULTISIG弹出最上面的项目,这是N(在这个例子中N是“3”)。然后它弹出N个项目,这是可以签名的公钥数。在这个例子中,是公钥A,B和C,然后,它弹出一个项目,即M,参与仲裁数目(需要多少个签名)。这里M = 2。此时,CHECKMULTISIG应弹出最后的M个项目,就是那些签名,并查看它们是否有效。然而,不幸的是,实施中的错误导致CHECKMULTISIG再弹出一个项目(总共M + 1个)。检查签名时,额外的项目被忽略,虽然它对CHECKMULTISIG本身没有直接影响。但是,必须存在额外的值,因为如果不存在,则当CHECKMULTISIG尝试弹出到空堆栈上时,会导致堆栈错误和脚本失败(将交易标记为无效)。因为额外的项目被忽略,它可以是任何东西,但通常使用0。

因为这个bug已经成为共识规则的一部分,所以现在它必须被永远复制。因此,正确的脚本验证将如下所示:

0 <Signature B> <Signature C> 2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG
1

这样解锁脚本就不是下面的:

<Signature B> <Signature C>
1

而是:

0 <Signature B> <Signature C>
1

从现在开始,如果你看到一个多签解锁脚本,你应该期望开头就看到有一个额外的0,其目的是解决一个bug,却意外地成为共识规则。

# 7.3 AccumulatorMultiSig

【第1章 比特币介绍】 (opens new window)中,我们曾介绍过迪拜的电子产品进口商Mohammed。他的公司账目广泛采用比特币的多重签名功能。多重签名脚本是比特币高级脚本最为常见的一种用途之一,是一种非常强大的功能。Mohammed的公司对所有客户付款,会计术语称为“应收账款”,即AR,都使用多重签名脚本。基于多重签名方案,客户支付的任何款项都会被锁定,必须至少两个签名才能解锁,一个来自Mohammed,另一个来自其合伙人或拥有备份密钥的律师。这样的多重签名机制能提升公司治理管控,同时也能有效防范盗窃、挪用和丢失。 ~~

最终的脚本非常长:

2 <Mohammed's Public Key> <Partner1 Public Key> <Partner2 Public Key> <Partner3 Public Key> <Attorney Public Key> 5 OP_C HECKMULTISIG
1

~~虽然多重签名十分强大,但使用起来还是多有不便。Mohammed必须在客户付款前将上面的脚本发送给每一位客户,而每一位客户也必须使用专用的能创建自定义交易脚本的比特币钱包软件,每位客户还得学会如何利用自定义脚本来创建交易。此外,由于脚本可能包含特别长的公钥,最终的交易脚本可能是最初交易脚本长度的5倍之多。超大的交易还将给客户造成费用负担。最后,这样一个大交易脚本将一直记录在所有节点内存的UTXO集中,直到该笔资金被使用。所有这些都使得这种复杂锁定脚本在实践中变得困难重重。

# 7.4 数据记录输出(RETURN操作符)

比特币的分布式和时间戳账本,即区块链技术,其潜在用途将大大超越支付领域。许多开发者试图充分发挥交易脚本语言的安全性和弹性优势,将其运用于数字公证服务、股票证书和智能合约等领域。使用比特币的脚本语言来实现这些目的的早期尝试,包括创建交易输出,把数据记录在区块链上,例如,以这样的方式记录文件的数字指纹,任何人可以通过引用该交易来建立该文件特定日期的存在证明。

运用比特币的区块链技术存储与比特币支付不相关数据的做法是一个有争议的话题。许多开发者认为其有滥用的嫌疑,因而试图予以阻止。另一些开发者则将之视为区块链技术强大功能的有力证明,认为应该给予大力支持。那些反对包含非支付数据的人辩称这将导致“区块链膨胀”,增加运行的全节点的磁盘存储成本,承担了区块链不应该携带的数据。而且,此类交易创建了不能花费的UTXO,使用目标比特币地址作为20字节的自由格式字段。因为比特币地址只是被当作数据使用,并不对应于私钥,所以会导致UTXO永远不能用于交易,因而是伪支付。这些交易永远不会被花费,所以永远不会从UTXO集中删除,会导致UTXO数据库的大小永远增加“膨胀”。

RETURN 允许开发者在交易输出上增加非支付数据。然后,与伪UTXO不同,RETURN 创造了一种明确的可验证不可消费型输出,此类数据无需存储于UTXO集。RETURN输出被记录在区块链上,它们会消耗磁盘空间,也会导致区块链规模的增加,但它们不存储在UTXO集中,因此也不会使得UTXO内存池膨胀,更不会增加全节点昂贵的内存代价。

RETURN 脚本的样式:

  FALSE RETURN <data>
1

“data”部分的长度由矿工根据自己处理能力进行限制。节点的缺省设置是UINT32_MAX字节(4096M)。

请记住,并不存在对应于RETURN 的解锁脚本,也就不能花费RETURN的输出。RETURN的关键点就是锁定的输出不能花费,因此它不需要被保存在UTXO集中供未来消费,RETURN是可验证但是不可花费的。 RETURN 常为一个金额为0比特币的输出, 因为分配到该输出的比特币都会永久消失。假如一笔 RETURN 被作为一笔交易的输入,脚本验证引擎将会阻止验证脚本的执行,将标记交易为无效。执行RETURN本质上导致脚本“返回”FALSE并停止执行。如果你不小心将 RETURN 的输出作为另一笔交易的输入,则该交易是无效的。

一笔交易可以有多个 RETURN 输出。

注释 RETURN最初提出的时候,限制为80字节,但发布时,限制被减少到40字节。 2015年2月,在Bitcoin Core的0.10版本中,限制提高到80字节。 节点可以选择不传播或不挖矿RETURN,或者只传播和挖矿包含少于80字节数据的RETURN。在2020年2月,所有的限制被取消。

# 7.5 时间锁(Timelocks)和 nSequence

时间锁是对交易或输出的限制,只允许在一个时间点之后才能消费。比特币从一开始就有一个交易级时间锁定功能,它由交易中的nLocktime字段实现。

时间锁对于延期交易和将资金锁定到将来某个日期很有用。更重要的是,时间锁将比特币脚本扩展到时间的维度,为复杂的多步骤智能合约打开了大门。

# 7.5.1 交易锁定时间(nLocktime)

比特币从一开始就有一个交易级的时间锁功能。交易锁定时间是交易级设置(交易数据结构中的一个字段),它定义了交易是否被打包进区块的最早时间。锁定时间也称为nLocktime,是来自于Bitcoin代码库中使用的变量名称。在大多数交易中将其设置为零,表示可以立即打包。如果nLocktime不为零,低于5亿(500,000,000),则将其解释为区块高度,这意味着交易在指定的区块高度之前不能打包进去区块。如果大于或等于5亿,它被解释为Unix时间戳,并且交易在指定时间之前无法打包进入区块。

但是如果交易中的所有输入的 nSequence 都等于 0xffffff,则忽略 nLockTime。

Unix 时间戳是自 Unix 时代以来经过的秒数,从1970年1月1日00:00:00 UTC 开始,减去闰秒。闰秒被忽略,闰秒的 Unix 时间与之前的闰秒相同。每一天都被当作86400秒来处理。由于这种处理,Unix 时间并不是 UTC 的真实表示。

# 7.5.2 nSequence

nSequence 是交易中每个输入的参数。如果一个交易的 nLocktime 锁被设置为未来的一个时间点,其一个或多个输入的nSequence < 0xffffff,那么该交易不能被打包进入区块。

# 7.5.2 支付通道

将 nLockTime 设置为未来时间,并且一个或多个输入的 nSequence 字段小于0xffffff 的交易被认为是非最终的,交易被保存在一个非最终的交易池中(区别与最终交易池),直到 nLockTime 过期或所有输入的 nSequence 都完成(都等于0xffffff)。

非最终交易可以使用包含相同输入且具有较高 nSequence 号的事务进行替换。这个功能可以用来建立支付通道,允许各方之间以点对点的方式动态交换数据。

支付通道是两方或多方可以直接交换和更新交易的机制。该机制包括建立或打开支付通道、更新通道以及最终确定或关闭支付通道的方法。该机制还涵盖任何当事方变得无反应的可能性,通常是在规定时间之后收回资金。

支付通道支持非常快速的交易更新,只有最终的结束交易会被记录在区块链上。

支付通道可以用于销售数据流、系列事件,或者对游戏等应用程序中的实时数据进行操作。它具有下列特点:

  • 支付通道可以随意开启和关闭
  • 支付通道可以直接由参与方打开,而不需要第三方
  • 使用一个支付通道的交易最终可以上链也可以被取消
  • 支付通道可以是私有的,也可以是公共的
  • 可以在一个通道中有很多参与方
  • 可以在通道中添加和删除参与方
  • 支付通道是数据的通道(data conduits)
  • 通道通常是通过链上支付关闭的,但是链上支付可以在通道之外进行,这将导致通道的更新。

# 7.5.2.1 支付通道的用例:流媒体电影

以下是为了提供流媒体内容而打开、使用和关闭支付渠道的步骤。

  1. 用户浏览目录,查看要观看的节目。内容可以是链接的,也可以是离链的。
  2. 用户选择内容,这里有几种管理通道的方法:
  • 通过公开的矿工网络
  • 每个私有通道使用预先充值的UTXO
  • 设置私有的独立渠道,供每位用户购买内容及提供服务
  1. 在此示例中,参与方将在公开的矿工网络中使用公开的支付通道,通过时间锁定的 UTXO 来提供内容。对于这个示例,我们假设使用的是单个 UTXO。这个 UTXO 进入一个双花监视池。用户发送一个带有以下输出脚本的交易给流媒体服务商,以启动支付通道:
  • Svn 是观众对支付通道的第 n 次签名
  • Pv 是观众的公钥
  • Hv 是观众的公钥哈希 PKH
  • Spn 是服务提供者对通道的第 n 次签名
  • Pp 是服务提供商的公钥
  • Hp 是服务提供商的公钥哈希 PKH
  • Hc0 是所选内容的 merkle 根的哈希
  • Hcn 是每个内容片段的哈希值 Hc1 就是第一片段的哈希
  • Cn 是第n个内容片段 C1 就是第一个数据片段
  • Hfm 是消息的哈希,用户可以通过这个散列来提前结束或暂停流
  • Fm 是提供者用来结束流的消息

被花费的输入序列号是1

交易在两个参与方的脚本迭代如下:

首先用户输入他们想要的支付,并使用 SIGHASH_ALL 进行签名进行支付,输出如下:

  1. 输出脚本Hc0 DROP DUP HASH160 H 160 hp EQUALVERIFY CHECKSIGVERIFY EQUALVERIFY DUP HASH160, 金额为第一个片段的价格
  2. 找零

如果支付通道关闭,查看者还提供以下数据,允许内容提供商使用支付通道输出: Sv0 Pv 的签名为 SIGHASH_ANYONECANPAY | SIGHASH_NONE。用户可以这样做,因为可以生成支付通道的当前 TXID,允许用户根据支付通道的最新版本对输出进行签名。

其余步骤请参考 https://wiki.bitcoinsv.io/index.php/Payment_Channels

# 7.6 复杂脚本

# 7.6.1## 流程控制脚本(条件语句 )

比特币脚本的一个更强大的功能是流程控制,也称为条件语句。您可能熟悉各种编程语言中的类似IF…THEN…ELSE的流程控制。比特币条件语句看起来有点不同,但本质上是相同的构造。

基本上,比特币条件操作码允许我们构造一个具有两种解锁方式的兑换脚本,具体取决于对逻辑条件求值的真/假结果。例如,如果x为真,则兑换脚本为A,否则ELSE兑换脚本为B。

此外,比特币条件表达式可以无限期地“嵌套”,这意味着一个条件语句可以包含另外一个条件语句,其中又会包含别的条件语句等等 。比特币脚本流程控制可用于构造非常复杂的脚本,可以有数百甚至数千个可能的执行路径。嵌套没有限制,但共识规则对脚本的最大字节数有限制。

比特币使用IF,ELSE,ENDIF和NOTIF操作码实现流程控制。此外,条件表达式可以包含布尔运算符,如BOOLAND,BOOLOR和NOT。

乍看之下,您可能会发现比特币的流程控制脚本令人困惑。那是因为比特币脚本是一种堆栈语言。正如当1+1表示为1 1 ADD时看起来是“逆向”的,比特币中的流程控制语句也看起来是“逆向”的。

在大多数传统(过程)编程语言中,流程控制如下所示:

大多数编程语言中的流控制伪代码

if (condition):
  code to run when condition is true
else:
  code to run when condition is false
code to run in either case
1
2
3
4
5

在基于堆栈的语言中,比如比特币脚本,逻辑条件出现在IF之前,看起来像是“逆向”的,如下所示:

Bitcoin脚本流程控制

 condition
IF
  code to run when condition is true
ELSE
  code to run when condition is false
ENDIF
code to run in either case 
1
2
3
4
5
6
7

阅读Bitcoin脚本时,请记住,条件语句在IF操作码的前面

# 7.6.2 VERIFY操作码条件语句

比特币脚本中的另一种条件形式是操作码以VERIFY结尾。 VERIFY后缀表示如果评估的条件不为TRUE,脚本的执行将立即终止,并且该交易被视为无效。

与提供可选执行路径的IF子句不同,VERIFY后缀充当保护子句,只有在满足前提条件时才会继续执行。

例如,以下脚本需要Bob的签名和产生特定哈希的原像(密钥)。 这两个条件必须都满足才能解锁:

有EQUALVERIFY保护子句的兑换脚本。

HASH160 <expected hash> EQUALVERIFY <Bob's Pubkey> CHECKSIG
1

为了兑换成功,Bob必须构建一个解锁脚本,提供有效的原像和签名:

满足上述兑换脚本的解锁脚本

<Bob's Sig> <hash pre-image>
1

没有原像,Bob无法继续执行到检查其签名的脚本部分。

该脚本可以用IF编写:

具有IF保护语句的兑换脚本

HASH160 <expected hash> EQUAL
IF
   <Bob's Pubkey> CHECKSIG
ENDIF
1
2
3
4

Bob的解锁脚本是一样的:

满足上述兑换脚本的解锁脚本以

<Bob's Sig> <hash pre-image>
1

使用IF的脚本与使用VERIFY后缀的操作码作用相同;,它们都可以作为保护语句。 但是,VERIFY的构造更有效率,少用了两个操作码。

那么,我们什么时候使用VERIFY,什么时候使用IF? 如果我们想要做的是附加一个前提条件(保护语句),那么VERIFY后缀更好。 然而,如果有不止一个执行路径(流程控制),那么IF ... ELSE流程控制语句更合适。

提示 诸如EQUAL之类的操作码会将结果(TRUE / FALSE)推送到堆栈上,留下它用于后续操作码的执行。 相比之下,操作码EQUALVERIFY后缀不会在堆栈上留下任何东西。 以VERIFY结尾的操作码都不会将结果留在堆栈上。

# 7.6.3 在脚本中使用流程控制

比特币脚本中流程控制的常见的用途是构建一个提供多个执行路径的兑换脚本,每个执行路径都是兑换UTXO的不同方式。

我们来看一个简单的例子,两个签名人,Alice和Bob,两人中任何一个都可以兑换。 使用多重签名,表示为1/2多重签名脚本。 为了演示,我们先使用IF语句执行相同的操作:

IF
 <Alice's Pubkey> CHECKSIG
ELSE
 <Bob's Pubkey> CHECKSIG
ENDIF
1
2
3
4
5

看到这个兑换脚本,你可能会想:“条件在哪里?IF语句前面什么也没有啊!”

条件并不是兑换脚本的一部分。 相反,条件是提供给解锁脚本,允许Alice和Bob“选择”他们想要的执行路径。

Alice用解锁脚本兑换:

<Alice's Sig> 1
1

最后的1作为条件(TRUE),使IF语句可以执行有Alice签名的第一个兑换路径。

如果是Bob兑换,他必须通过给IF语句赋一个FALSE值才能选择第二个执行路径:

<Bob's Sig> 0
1

Bob的解锁脚本将0放置在堆栈上,导致IF语句执行第二个(ELSE)脚本,该脚本需要Bob的签名。

由于可以嵌套IF语句,就可以创建一个执行路径的“迷宫”。 解锁脚本可以提供一个“映射”,选择实际执行的路径:

IF
    script A
ELSE
   IF
script B
  ELSE
script C
  ENDIF
ENDIF
1
2
3
4
5
6
7
8
9

在这种情况下,有三个执行路径(脚本A,脚本B和脚本C)。 解锁脚本以一系列TRUE或FALSE值的形式提供路径。 例如要选择路径脚本B,解锁脚本必须以1 0(TRUE,FALSE)结尾。 这些值将被推送到堆栈,第二个值(FALSE)首先停留在堆栈的顶部。 外部IF语句弹出FALSE值并执行第一个ELSE语句。 然后,TRUE值移动到堆栈的顶部,再通过内部(嵌套)的IF来执行,选择B执行路径。

使用这个结构,构造的兑换脚本就可以有数十或数百个执行路径,每个脚本提供了一种不同的方式来兑换UTXO。 花费时,构建一个解锁脚本,通过在每个流程控制点的堆栈上放置相应的TRUE和FALSE值来指引执行路径。

# 7.7 复杂的脚本示例

在本节中,我们将本章中的许多概念合并成一个例子。

我们的例子使用了迪拜一家公司所有者Mohammed的故事,他们主营进出口业务。

在这个例子中,Mohammed希望用灵活的规则建立公司资本账户。他创建的方案需要使用时间锁设置不同级别的授权。 多重签名计划的参与者是Mohammed,和他的两个合伙人Saeed和Zaira,以及他们的公司律师Abdul。三个合伙人根据多数规则作出决定,也就是三人中的两人必须同意才可以。然而,如果他们的密钥出现问题,他们希望他们的律师能够用三个合伙人中任何一人的签名收回资金。最后,如果所有的合伙人一段时间临时都联系不上或不能工作,他们希望律师能够直接接管该帐户。

这是Mohammed设计的实现上述目的脚本(每一行前面的数字是行号):

具有时间锁的可变多重签名

01  IF
02    IF
03      2
04    ELSE
05      <30 days> CHECKSEQUENCEVERIFY DROP
06      <Abdul the Lawyer's Pubkey> CHECKSIGVERIFY
07      1
08    ENDIF
09    <Mohammed's Pubkey> <Saeed's Pubkey> <Zaira's Pubkey> 3 CHECKMULTISIG
10  ELSE
11    <90 days> CHECKSEQUENCEVERIFY DROP
12    <Abdul the Lawyer's Pubkey> CHECKSIG
13  ENDIF
1
2
3
4
5
6
7
8
9
10
11
12
13

Mohammed的脚本使用嵌套的IF ... ELSE流程控制语句实现三个执行路径。

在第一个执行路径中,该脚本是三个合伙人的简单的2/3多重签名。该执行路径由第3行和第9行组成。第3行将多重签名的法定人数设置为2(2/3)。 通过在解锁脚本的末尾设置TRUE TRUE来选择该脚本:

第一个执行路径的解锁脚本(2/3 多签)

0 <Mohammed's Sig> <Zaira's Sig> TRUE TRUE
1

提示 此解锁脚本开头的0是因为CHECKMULTISIG中的一个错误,会从堆栈中多弹出一个额外的值。 CHECKMULTISIG会忽略这个额外的值,但它必须存在,否则脚本执行将失败。 推送0(通常)是解决bug的方法,如【7.2 多重签名CHECKMULTISIG执行中的bug】 (opens new window)所述。

第二个执行路径只能在UTXO创建30天后才能使用。 此时,它需要Abdul(律师)和三个合伙人之一(1/3)的签名。 这是通过第7行实现的,该行将多签的法定人数设置为1。要选择此执行路径,解锁脚本将以FALSE TRUE结束:

第二个执行路径的解锁脚本(律师 + 1/3)

0 <Saeed's Sig> <Abdul's Sig> FALSE TRUE
1

提示 为什么先FALSE后TRUE? 反了吗?是这两个值被推到堆栈的顺序,先推FALSE,后推 TRUE。 因此,第一个IF操作码首先弹出的是TRUE。

最后,第三个执行路径允许律师单独花费资金,但只能在90天之后。 要选择此执行路径,解锁脚本必须以FALSE结束:

第三个执行路径的解锁脚本(仅适用于律师)

<Abdul's Sig> FALSE
1

在纸上运行脚本来查看它在堆栈上的行为。

阅读这个例子还需要考虑几件事情。 看看你能不能找到答案?

  • 为什么律师不能通过在解锁脚本上选择FALSE,随时兑换第三条执行路径?
  • 在UTXO挖出后,5天、35天和105天分别可以使用多少条执行路径?
  • 如果律师失去密钥,资金是否丢失? 如果91天过去了,你的答案是否会改变?
  • 合伙人如何每隔29天或89天“重置”时钟,以防止律师获取资金?
  • 为什么这个脚本中的一些CHECKSIG操作码有VERIFY后缀,而其他的没有?

#