精通比特币(第二版)

 主页   资讯   文章   代码   电子书 

第七章 高级交易和脚本

7.1 介绍

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

首先,我们将了解多重签名multisignature脚本。接下来,我们将查看第二种最常见的交易脚本支付脚本哈希Pay-to-Script-Hash,它开启了复杂脚本的整个世界。然后,查看新的脚本操作符,能够通过时间锁timelocks给比特币添加时间维度。最后,我们将研究隔离见证Segregated Witness,这是对交易结构的架构更改。

7.2 多重签名

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

目前,标准多重签名脚本限制在最多3个公钥,这意味着可以执行从1/1到3/3或这个范围内的任意组合的多重签名。在本书出版时,可能会取消对3个公钥的限制,建议检查issstandard()函数,查看网络当前接受的值。注意,3个公钥限制只应用于标准多重签名脚本,不适用于P2SH脚本包装的多重签名脚本。P2SH多重签名脚本限制为15个密钥,允许最多15/15多重签名。我们将在【7.3 P2SH(Pay-to-Script-Hash)】学习P2SH。

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

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

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

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

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

<Signature B> <Signature C>

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

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

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

执行时,仅当解锁脚本与锁定脚本设置的条件匹配时,此组合脚本的评估结果才为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

首先,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

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

<Signature B> <Signature C>

而是:

0 <Signature B> <Signature C>

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

7.3 P2SH(Pay-to-Script-Hash)

支付脚本哈希P2SH是2012年推出的一种功能强大的新型交易,它大大简化了复杂交易脚本的使用。为了解释P2SH的必要性,让我们看一个实际的例子。

【第1章 比特币介绍】中,我们曾介绍过迪拜的电子产品进口商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

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

P2SH正是为了解决这一实际难题而被引入的,它使复杂脚本的使用能与直接向比特币地址支付一样简单。使用P2SH支付,复杂的锁定脚本被其电子指纹(加密哈希)所取代。当随后出现的一笔交易试图花费这个UTXO时,除了解锁脚本外,它还必须包含与哈希匹配的脚本。简单地说,P2SH意味着“支付给匹配这个哈希的脚本,这个脚本将在以后花费这个输出时呈现。”

在P2SH交易中,锁定脚本被哈希值取代,称为兑换脚本redeem script,因为它在兑换时提交给系统,而不是作为锁定脚本。表7-1显示了不带P2SH的脚本,表7-2显示用P2SH编码的相同脚本。

表7-1 不含P2SH的复杂脚本

Locking Script 2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 CHECKMULTISIG
Unlocking Script Sig1 Sig2

表7-2 P2SH复杂脚本

Redeem Script 2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 CHECKMULTISIG
Locking Script HASH160 <20-byte hash of redeem script> EQUAL
Unlocking Script Sig1 Sig2

从表中可以看出,对于P2SH,详细描述了花费输出条件的复杂脚本(兑换脚本)不会在锁定脚本中显示。相反,兑换脚本本身,锁定脚本中只出现了它的哈希值,在以后花费输出时才作为解锁脚本的一部分出现。这会把费用和复杂性的负担从交易发送者转移给接收者(承担支出)。

让我们再看下Mohammed公司的复杂的多重签名脚本和相应的P2SH脚本。

首先,看一下Mohammed公司使用的多重签名脚本,用于来自客户的所有付款:

2 <Mohammed's Public Key> <Partner1 Public Key> <Partner2 Public Key> <Partner3 Public Key> <Attorney Public Key> 5 CHECKMULTISIG

如果占位符由实际的公钥(以04开头的520字节)替代,你会看到脚本非常长:

2
04C16B8698A9ABF84250A7C3EA7EEDEF9897D1C8C6ADF47F06CF73370D74DCCA01CDCA79DCC5C395D7EEC6984D83F1F50C900A24DD47F569FD4193AF5DE762C58704A2192968D8655D6A935BEAF2CA23E3FB87A3495E7AF308EDF08DAC3C1FCBFC2C75B4B0F4D0B1B70CD2423657738C0C2B1D5CE65C97D78D0E34224858008E8B49047E63248B75DB7379BE9CDA8CE5751D16485F431E46117B9D0C1837C9D5737812F393DA7D4420D7E1A9162F0279CFC10F1E8E8F3020DECDBC3C0DD389D99779650421D65CBD7149B255382ED7F78E946580657EE6FDA162A187543A9D85BAAA93A4AB3A8F044DADA618D087227440645ABE8A35DA8C5B73997AD343BE5C2AFD94A5043752580AFA1ECED3C68D446BCAB69AC0BA7DF50D56231BE0AABF1FDEEC78A6A45E394BA29A1EDF518C022DD618DA774D207D137AAB59E0B000EB7ED238F4D800 5 CHECKMULTISIG

整个脚本都可由仅为20个字节的加密哈希所取代,首先采用SH256哈希算法,再对结果运用RIPEMD160算法。

命令行界面使用libbitcoin-explorer (bx)产生下面的脚本哈希:

echo \
2 \
[04C16B8698A9ABF84250A7C3EA7EEDEF9897D1C8C6ADF47F06CF73370D74DCCA01CDCA79DCC5C395D7EEC6984D83F1F50C900A24DD47F569FD4193AF5DE762C587] \
[04A2192968D8655D6A935BEAF2CA23E3FB87A3495E7AF308EDF08DAC3C1FCBFC2C75B4B0F4D0B1B70CD2423657738C0C2B1D5CE65C97D78D0E34224858008E8B49] \
[047E63248B75DB7379BE9CDA8CE5751D16485F431E46117B9D0C1837C9D5737812F393DA7D4420D7E1A9162F0279CFC10F1E8E8F3020DECDBC3C0DD389D9977965] \
[0421D65CBD7149B255382ED7F78E946580657EE6FDA162A187543A9D85BAAA93A4AB3A8F044DADA618D087227440645ABE8A35DA8C5B73997AD343BE5C2AFD94A5] \
[043752580AFA1ECED3C68D446BCAB69AC0BA7DF50D56231BE0AABF1FDEEC78A6A45E394BA29A1EDF518C022DD618DA774D207D137AAB59E0B000EB7ED238F4D800] \
5 CHECKMULTISIG \
| bx script-encode | bx sha256 | bx ripemd160
54c557e07dde5bb6cb791c7a540e0a4796f5e97e

上面的一系列命令首先将Mohammed的多签兑换脚本编码为一个序列化的十六进制编码比特币脚本。下面的bx命令使用RIPEMD160再次哈希,生成最终的脚本哈希:

Mohammed的兑换脚本20字节哈希:

 54c557e07dde5bb6cb791c7a540e0a4796f5e97e 

一笔P2SH交易把输出锁定在这个哈希,而不是那个特别长的兑换脚本。使用的锁定脚本为:

HASH160 54c557e07dde5bb6cb791c7a540e0a4796f5e97e EQUAL

正如你所看到的,这个脚本简短多了。P2SH交易等同于“支付给包含该哈希的脚本”,而不是“支付给5个多重签名脚本”。客户在向Mohammed公司支付时,只需在其支付指令中纳入这个非常简短的锁定脚本即可。当 Mohammed和他的合伙人想要花费这笔UTXO时,附上原始兑换脚本(他们的哈希锁定到UTXO的那个脚本)和必要的解锁签名即可,如:

<Sig1> <Sig2> <2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG>

两个脚本经由两步实现组合。 首先,将兑换脚本与锁定脚本比对以确认其与哈希是否匹配:

<2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG> HASH160 <redeem scriptHash> EQUAL

假如兑换脚本哈希匹配,解锁脚本自行执行以解锁兑换脚本:

<Sig1> <Sig2> 2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG

本章中描述的几乎所有脚本只能以P2SH脚本来实现。 它们不能直接用在UTXO的锁定脚本中。

7.3.1 P2SH地址

P2SH的另一重要特征是它能将脚本哈希编码为一个地址,正如BIP-13中所定义的。P2SH地址是采用Base58Check对20个字节哈希的脚本进行编码,就像比特币地址是公钥20字节哈希的Base58Check编码一样。由于P2SH地址采用5作为前缀,这导致基于Base58编码的地址以“3”开头。

例如,Mohammed的复杂脚本,Base58Check编码后的P2SH地址为“39RF6JqABiHdYHkfChV6USGMe6Nsr66Gzw”。我们可以通过bx命令确认:

echo \
'54c557e07dde5bb6cb791c7a540e0a4796f5e97e'\
 | bx address-encode -v 5
39RF6JqABiHdYHkfChV6USGMe6Nsr66Gzw

现在,Mohammed可以把这个地址发送给他的客户,他们可以采用任意比特币钱包进行简单支付,就像是比特币地址一样。前缀“3”暗示客户这是一种特殊类型的地址,一种对应于脚本而不是公钥的地址,但它作为付款方式与比特币地址完全相同。

P2SH地址隐藏了所有的复杂性,因此,运用其进行支付的人根本看不到脚本。

7.3.2 P2SH的优点

与在锁定输出中直接使用复杂脚本相比,P2SH特性提供了以下好处:

  • 在交易输出中,复杂脚本由简短电子指纹取代,使得交易代码变短。
  • 脚本被编译为地址,支付发送者及其比特币钱包都不需要复杂工程就可以执行P2SH。
  • P2SH将构建脚本的负担转移至接收方,而非发送者。
  • P2SH将长脚本数据的存储负担从输出方(既存储在区块链,又存储在UTXO集),转移至输入方(只存储在区块链中)。
  • P2SH将长脚本数据的存储负担从当前(支付时)转移至未来(花费时)。
  • P2SH将长脚本的交易费成本从发送方转移至接收方,接收方必须包含长的赎兑换脚本才能使用该笔资金。

7.3.3 兑换脚本和验证

在0.9.2版Bitcoin Core客户端之前,P2SH通过IsStandard(),仅限于标准类型的比特币交易脚本。这也意味着花费交易中的兑换脚本只能是标准化的P2PK、P2PKH或者多重签名。

0.9.2版的Bitcoin Core客户端,P2SH交易能包含任意有效的脚本,这使得P2SH标准更为灵活,可以用于多种新的或复杂类型的交易进行实验。

请记住不能将P2SH植入P2SH兑换脚本,因为P2SH规范不是递归的。虽然在技术上可以将RETURN【7.4 数据记录输出(RETURN操作符)】包含在兑换脚本中,规则中也未阻止此这样操作,但这没有实际用途,因为在验证期间执行RETURN将导致交易被标记为无效。

需要注意的是,因为在尝试使用P2SH输出之前,兑换脚本不会呈现给网络,因此,如果使用无效兑换脚本的哈希锁定输出,则不管如何都会进行处理。该UTXO将会被成功锁定,但是你将不能使用该笔资金,包含兑换脚本的花费交易也不被接受,因为该脚本是无效的。这样就会产生风险,你把比特币锁定在永不能花费的P2SH中。比特币网络本身会接受这个P2SH锁定脚本,即便它对应的是无效的兑换脚本,因为脚本哈希没有给出它所表示的脚本的含义。

注释 P2SH锁定脚本包含一个兑换脚本哈希,其中不包括该兑换脚本本身的任何线索。即便在兑换脚本无效的情况下,P2SH交易也会被认为有效并被接受。你可能会意外地锁死比特币,以后再也无法使用它。

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

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

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

在0.9版的Bitcoin Core客户端上,通过采用RETURN 操作符最终实现了妥协。RETURN 允许开发者在交易输出上增加80字节的非支付数据。然后,与伪UTXO不同,RETURN 创造了一种明确的可验证不可消费型输出,此类数据无需存储于UTXO集。RETURN输出被记录在区块链上,它们会消耗磁盘空间,也会导致区块链规模的增加,但它们不存储在UTXO集中,因此也不会使得UTXO内存池膨胀,更不会增加全节点昂贵的内存代价。

RETURN 脚本的样式:

  RETURN <data>

“data”部分被限制为80字节,且多表示为哈希值,如源自SHA256算法输出的值(32字节)。许多应用都在其前面加上前缀以方便识别。例如, 这家网站Proof of Existence 的数字公证服务使用8字节前缀DOCTIOND,16进制ASCII编码为44×4F 43 50 50 4F 4F 46。

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

一笔标准交易(通过了 isStandard() 函数检验的)只能有一个 RETURN 输出。但是单个RETURN 输出能与任意类型的输出交易进行组合。

Bitcoin Core 0.10版本添加了两个新的命令行选项。 选项datacarrier控制RETURN交易的传播和挖矿,默认设置为“1”以允许它们。 选项datacarriersize采用一个数字参数,指定RETURN脚本的最大大小(以字节为单位),默认为83字节,允许最多80个字节的RETURN数据加上一个字节的RETURN操作码和两个字节的PUSHDATA操作码。

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

7.5 时间锁(Timelocks)

时间锁是对交易或输出的限制,只允许在一个时间点之后才能消费。比特币从一开始就有一个交易级时间锁定功能,它由交易中的nLocktime字段实现。在2015年底和2016年中期推出了两个新的时间锁功能,提供UTXO级别的时间锁功能,这就是CHECKLOCKTIMEVERIFY和CHECKSEQUENCEVERIFY。

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

7.5.1 交易锁定时间(nLocktime)

比特币从一开始就有一个交易级的时间锁功能。交易锁定时间是交易级设置(交易数据结构中的一个字段),它定义了交易有效,可以在网络上传播或添加到区块链的最早时间。锁定时间也称为nLocktime,是来自于Bitcoin Core代码库中使用的变量名称。在大多数交易中将其设置为零,表示立即传播和执行。如果nLocktime不为零,低于5亿,则将其解释为区块高度,这意味着交易在指定的区块高度之前无效,并且不被传播,也不被包含在区块链中。如果大于或等于5亿,它被解释为Unix纪元时间戳(自1-1-1970之后的秒数),并且交易在指定时间之前无效。指定未来区块或时间的nLocktime交易必须由发起系统持有,并且只有在有效后才被发送到比特币网络。如果交易在指定的nLocktime之前传输到网络,那么第一个节点就会拒绝该交易,并且不会传播到其他节点。使用nLocktime等同于一张延期支票。

7.5.1.1 交易时间锁限制

nLocktime就是一个限制,虽然将来有可能花费这些输出,但是到指定时间为止,还不能说不能花费它们。我们用下面的例子解释一下。

Alice签署了一笔交易,支付给Bob的地址,并将交易nLocktime设定为未来的3个月。Alice把这笔交易发送给Bob搁置起来。有了这个交易,Alice和Bob知道:

  • 在3个月过去之前,Bob不能完成交易进行兑换。
  • Bob可以在3个月后接受交易。

然而:

  • Alice可以创建另一笔交易,不设置时间锁,花费上面两倍的输入。 这样,Alice就可以在3个月过去之前花费相同的UTXO。
  • Bob不能保证Alice不会这样做。

了解交易nLocktime的限制很重要。 唯一的保证是Bob在3个月过去之前无法兑换它,却无法保证Bob最终是否可以得到资金。 为了实现这样的保证,时间锁限制必须放在UTXO上,成为锁定脚本的一部分,而不是交易的一部分。 这是通过另一种形式的时间锁来实现的,称为检查锁定时间验证(CLTV)。

7.5.2 检查锁定时间验证Check Lock Time Verify (CLTV)

2015年12月,通过比特币软分叉升级引入了一种新形式的时间锁。根据BIP-65中的规范,脚本语言中添加了一个名为CHECKLOCKTIMEVERIFY(CLTV)的新脚本操作符。 CLTV是每个输出的时间锁,而不是像nLocktime一样是每个交易的时间锁。这使得在应用时间锁的方式上具有更大的灵活性。

简单地说,通过在输出的兑换脚本中添加CLTV操作码,限制了输出,因此只能在指定的时间过后花费。

提示 nLocktime是交易级别时间锁,而CLTV是基于输出的时间锁。

CLTV不替换nLocktime,而是限制特定的UTXO,使它们只能在大于或等于nLocktime设置的值的将来交易中使用。

CLTV操作码采用一个参数作为输入,为与nLocktime相同格式的数字(区块高度或Unix纪元时间)。如VERIFY后缀所示,CLTV是在结果为false时停止脚本执行的操作码类型。如果结果为TRUE,则继续执行。

为了使用CLTV来锁定输出,必须将其插入到创建输出交易的输出兑换脚本中。例如,如果Alice支付Bob的地址,输出通常会包含如下P2PKH脚本:

DUP HASH160 <Bob's Public Key Hash> EQUALVERIFY CHECKSIG

要锁定一段时间,比如说3个月以后,交易将是一个包含兑换脚本的P2SH交易:

<now + 3 months> CHECKLOCKTIMEVERIFY DROP DUP HASH160 <Bob's Public Key Hash> EQUALVERIFY CHECKSIG

其中<now + months>是从交易开始被挖矿时间起估计3个月的区块高度或时间值:当前块高度+12,960(块)或当前Unix纪元时间+7,760,000(秒)。现在,不要担心CHECKLOCKTIMEVERIFY之后的DROP操作码,下面很快就会解释。

当Bob尝试花费这个UTXO时,他构建了一个引用UTXO作为输入的交易。他在该输入的解锁脚本中使用了他的签名和公钥,并将交易nLocktime设置为等于或大于Alice设置的CHECKLOCKTIMEVERIFY 时间锁。然后,Bob把这笔交易广播到比特币网络上。

Bob的交易评估如下。如果Alice设置的CHECKLOCKTIMEVERIFY参数小于或等于支出交易的nLocktime,脚本执行将继续(就好像执行“无操作”或NOP操作码一样)。否则,脚本执行停止,并且该交易被视为无效。

更确切地说,CHECKLOCKTIMEVERIFY失败并停止执行,标记交易无效(来自:BIP-65):

  1. 堆栈是空的要么
  2. 堆栈中的顶部项小于0;要么
  3. 顶层堆栈项和nLocktime字段的锁定时间类型(高度或者时间戳)不相同;要么
  4. 顶层堆栈项大于交易的nLocktime字段;要么
  5. 输入的nSequence字段为0xffffffff。

注释 CLTV和nLocktime描述时间锁必须使用相同的格式,无论是区块高度还是自Unix纪元以来经过的秒数。 最重要的是,在一起使用时,nLocktime的格式必须与输出中的CLTV格式相匹配,它们必须是区块高度或秒数时间。

执行后,如果满足CLTV的要求,则它前面的时间参数将作为堆栈上的顶部项保留,并且可能需要随DROP一起删除,以便正确执行后续脚本操作码。 经常在脚本中看到CHECKLOCKTIMEVERIFY后面跟着DROP就是这个原因。

通过将nLocktime与CLTV结合使用,【7.5.1.1交易时间锁限制】中描述的情况就发生了变化。 Alice就不能再花这笔钱了(因为它被Bob的密钥锁定了),Bob也不能在3个月的锁定期终止前花掉它。

通过将时间锁功能直接引入到脚本语言中,CLTV允许我们开发一些非常有趣的复杂脚本。

该标准在BIP-65(CHECKLOCKTIMEVERIFY)中定义(附录BIP-65)。

7.5.3 相对时间锁

nLocktime和CLTV都是绝对时间锁absolute timelocks,它们指定绝对时间点。接下来我们研究的两个时间锁功能,是相对时间锁relative timelocks,它们设置的花费输出的条件为从区块链中的输出被确认开始所经过的时间。

相对时间锁是有用的,因为它们允许两个或多个相互依赖的交易链被下链处理,同时对一个交易施加时间限制,该时间限制依赖于其前一个交易从被确认开始经过的时间。换句话说,只有这个UTXO被记录在区块链,时钟才开始计数。这个功能在双向状态通道和闪电网络中特别有用,我们将在后面章节【2.6支付通道和状态通道】中看到。

相对时间锁和绝对时间锁一样,都是通过交易级特性和脚本级操作码实现的。交易级相对时间锁是通过设置作为共识规则的nSequence的值实现的,它是每个交易输入中都有设置的交易字段。脚本级相对时间锁使用CHECKSEQUENCEVERIFY(CSV)操作码实现。

相对时间锁是根据【BIP-68】【BIP-112】的规范共同实现的,其中BIP-68通过与相对时间锁运用一致性增强的数字序列实现,BIP-112中是运用到了CHECKSEQUENCEVERIFY这个操作码实现。

BIP-68和BIP-112是在2016年5月作为软分叉升级时被激活的一个共识规则。

7.5.4 nSequence相对时间锁

相对时间锁可以设置在每个交易输入中,方法是设置每个输入中的nSequence字段。

7.5.4.1 nSequence的本义

nSequence字段的最初设计是想在内存中修改交易(但是从未运用过)。这种情况下,一笔交易的输入包含的nSequence值低于232-1(0xffffffff),就表示该交易尚未“完成”。这样的交易将一直保留在内存池中,直到被花费相同输入,具有更高nSequence值的的另一笔交易代替。一旦收到一笔交易,其nSequence值为0xFFFFFFFF,那么它将被视为“完成”并开始挖矿。

nSequence的最初设计从未被正确实现,在不使用时间锁的交易中,nSequence的值通常设置为0xFFFFFFFF。对于具有nLocktime或CHECKLOCKTIMEVERIFY的交易,nSequence值必须设置为小于231,以使时间锁保护有效,如下面所述。

7.5.4.2 nSequence作为一个共同执行的相对时间锁定

由于BIP-68的激活,新的共识规则适用于任何包含nSequence值小于2^31的输入的交易(bit 1<<31 is not set)。以编程方式,这意味着如果没有设置最高有效(bit 1<<31),它是一个表示“相对锁定时间”的标志。否则(bit 1<<31set),nSequence值被保留用于其他用途,例如启用CHECKLOCKTIMEVERIFY,nLocktime,Opt-In-Replace-By-Fee以及其他未来的新产品。

一笔输入交易,当输入脚本中的nSequence值小于2^31时,就是相对时间锁定的输入交易。这种交易只有到了相对锁定时间后才生效。例如,具有30个区块的nSequence相对时间锁的一个输入的交易只有在从输入中引用的UTXO开始的时间起至少有30个块时才有效。由于nSequence是每个输入字段,因此交易可能包含任何数量的时间锁定输入,所有这些都必须具有足够的时间以使交易有效。

交易可以包括时间锁定输入(nSequence <2^31)和没有相对时间锁定(nSequence> = 2^31)的输入。 nSequence值以块或秒为单位指定,但与nLocktime中使用的格式略有不同。类型标志用于区分计数块和计数时间(以秒为单位)的值。类型标志设置在第23个最低有效位(即值1 << 22)。如果设置了类型标志,则nSequence值将被解释为512秒的倍数。如果未设置类型标志,则nSequence值被解释为块数。

当将nSequence解释为相对时间锁定时,只考虑16个最低有效位。一旦评估了标志(位32和23),nSequence值通常用16位掩码(例如nSequence&0x0000FFFF)“屏蔽”。 下图显示由BIP-68定义的nSequence值的二进制布局。 BIP-68 definition of nSequence encoding Figure 1. BIP-68 definition of nSequence encoding (Source: BIP-68)

基于nSequence值的一致执行的相对时间锁定在BIP-68中。标准定义在BIP-68, Relative lock-time using consensus-enforced sequence numbers.

7.5.5 带CSV的相对时间锁

就像CLTV和nLocktime一样,有一个脚本操作码用于相对时间锁定,它利用脚本中的nSequence值。该操作码是CHECKSEQUENCEVERIFY,通常简称为CSV。 在UTXO的赎回脚本中评估时,CSV操作码仅允许在输入nSequence值大于或等于CSV参数的交易中进行消耗。实质上,这限制了UTXO的消耗,直到UTXO开采时间过了一定数量的块或秒。

与CLTV一样,CSV中的值必须与相应nSequence值中的格式相匹配。如果CSV是根据块指定的,那么nSequence也是如此。如果以秒为单位指定CSV,那么nSequence也是如此。

当几个(已经形成链)交易被保留为“脱链”时,创建和签名这几个(已经形成链)交易但不传播时,CSV的相对时间锁特别有用。在父交易已被传播,直到消耗完相对锁定时间,才能使用子交易。这个用例的一个应用可以在state_channelslightning_network/请加上链接 章节中看到。 CSV 细节参见 BIP-112, CHECKSEQUENCEVERIFY.

7.5.6中位时间过去Median-Time-Past

作为激活相对时间锁定的一部分,时间锁定(绝对和相对)的“时间”方式也发生了变化。在比特币中,墙上时间(wall time)和共识时间之间存在微妙但非常显著的差异。比特币是一个分散的网络,这意味着每个参与者都有自己的时间观。网络上的事件不会随时随地发生。网络延迟必须考虑到每个节点的角度。最终,所有内容都被同步,以创建一个共同的分类帐。比特币在过去存在的分类账状态中每10分钟达成一个新的共识。

区块头中设置的时间戳由矿工设定。共识规则允许一定的误差来解决分散节点之间时钟精度的问题。然而,这诱惑了矿工去说谎,以便通过包括还不在范围内的时间交易来赚取额外矿工费。有关详细信息,请参阅以下部分。

为了杜绝矿工说谎,加强时间安全性,在相对时间锁的基础上又新增了一个BIP。这是BIP-113,它定义了一个称为“中位时间过去 /(Median-Time-Past)”的新的共识测量机制。通过取最后11个块的时间戳并计算其中位数作为“中位时间过去”的值。这个中间时间值就变成了共识时间,并被用于所有的时间计算。过去约两个小时的中间点,任何一个块的时间戳的影响减小了。通过这个方法,没有一个矿工可以利用时间戳从具有尚未成熟的时间段的交易中获取非法矿工费。

Median-Time-Past更改了nLocktime,CLTV,nSequence和CSV的时间计算的实现。由Median-Time-Past计算的共识时间总是大约在挂钟时间后一个小时。如果创建时间锁交易,那么要在nLocktime,nSequence,CLTV和CSV中进行编码的估计所需值时,应该考虑它。 Median-Time-Past细节参见BIP-113.

7.5.7针对费用狙击(Fee Sniping)的时间锁定

费用狙击是一种理论攻击情形,矿工试图从将来的块(挑选手续费较高的交易)重写过去的块,实现“狙击”更高费用的交易,以最大限度地提高盈利能力。

例如,假设存在的最高块是块#100,000。如果不是试图把#100,001号的矿区扩大到区块链,那么一些矿工们会试图重新挖矿#100,000。这些矿工可以选择在候选块#100,000中包括任何有效的交易(尚未开采)。他们不必使用相同的交易来恢复块。事实上,他们有动力选择最有利可图(最高每kBB)的交易来包含在其中。它们可以包括处于“旧”块#100,000中的任何交易,以及来自当前内存池的任何交易。当他们重新创建块#100,000时,他们本质上可以将交易从“现在”提取到重写的“过去”中。

今天,这种袭击并不是非常有利可图,因为回报奖励(因为包括一定数量的比特币奖励)远远高于每个区块的总费用。但在未来的某个时候,交易费将是奖励的大部分(甚至是奖励的整体)。那时候这种情况变得不可避免了。

为了防止“费用狙击”,当Bitcoin Core /钱包 创建交易时,默认情况下,它使用nLocktime将它们限制为“下一个块”。在我们的环境中,Bitcoin Core /钱包将在任何创建的交易上将nLocktime设置为100,001。在正常情况下,这个nLocktime没有任何效果 - 交易只能包含在#100,001块中,这是下一个区块。 但是在区块链分叉攻击的情况下,由于所有这些交易都将被时间锁阻止在#100,001,所以矿工们无法从筹码中提取高额交易。他们只能在当时有效的任何交易中重新挖矿#100,000,这导致实质上不会获得新的费用。 为了实现这一点,Bitcoin Core/钱包将所有新交易的nLocktime设置为<current block #+ 1>,并将所有输入上的nSequence设置为0xFFFFFFFE以启用nLocktime。

7.6具有流量控制的脚本(条件子句 (Conditional Clauses))

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

在基本层面上,比特币条件操作码允许我们构建一个具有两种解锁方式的赎回脚本,这取决于评估逻辑条件的TRUE / FALSE结果。例如,如果x为TRUE,则赎回脚本为A,ELSE赎回脚本为B. 此外,比特币条件表达式可以无限期地“嵌套”,这意味着这个条件语句可以包含其中的另外一个条件,另外一个条件其中包含别的条件等等 。Bitcoin脚本流控制可用于构造非常复杂的脚本,具有数百甚至数千个可能的执行路径。嵌套没有限制,但协商一致的规则对脚本的最大大小(以字节为单位)施加限制。

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

乍看之下,您可能会发现比特币的流量控制脚本令人困惑。那是因为比特币脚本是一种堆栈语言。同样的方式,当1+1看起来“向后”当表示为1 1 ADD时,比特币中的流控制条款也看起来“向后”(backward)。 在大多数传统(程序)编程语言中,流控制如下所示: 大多数编程语言中的流控制伪代码

if (condition):
  code to run when condition is true
else:
  code to run when condition is false
code to run in either case

在基于堆栈的语言中,比如比特币脚本,逻辑条件出现在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 

阅读Bitcoin脚本时,请记住,评估的条件是在IF操作码之前。

7.6.1带有VERIFY操作码的条件子句

比特币脚本中的另一种条件是任何以VERIFY结尾的操作码。 VERIFY后缀表示如果评估的条件不为TRUE,脚本的执行将立即终止,并且该交易被视为无效。 与提供替代执行路径的IF子句不同,VERIFY后缀充当保护子句,只有在满足前提条件的情况下才会继续。

例如,以下脚本需要Bob的签名和产生特定哈希的前图像(秘密地)。

解锁时必须满足这两个条件:

1)具有EQUALVERIFY保护子句的赎回脚本。

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

为了兑现这一点,Bob必须构建一个解锁脚本,提供有效的前图像和签名:

2)一个解锁脚本以满足上述赎回脚本。

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

没有前图像,Bob无法访问检查其签名的脚本部分。

该脚本可以用IF编写: 具有IF保护条款的兑换脚本

HASH160 <expected hash> EQUAL
IF
   <Bob's Pubkey> CHECKSIG
ENDIF

Bob的解锁脚本是一样的: 解锁脚本以满足上述兑换脚本

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

使用IF的脚本与使用具有VERIFY后缀的操作码相同; 他们都作为保护条款。 然而,VERIFY的构造更有效率,使用较少的操作码。

那么,我们什么时候使用VERIFY,什么时候使用IF? 如果我们想要做的是附加一个前提条件(保护条款),那么验证是更好的。 然而,如果我们想要有多个执行路径(流控制),那么我们需要一个IF ... ELSE流控制子句。

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

7.6.2在脚本中使用流控制

比特币脚本中流量控制的一个非常常见的用途是构建一个提供多个执行路径的赎回脚本,每个脚本都有一种不同的赎回UTXO的方式。

我们来看一个简单的例子,我们有两个签名人,Alice和Bob,两人中任何一个都可以兑换。 使用多重签名,这将被表示为1-of-2 多重签名脚本。 为了示范,我们将使用IF子句做同样的事情:

IF
 <Alice's Pubkey> CHECKSIG
ELSE
 <Bob's Pubkey> CHECKSIG
ENDIF

看这个赎回脚本,你可能会想:“条件在哪里?”IF子句之前没有什么!“ 条件不是赎回脚本的一部分。

相反,该解锁脚本将提供该条件,允许Alice和Bob“选择”他们想要的执行路径。

Alice用解锁脚本兑换了这个:

<Alice's Sig> 1

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

为了兑换这个Bob,他必须通过给IF子句赋一个FALSE值来选择第二个执行路径:

<Bob's Sig> 0

Bob的解锁脚本在堆栈中放置一个0,导致IF子句执行第二个(ELSE)脚本,这需要Bob的签名。

由于可以嵌套IF子句,所以我们可以创建一个“迷宫”的执行路径。 解锁脚本可以提供一个选择执行路径实际执行的“地图”:

IF
    script A
ELSE
   IF
script B
  ELSE
script C
  ENDIF
ENDIF

在这种情况下,有三个执行路径(脚本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设计的脚本: 具有时间锁定(Timelock)变量的多重签名

IF
  IF
    2
  ELSE
    <30 days> CHECKSEQUENCEVERIFY DROP
    <Abdul the Lawyer's Pubkey> CHECKSIGVERIFY
    1
  ENDIF
  <Mohammed's Pubkey> <Saeed's Pubkey> <Zaira's Pubkey> 3 CHECKMULTISIG
ELSE
  <90 days> CHECKSEQUENCEVERIFY DROP
  <Abdul the Lawyer's Pubkey> CHECKSIG
ENDIF

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

在第一个执行路径中,该脚本作为三个合作伙伴的简单的2-of-3 multisig操作。

该执行路径由第3行和第9行组成。第3行将multisig的定额设置为2(2 - 3)。

该执行路径可以通过在解锁脚本的末尾设置TRUE TRUE来选择: 解锁第一个执行路径的脚本(2-of-3 multisig)

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

提示 此解锁脚本开头的0是因为CHECKMULTISIG中的错误从堆栈中弹出一个额外的值。 额外的值被CHECKMULTISIG忽略,否则脚本签名将失败。 推送0(通常)是解决bug的方法,如CHECKMULTISIG执行中的错误章节所述。

第二个执行路径只能在UTXO创建30天后才能使用。 那时候,它需要签署Abdul(律师)和三个合作伙伴之一(三分之一)。

这是通过第7行实现的,该行将多选的法定人数设置为1。要选择此执行路径,解锁脚本将以FALSE TRUE结束: 解锁第二个执行路径的脚本(Lawyer + 1-of-3)

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

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

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

<Abdul's Sig> FALSE

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

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

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