第七章 高级交易和脚本

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个公钥的限制,建议检查 isStandard() 函数,查看网络当前接受的值。注意,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的复杂脚本

表7-2 P2SH复杂脚本

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

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

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

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

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

2
04C16B8698A9ABF84250A7C3EA7EEDEF9897D1C8C6ADF47F06CF73370D74DCCA01CDCA79DCC5C395D7EEC6984D83F1F50C900A24DD47F569FD4193AF5DE762C58704A2192968D8655D6A935BEAF2CA23E3FB87A3495E7AF308EDF08DAC3C1FCBFC2C75B4B0F4D0B1B70CD2423657738C0C2B1D5CE65C97D78D0E34224858008E8B49047E63248B75DB7379BE9CDA8CE5751D16485F431E46117B9D0C1837C9D5737812F393DA7D4420D7E1A9162F0279CFC10F1E8E8F3020DECDBC3C0DD389D99779650421D65CBD7149B255382ED7F78E946580657EE6FDA162A187543A9D85BAAA93A4AB3A8F044DADA618D087227440645ABE8A35DA8C5B73997AD343BE5C2AFD94A5043752580AFA1ECED3C68D446BCAB69AC0BA7DF50D56231BE0AABF1FDEEC78A6A45E394BA29A1EDF518C022DD618DA774D207D137AAB59E0B000EB7ED238F4D800 5 CHECKMULTISIG

整个脚本都可由仅为20个字节的加密哈希所取代,首先采用SHA256哈希算法,再对结果运用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 + 3 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被记录在区块链,时钟才开始计数。这个功能在双向状态通道和闪电网络中特别有用,我们将在后面章节【12.6支付通道和状态通道】中看到。

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

相对时间锁是根据【BIP-68】【BIP-112】的规范共同实现的,其中BIP-68描述的是基于nSequence值的共识执行的相对时间锁,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值小于231的任何交易( 1<<31位未设置为1)。从编程角度,如果没有设置最高位(1<<31位 )为1,意味着它是一个表示“相对锁定时间”的标志。否则( 设置了1<<31位为1),nSequence值就被保留用于其他用途,例如启用CHECKLOCKTIMEVERIFY,nLocktime,Opt-In-Replace-By-Fee以及其他未来的新开发功能。

交易的输入中的nSequence值小于231,就表示具有相对时间锁。这种交易中的输入只有相对锁定时间到期后才能有效。例如,一笔交易的输入的nSequence相对时间锁是30个区块,那么只有当输入引用的UTXO被挖出后再经过30个区块之后,该交易才有效。由于nSequence是每个输入中的字段,因此交易可能包含任何数量的时间锁定输入,这其中的每个输入都必须满足时间限制交易才能有效。交易中的输入可以是时间锁定输入(nSequence <231),也可以是没有相对时间锁定(nSequence> = 231)的输入。

nSequence值以块或秒为单位,但与nLocktime中使用的格式略有不同。类型(type)标志用于区分计数块和计数时间(以秒为单位)。类型标志设置在第23个最低有效位(即值1 << 22)。如果设置了类型标志,则nSequence值将被解释为512秒的倍数。如果未设置类型标志,则nSequence值被解释为区块数。

当将nSequence解释为相对时间锁时,只考虑16个最低有效位。一旦对标志(位32和23)求值,nSequence值通常用16位掩码(例如nSequence或者0x0000FFFF)进行“屏蔽”。

图7-1 BIP-68 中nSequence编码的定义(出处: BIP-68)

BIP-68规定了基于nSequence值的共识执行的相对时间锁。该标准参见BIP-68, Relative lock-time using consensus-enforced sequence numbers。.

7.5.5 带CSV的相对时间锁

就像CLTV和nLocktime一样,在脚本中有一个脚本操作码使用nSequence值作为相对时间锁。该操作码是CHECKSEQUENCEVERIFY,通常简称为CSV。

在UTXO的兑换脚本中执行时,CSV操作码仅允许花费交易中nSequence值大于或等于CSV参数的输入。实质上,这限制了未达一定数量的区块或秒数的UTXO的消费。

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

当创建和签署多个(链接的)交易,但不想对外广播,想让它们保持“链下”时,使用CSV的相对时间戳特别有用。在父交易按相对时间锁中指定的时间进行传播、被挖矿和老化之前,子交易是不能使用的。有一个应用案例可以在【12.6支付通道和状态通道】【12.7可路由的支付通道(闪电网络)】章节中看到。

CSV 细节参见 BIP-112, CHECKSEQUENCEVERIFY.

7.5.6 中位时间过去

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

区块头中的时间戳由矿工设定。共识规则允许一定的余地来解决去中心化节点之间时钟精度的问题。然而,这给矿商创造了一个不好的激励,他们在一个区块内谎报时间,通过纳入尚未到期的时间锁交易来赚取额外费用。有关详细信息,请参阅以下部分。

为了杜绝矿工说谎,加强时间锁的安全性,与相对时间锁同时激活的还有一个BIP。这就是BIP-113,它定义了一个称为中位时间过去Median-Time-Past的新的共识测量机制。

中位时间过去是通过读取最后11个块的时间戳并求出中位值来计算的。这个中位时间值就变成了共识时间,并被用于所有的时间锁计算。通过结合11个区块,没有一个矿工会为了获得尚未到期的时间戳的交易中的交易费,去影响时间戳。

中位时间过去改变了nLocktime,CLTV,nSequence和CSV的时间计算的实现。由中位时间过去计算的共识时间总是比现实时间晚大约一个小时。如果要创建时间锁交易,评估nLocktime,nSequence,CLTV和CSV中编码所需时间值时,应该考虑在这个因素。 中位时间过去参见【BIP-113】。

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

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

例如,假设存在的最高块是块#100,000。有些矿工现在不是试图对#100,001区块进行挖矿来延长区块链,而是试图重新挖矿#100,000区块。这些矿工选择在自己的候选块#100,000中包括任何有效的交易(还未挖出)。他们不必使用相同的交易来重挖区块。事实上,他们会更倾向于选择在其中添加最有利可图(每kB最高交易费)的交易。他们会纳入“旧”块#100,000中的任何交易,还有他们当前内存池的任何交易。当他们重新创建块#100,000时,本质上就把“现在”的交易拉出来重写到“过去”中。

今天,这种攻击还不是非常有利可图,因为挖矿奖励远远高于每个区块中的总费用。但在未来的某个时候,交易费将是奖励的大部分(甚至是奖励的整体),那时候这种情况就会很难避免了。

为了防止“费用狙击”,Bitcoin Core创建交易时,默认情况下,使用nLocktime将它们限制为“下一个区块”。在刚才的场景中,Bitcoin Core 会将其创建的任何交易的nLocktime设置为100001。在正常情况下,这个nLocktime不起作用-无论如何,这些交易只能包含在块100001中,就是下一个块。

但是在区块链分叉攻击时,由于所有这些交易都将被时间锁阻止在#100,001,所以矿工们无法从内存池中提取高收费交易,因为所有这些交易都将被时间锁定到区块100001中。他们只能在当时有效的交易中重新挖区块#100,000,实质上不会获得新的费用。

为了实现这一点,Bitcoin Core将所有新交易的nLocktime设置为<当前区块号+ 1>,并将所有输入上的nSequence设置为0xFFFFFFFE以启用nLocktime。

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

比特币脚本的一个更强大的功能是流程控制,也称为条件语句。您可能熟悉各种编程语言中的类似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

在基于堆栈的语言中,比如比特币脚本,逻辑条件出现在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的签名和产生特定哈希的原像(密钥)。 这两个条件必须都满足才能解锁:

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

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

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

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

<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? 如果我们想要做的是附加一个前提条件(保护语句),那么VERIFY后缀更好。 然而,如果有不止一个执行路径(流程控制),那么IF ... ELSE流程控制语句更合适。

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

7.6.2 在脚本中使用流程控制

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

我们来看一个简单的例子,两个签名人,Alice和Bob,两人中任何一个都可以兑换。 使用多重签名,表示为1/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设计的实现上述目的脚本(每一行前面的数字是行号):

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

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

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

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

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

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

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

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

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

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

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

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

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

<Abdul's Sig> FALSE

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

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

  • 为什么律师不能通过在解锁脚本上选择FALSE,随时兑换第三条执行路径?

  • 在UTXO挖出后,5天、35天和105天分别可以使用多少条执行路径?

  • 如果律师失去密钥,资金是否丢失? 如果91天过去了,你的答案是否会改变?

  • 合伙人如何每隔29天或89天“重置”时钟,以防止律师获取资金?

  • 为什么这个脚本中的一些CHECKSIG操作码有VERIFY后缀,而其他的没有?

7.8 隔离见证

隔离见证(segwit)是对比特币共识规则和网络协议的升级,是作为BIP-9软分叉被提议并实施的,并于2017年8月1日在比特币的主网上激活。

在密码学中,术语“见证”用于描述解决密码难题的方案。在比特币术语中,见证满足放置在未花费交易输出(UTXO)上的加密条件。

在比特币环境中,数字签名是见证的一种类型,但更广义来说,见证是指能够满足对UTXO施加的条件并解锁该UTXO以供消费的任何解决方案。术语“见证”是“解锁脚本”或“scriptSig”的更一般的术语。

在引入隔离见证之前,交易中的每一个输入后面紧跟着的就是解锁它的见证数据。见证数据作为每个输入的一部分嵌入到交易中。术语隔离见证(简称segwit)只是指把特定输出的签名或解锁脚本隔离开。“单独的scriptSig”或“单独的签名”就是它最简单的形式。

因此,隔离见证是对比特币的一种架构更改,旨在将见证数据从交易的scriptSig(解锁脚本)字段移动到伴随交易的独立的见证数据结构中。客户端要求的交易数据可以包括见证数据,也可以不包括。

在本节中,我们将讨论隔离见证的一些好处,描述用于部署和实现此体系结构更改的机制,并演示在交易和地址中如何使用隔离见证。

隔离见证由以下BIP定义:

BIP-141

隔离见证的主要定义。

BIP-143

0版本见证程序的交易签名验证

BIP-144

对等服务-新的网络消息和序列化格式

BIP-145

隔离见证的getblocktemplate更新(用于挖矿)

BIP-173

原生v0-16见证输出的Base32地址格式

7.8.1 为什么要用隔离见证

隔离见证是一种架构性更改,对比特币的可扩展性、安全性、经济激励和性能有多方面影响:

交易延展性

将见证数据从交易中移出,用作标识符的交易哈希就不再包括见证数据。由于见证数据是交易中唯一可由第三方修改的部分,请参阅【7.8.5.5 交易标识符】,删除它也就消除了交易延展性攻击的机会。有了隔离见证,交易哈希就不能被除了交易的创建者之外的任何人更改,这大大促进了许多依赖于高级比特币交易构造的其他协议的实现,如支付通道、链式交易和闪电网络。

脚本版本控制

随着隔离见证脚本的引入,每个锁定脚本前面都有一个脚本版本号,类似于交易和区块的版本号。添加脚本版本号允许以后向兼容的方式(例如使用软分叉升级)升级脚本语言,引入新的脚本操作对象、语法或语义。以无中断方式升级脚本语言的能力将大大加快比特币的创新速度。

网络和存储扩展

见证数据通常对交易的总大小有很大贡献。更复杂的脚本,如用于多签或支付通道的脚本体积会非常大。在某些情况下,这些脚本会占到交易数据的大多数(超过75%)。通过将见证数据从交易中移出,隔离见证提高了比特币的可扩展性。节点可以在验证签名后删减见证数据,或者在进行简化的支付验证时完全忽略见证数据。见证数据不再需要发送到所有节点,也不需要被所有节点存储在磁盘上。

签名验证优化

隔离见证升级签名函数(CHECKSIG,CHECKONSIGG等),以减少算法的计算复杂度。在引入隔离见证之前,用于生成签名的算法需要大量与交易大小成比例的哈希操作。相对于签名操作的数量数据哈希计算复杂度增加到O(n2),给验证签名的所有节点带来了巨大的计算负担。有了隔离见证,算法的复杂度降低到O(n)。

离线签名改进

隔离见证签名包含由签名的哈希中的每个输入引用的值(金额)。以前,离线签名设备(如硬件钱包)必须在签名交易之前验证每个输入的金额。这通常是通过流式传输大量先前作为输入引用的交易的数据来实现的。由于金额现在是已签名的提交哈希的一部分,因此离线设备不需要以前的交易。如果金额不匹配(由被入侵的在线系统篡改),签名将是无效的。

7.8.1 隔离见证如何工作

乍一看,隔离见证似乎只是对交易构造方式的改变,属于交易级特性,但事实并非如此。其实,隔离见证是对单独的UTXO花费方式的更改,因此可以说是每个输出层面的特性。

交易可以花费使用了隔离见证的输出或传统(内嵌见证)输出,或两者都有。因此,将交易称为“隔离见证交易”没有多大意义。相反,应该将特定的交易输出称为“隔离见证输出”。

当交易使用UTXO时,必须提供一个见证。在传统的UTXO中,锁定脚本要求见证数据内嵌在花费UTXO的交易的输入部分。但是,隔离见证UTXO指定了一个锁定脚本,该脚本可以用输入之外的见证数据(隔离)满足。

7.8.2 软分叉(后向兼容)

隔离见证是对输出和交易架构方式的重大变革。通常情况下,这种变革需要改变共识规则,要同时更换每个比特币节点和钱包,也就是硬分叉。但是,隔离见证引入的时候采用的是一个更少破坏性的变革,是后向兼容的,称为软分叉。这种类型的升级允许未升级的软件忽略更改并继续运行,而不会造成任何中断。

隔离见证输出被构造成未升级的旧系统仍然可以验证它们。对于旧的钱包或节点,隔离见证输出看起来和其他任何可以花费的输出没什么区别。这样的输出可以使用空签名花费,因此交易内部没有签名(它是隔离的)这一事实不会使交易无效。但是,新的钱包和挖矿节点会看到隔离见证输出,并希望在交易的见证数据中找到有效的见证。

7.8.3 隔离见证输出和交易示例

接下来看一些示例交易,看看它们在隔离见证下会发生什么变化。我们首先研究如何使用隔离见证程序改造Pay-to-Public-Key-Hash(P2PKH)支付。然后,再来研究隔离见证同样如何作用于Pay-to-Script-Hash(P2SH)脚本。最后,研究如何将前面两个隔离见证程序嵌入到P2SH脚本中。

7.8.3.1 Pay-to-Witness-Public-Key-Hash (P2WPKH)

【2.1.2 买一杯咖啡】中,Alice创建一笔交易,付给Bob一杯咖啡的费用。该笔交易构建了一个价值0.015BTC的 P2PKH 输出(Bob可用来花费),该输出脚本看起来像这样:

P2PKH 输出脚本示例:

DUP HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 EQUALVERIFY CHECKSIG 

如果通过隔离见证,Alice将会创建一个支付给见证公钥哈希Pay-to-Witness-Public-Key-Hash(P2WPKH)脚本,看起来是这样的:

P2WPKH 输出脚本示例:

0 ab68025513c3dbd2f7b92a94e0581f5d50f654e7

正如你所见,隔离见证输出的锁定脚本比传统输出简单得多。它包含两个值,会被推送到脚本计算堆栈中。对于一个传统(非隔离见证)比特币客户端来说,这两个推送值看起来像是一个任何人都能花费的输出,而不需要签名(或者更确切的说,能被空的签名使用)。而对一个更新的、隔离见证客户端来说,第一个数字(0)被解释为一个版本号(见证版本),第二部分(20字节)相当于一个锁定脚本,被称为见证程序 witness program。这20字节的见证程序就像是 P2PKH 脚本中的公钥哈希值一样。

现在,再看看Bob用来去花费这个输出对应的交易。对于原始脚本(非隔离见证),Bob的交易必须在交易输入中包含签名:

以下被解码的交易,显示了使用签名花费的 P2PKH 输出:

[...]
“Vin” : [
"txid": "0627052b6f28912f2703066a912ea577f2ce4da4caa5a5fbd8a57286c345c2f2",
"vout": 0,
      "scriptSig": “<Bob’s scriptSig>”,
]
[...]

但是,要花费隔离见证输出,这个交易输入中没有签名。相反,Bob的交易只有空的 scriptSig 和交易本身之外的隔离见证:

以下被解码的交易,显示了使用隔离见证数据花费的 P2WPKH 输出:

[...]
“Vin” : [
"txid": "0627052b6f28912f2703066a912ea577f2ce4da4caa5a5fbd8a57286c345c2f2","vout": 0,
        "scriptSig": “”,
]
[...]
“witness”: “<Bob’s witness data>”
[...]

7.8.3.2 钱包的P2WPKH 构造

尤其值得注意的是,P2WPKH 应该只能由收款人(接收方)创建,而不是由发送者从已知的公钥、P2PKH 脚本或地址进行转换。发送方无从知道接收者的钱包是否具有能力构建隔离见证交易,并消费 P2WPKH 输出。

另外,P2WPKH 输出必须从压缩公钥的哈希值中创建。未压缩公钥在隔离见证中是非标准的,可能会被将来的软分叉明确禁用。如果在 P2WPKH 中使用的哈希值来自未压缩公钥,那么它可能是不可消费的,资金就可能丢失。P2WPKH 输出应该由收款人的钱包,通过私钥派生的压缩公钥来创建。

警告 P2WPKH 应该由收款人(接收者)通过将压缩公钥转换成P2WPKH哈希值进行创建。绝对不要将P2PKH脚本、比特币地址或未压缩公钥转换成P2WPKH见证脚本。

7.8.3.3 Pay-to-Witness-Script-Hash (P2WSH)

第二种类型的验证程序对应“支付给脚本哈希”( Pay-to-Script-Hash , P2SH)脚本。我们在【7.3 P2SH(Pay-to-Script-Hash)】中见过这类脚本。例子中,Mohammed的公司使用P2SH 来表示一个多重签名脚本。对Mohammed’s的公司的支付被编码成一个这样的锁定脚本:

P2SH 输出脚本示例:

HASH160 54c557e07dde5bb6cb791c7a540e0a4796f5e97e EQUAL

这个P2SH脚本引用了一个兑换脚本的哈希值,该脚本定义了一个花费资金的“2/3”的多重签名要求。为了花费该输出,Mohammed的公司需提供兑现脚本(其哈希值与P2SH输出中的脚本哈希值匹配)和满足兑现脚本所必需的签名,所有这些都在交易输入中:

下面的解码交易显示要被花费的P2SH输出:

[...]
“Vin” : [
"txid": "abcdef12345...",
"vout": 0,
      "scriptSig": “<SigA> <SigB> <2 PubA PubB PubC PubD PubE 5 CHECKMULTISIG>”,
 ]

现在,让我们看看整个示例如何升级成为隔离见证。如果Mohammed的客户使用的是兼容隔离见证的钱包,他们就能创建一个P2WSH输出进行付款,如下:

P2WSH 输出脚本示例:

0 9592d601848d04b172905e0ddb0adde59f1590f1e553ffc81ddc4b0ed927dd73

再一次,就像P2WPKH的例子一样,你可以看到,隔离见证等效脚本要简单得多,省略了P2SH脚本中的各种脚本操作符。而且,隔离见证程序仅包含两个推送到堆栈的值:一个见证版本(0)和一个32字节的兑换脚本的哈希值。

提示 P2SH使用结果为20字节的RIPEMD160(SHA256(script)) 哈希算法,P2WSH见证程序使用了结果为32字节的SHA256(脚本)哈希算法。在选择哈希算法时,这一差异是有意为之,用于通过哈希值长度来区分两种类型的见证程序(P2WPKH and P2WSH),并为P2WSH(128bit安全,P2SH是80bit安全)提供更强的安全性。

Mohammed的公司可以通过提供正确的兑换脚本和足够的签名满足花费P2WSH输出。兑换脚本和签名都将作为见证数据的一部分被隔离在消费交易之外。在交易输入内部,Mohammed的钱包会放一个空的scriptSig:

下面的解码交易显示了带有隔离见证数据的P2WSH输出:

[...]
“Vin” : [
"txid": "abcdef12345...","vout": 0,
      "scriptSig": “”,
]
[...]
“witness”: “<SigA> <SigB> <2 PubA PubB PubC PubD PubE 5 CHECKMULTISIG>”
[...]

7.8.3.4 P2WPKH和P2WSH的区别

在前面的两节中,我们展示了两种类型的见证程序:支付给见证公钥哈希 (P2WPKH)和 支付给见证脚本哈希(P2WSH) 。这两种见证程序都有一个字节版本号和跟随其后的更长的一个哈希值组成。它们看起来非常相似,但解释却完全不同:一个被解释为一个公钥哈希值,被签名所满足,另一个被解释为脚本哈希值,被兑换脚本所满足。他们之间的关键区别是哈希值的长度:

  • P2WPKH中的公钥哈希值是20字节。

  • P2WSH中的脚本哈希值是32字节。

正是这个区别使得钱包可以区分这两种类型的见证程序。通过查看哈希值的长度,钱包可以确定是哪种类型的见证程序,P2WPKH 或者 P2WSH。

7.8.4 隔离见证升级

正如我们前面看到的例子,隔离见证的升级需要经过两步过程。首先,钱包必须创建特殊的隔离见证输出。然后,这些输出可以被知道如何构建隔离见证交易的钱包花费。在这些例子中,Alice的钱包是支持隔离见证的,能够使用隔离见证脚本创建特殊输出。Bob的钱包也是支持隔离见证的,能够花费这些输出。这个例子中隐含的是,实际上,Alice的钱包需要知道Bob使用的是支持隔离见证的钱包,也能花费这些输出。否则,如果Bob的钱包没有升级,Alice试图对Bob创建隔离见证付款,那么Bob的钱包将无法检测到这些付款。

提示 对于P2WPKH和P2WSH付款类型,付款和收款钱包都需要升级才能使用隔离见证。此外,付款的钱包还需要知道收款的钱包已经兼容隔离见证。

隔离见证不会在整个网络中同时实施。相反,隔离见证被实施为向后兼容的升级,这时新老客户端都有。钱包开发人员会各自升级自己的钱包软件,添加隔离见证功能。当付款钱包和收款钱包都支持隔离见证时,使用P2WPKH和P2WSH付款类型。传统的P2PKH和P2SH将继续为未升级的钱包工作。这留下了两个重要的场景,下一节将讨论这个情况:

  • 付款钱包不支持隔离见证,如何支付给能处理隔离见证的收款钱包。

  • 付款钱包支持隔离识别,如何根据地址识别和区分收款钱包是否具有隔离见证功能。

7.8.4.1 在P2SH中嵌入隔离见证

举个例子,假设Alice的钱包没有升级隔离见证,但Bob的钱包已经升级,可以处理隔离见证交易。Alice和Bob可以使用“旧”的非隔离见证交易。但是Bob可能想使用隔离见证,享受适用于见证数据的折扣,降低交易费用。

在这种情况下,Bob的钱包会构建一个包含隔离见证脚本的P2SH地址。Alice的钱包认为这是一个“正常的”P2SH地址,并可以在未识别隔离见证的情况下付款。然后,Bob的钱包可以通过隔离见证交易来花费这笔款项,这样充分利用了隔离见证交易并降低交易费用。

两种形式的见证脚本P2WPKH和P2WSH都可以嵌入到P2SH地址中。第一个是P2SH(P2WPKH),第二个是P2SH(P2WSH)。

7.8.4.2 P2SH 中的 P2WPKH

我们将检查的第一种见证脚本是P2SH(P2WPKH)。这是一个支付到见证公钥哈希的见证程序,嵌入在P2SH脚本中,所以可以被不支持隔离见证的钱包使用。

Bob的钱包用Bob的公钥构造了一个P2WPKH见证程序。然后这个见证程序被哈希,这个哈希值被编码成P2SH脚本。P2SH脚本被转换成以“3”开头的比特币地址,正如【7.3 P2SH(Pay-to-Script-Hash)】部分看到的。

Bob的钱包开始使用的就是之前看到的P2WPKH 见证程序:

Bob的P2WPKH见证程序:

0 ab68025513c3dbd2f7b92a94e0581f5d50f654e7

P2WPKH见证程序由见证版本和Bob的20字节公钥哈希组成。

Bob的钱包然后哈希之前的见证程序,先用SHA256,然后用RIPEMD160,产生另一个20字节的哈希值。

我们使用bx命令重现:

P2WPKH见证程序HASH160后的值

echo \
'0 [ab68025513c3dbd2f7b92a94e0581f5d50f654e7]'\
 | bx script-encode | bx sha256 | bx ripemd160
3e0547268b3b19288b3adef9719ec8659f4b2b0b

接下来,兑换脚本哈希被转换成比特币地址。再看一下bx命令结果:

P2SH地址

echo \
'3e0547268b3b19288b3adef9719ec8659f4b2b0b' \
| bx address-encode -v 5
37Lx99uaGn5avKBxiW26HjedQE3LrDCZru

现在,Bob可以把这个地址展示给顾客,方便他们付款。Alice的钱包可以支付给37Lx99uaGn5avKBxiW26HjedQE3LrDCZru,就像任何其他比特币地址一样。

为了给Bob付款,Alice的钱包会用P2SH脚本锁定输出:

HASH160 3e0547268b3b19288b3adef9719ec8659f4b2b0b EQUAL

即使Alice的钱包不支持隔离见证,Bob也可以使用隔离见证交易花费它创建的付款。

7.8.4.3 P2SH 中的 P2WSH

同样,多重签名脚本或其他复杂脚本的P2WSH见证程序也可以嵌入到P2SH脚本和地址中,使得任何钱包都可以实现兼容隔离见证的支付。

正如我们在【7.8.4.3 Pay-to-Witness-Script-Hash (P2WSH)】中看到的,Mohammed的公司正在使用带多重签名脚本的隔离见证支付。为了让任何一个客户都有可能向他的公司付款,不管他们的钱包是否已经升级支持隔离见证,Mohammed的钱包都可以将P2WSH见证程序嵌入到P2SH脚本中。

首先,Mohammed的钱包用SHA256(仅一次)对兑换脚本进行哈希运算。我们使用bx命令:

Mohammed的钱包创建了P2WSH见证程序

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

接下来,哈希过的兑换脚本转换为P2WSH见证程序:

0 9592d601848d04b172905e0ddb0adde59f1590f1e553ffc81ddc4b0ed927dd73

然后,使用SHA256和RIPEMD160对见证程序本身进行哈希,生成一个新的20字节哈希,就像传统P2SH中使用的那样。还是使用bx命令:

P2WSH见证程序的HASH160结果

echo \
'0 [9592d601848d04b172905e0ddb0adde59f1590f1e553ffc81ddc4b0ed927dd73]'\
 | bx script-encode | bx sha256 | bx ripemd160
86762607e8fe87c0c37740cddee880988b9455b2

接下来,钱包从这个哈希构造一个P2SH比特币地址。同样,我们使用bx命令计算:

P2SH比特币地址

echo \
'86762607e8fe87c0c37740cddee880988b9455b2'\
 | bx address-encode -v 5
3Dwz1MXhM6EfFoJChHCxh1jWHb8GQqRenG

现在,Mohammed的客户无需支持隔离见证就可以向这个地址付款。要向Mohammed发送付款,钱包将使用以下P2SH脚本锁定输出:

用于锁定对Mohammed多签的付款的P2SH脚本

HASH160 86762607e8fe87c0c37740cddee880988b9455b2 EQUAL

然后,Mohammed的公司就可以利用隔离见证的特点,包括更低的交易费用,构建隔离见证交易来消费这些款项。

7.8.4.4 隔离见证地址

即使在隔离见证激活后,大多数钱包也需要一段时间才能升级。首先,正如我们在上一节中看到的,隔离见证将嵌入到P2SH中,以简化支持与不支持隔离见证钱包之间的兼容性。

然而,一旦钱包广泛支持隔离见证,直接使用隔离见证设计的原生地址格式,对见证脚本进行编码就很有意义,而不再是将其嵌入P2SH。

原生隔离见证地址格式在BIP-173中定义:

【BIP-173】 原生v0-16见证输出的Base32地址格式

BIP-173只编码见证(P2WPKH和P2WSH)脚本。它与非隔离见证的 P2PKH或P2SH脚本不兼容。与“传统”比特币地址的Base58编码相比,BIP-173是校验和Base32编码。BIP-173地址也称为bech32地址,暗示使用了“BCH”错误检测算法和32字符编码集。

BIP-173地址使用32个小写字母数字字符集,经过了精心选择,减少因误读或敲错产生的错误。选择一个小写字符集,bech32就更易于阅读、提起,并且二维码编码效率提高了45%。

BCH错误检测算法是对以前的校验和算法(从Base58Check)的一个巨大改进,不仅允许检测,而且允许纠正错误。在检测错误时,地址输入界面(如表单中的文本字段)可以检测并突出显示最可能敲错的字符。

根据BIP-173规范,以下是bech32地址的一些示例:

主网 P2WPKH: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4

测试网 P2WPKH: tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx

主网 P2WSH: bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3

测试网 P2WSH: tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7

就像上面示例中所见,隔离见证bech32字符串最长为90个字符,由三部分组成:

人类可读部分: 前缀“bc”和“tb”表示是主网还是测试网

分隔符: 数字“1”,不是32字符编码集的一部分,只是作为分隔符出现在这个位置

数据部分: 最少6个字母数字字符,经过校验和编码的见证脚本

目前,只有少数钱包接受或产生原生隔离见证bech32地址,但随着隔离见证使用率的增加,这些将会越来越多。

7.8.4.5 交易标识符

隔离见证的最大好处之一就是消除了第三方交易延展性。

在隔离见证之前,交易可以通过第三方巧妙地修改其签名,在不改变任何基本属性(输入,输出,金额)的情况下更改其交易ID(哈希)。这为拒绝服务DOS攻击,以及对有缺陷的钱包软件的攻击创造了机会,这些软件假定未经证实的交易哈希是不可变的。

通过引入隔离见证,交易有两个标识符txid和wtxid。传统的txid是序列化交易的双SHA256哈希,没有见证数据。交易的wtxid是具有见证数据的交易的新序列化格式的双SHA256哈希。

传统txid的计算方式与非隔离见证交易完全相同。但是,由于隔离见证交易在每个输入中都有空的scriptSig,不存在可由第三方修改的交易部分。因此,在隔离交易中,即使交易未经确认,txid也是不能被第三方修改的。

wtxid就像一个“扩展的”ID,因为该哈希也包含了见证数据。如果交易在没有见证数据情况下传输,则wtxid和txid是相同的。注意,由于wtxid包含见证数据(签名),并且由于见证数据可能具有延展性,所以在交易确认之前,wtxid被认为是可延展的。只有隔离见证的txid可以被第三方认为是不可变的,并且只有当交易的所有输入都是隔离见证输入时才是不变的。

提示 隔离见证交易有两个ID:txid和wtxid。txid是没有见证数据的交易的哈希,wtxid是包含见证数据的哈希。所有输入都是隔离见证输入的交易,不受第三方交易延展性影响。

7.8.5 隔离见证新的签名算法

隔离见证修改了四个签名验证函数(CHECKSIG,CHECKSIGVERIFY,CHECKMULTISIG和CHECKMULTISIGVERIFY)的语义,改变了交易承诺哈希的计算方式。

比特币交易中的签名应用于一个承诺哈希 commitment hash,该哈希值由交易数据计算得出,锁定了数据中表示签名者承诺这些金额的特定部分。例如,在简单的SIGHASH_ALL类型签名中,承诺哈希包括所有的输入和输出。

不幸的是,计算承诺哈希的方法导致了这样一种可能,即验证签名的节点可能会被迫执行大量哈希计算。具体地说,相对于交易中的签名操作的数量,其哈希操作数量增加了O(n2)复杂度。因此,攻击者可以创建具有大量签名操作的交易,从而导致整个比特币网络必须执行数百或数千个哈希操作来验证该交易。

隔离见证带来机会,通过改变承诺哈希计算方式来解决这个问题。对于隔离见证版本0见证程序,使用BIP-143中规定的改进的承诺哈希算法进行签名验证。

新算法实现了两个重要目标。首先,哈希操作的数量与签名操作的数量相比,平缓增长O(n)复杂度,从而减少了使用过于复杂的交易创建拒绝服务攻击的机会。其次,承诺哈希现在还包括作为承诺的一部分的每个输入的值(金额)。这意味着签名者可以承诺特定的输入值,而无需“获取”和检查输入引用的前一个交易。在离线设备(如硬件钱包)的情况下,这极大地简化了主机与硬件钱包之间的通信,消除了对以前的交易流进行验证的需要。硬件钱包可以接受不可信主机“声明的”的输入值。由于签名是无效的,如果输入值不正确,硬件钱包在签名输入之前不需要验证该值。

7.8.6 隔离见证的经济激励

比特币挖矿节点和全节点支持比特币网络和区块链,会产生资源成本。随着比特币交易量的增加,资源成本(CPU,网络带宽,磁盘空间,内存)也在增加。矿工通过收取与每次交易的大小(字节)成比例的费获得补偿。不挖矿的全节点不能得到补偿,因为需要运行权威的完全验证全索引节点,他们也会有这些成本,他们会使用该节点来运营比特币业务。

如果没有交易费用,比特币数据可能会大幅增加。费用旨在通过基于市场的价格发现机制,使比特币用户的需求与交易对网络带来的负担保持一致。

基于交易占据空间大小的费用计算方式认为交易中的所有数据其成本是相同的。但是从全节点和矿工的角度来看,交易的某些部分的成本要高得多。加入比特币网络的每笔交易都会影响节点上四种资源的消耗:

  • 磁盘空间

每个交易都存储在区块链中,从而增加区块链的总大小。区块链存储在磁盘上,但可以通过“修剪pruning”旧交易来优化存储。

  • CPU

每个交易都必须经过验证,这需要CPU时间。

  • 带宽

每笔交易至少通过网络传输一次(通过泛洪传播)。如果在区块传播协议中没有任何优化,交易将作为区块的一部分再次传输,对网络容量的影响会加倍。

  • 内存

验证交易的节点将UTXO索引或整个UTXO集保留在内存中,以加快验证。由于内存比磁盘至少贵一个数量级,所以UTXO集的增长不成比例增加了节点的运行成本。

从列表中可以看出,并不是交易的每个部分都对运行节点的成本或者比特币扩展以支持更多交易的能力产生同等的影响。交易中最昂贵的部分是新创建的输出,因为它们被添加到内存中的UTXO集。相比之下,签名(又名见证数据)对网络增加的负担较小,运行节点的成本增加也不多,因为见证数据只被验证一次,之后又不再使用。此外,在收到新的交易并验证见证数据之后,节点可以立即丢弃该见证数据。如果按照交易规模计算费用,而不区分这两种数据,那么市场上的收费激励就不符合交易实际成本。事实上,目前的费用结构实际上鼓励了相反的行为,因为见证数据是交易的最大部分。

收费产生的激励之所以重要,是因为它们会影响钱包的行为。所有的钱包都必须执行一些策略来组合交易,这些策略要考虑到许多因素,比如隐私(减少地址重复使用),碎片化(大量找零)以及费用等。如果费用压倒性地促使钱包在交易中使用尽可能少的投入,这可能导致选择UTXO和改变地址策略,从而不经意地膨胀UTXO集。

交易在其输入中消耗UTXO,并用它们的输出创建新的UTXO。因此,输入比输出多的交易将导致UTXO集合的减少,而输出多于输入的交易将导致UTXO集合的增加。让我们考虑输入和输出之间的差异,并称之为“净增UTXO(Net-new-UTXO)”。这是一个重要的指标,因为它告诉我们交易将对最昂贵的全网资源(内存中的UTXO集)产生什么影响。净增UTXO为正值的交易增加了这一负担。净增UTXO为负值的交易就可以减轻负担。因此,因此,我们希望鼓励负净增UTXO或零净增UTXO持平的交易。

让我们来看一个例子,说明有无隔离见证,交易费用计算产生了哪些激励措施。我们来看两个不同的交易。交易A是3输入,2输出的交易,净增UTXO为-1,意味着它消耗的UTXO比它创建的多了一个,将UTXO减1。交易B是2输入3输出的交易,净增UTXO为1,意味着它向UTXO集增加一个UTXO,增加整个比特币网络的成本。这两个交易都使用多重签名(2/3)脚本来演示复杂脚本如何增加隔离见证对费用的影响。我们假设交易费为每字节30 satoshi,见证数据的折扣费用为75%:

  • 未使用隔离见证

    交易A费用:25,710 satoshi 交易B费用:18,990 satoshi

  • 使用隔离见证

    交易A手续费:8,130 satoshi 交易B手续费:12,045 satoshi

实施了隔离见证实施,这两笔交易的费都减少了。但通过比较这两个交易的成本,我们发现,在隔离见证之前,净增UTXO为负的交易的费用更高。在隔离见证之后,交易费用与减少新增UTXO的动机就保持一致,而不是无意中惩罚包含较多输入的交易。

因此,隔离见证对比特币用户支付的费用有两个主要的影响。首先,隔离见证通过折扣见证数据和增加比特币区块链的能力来降低交易的总体成本。其次,隔离见证对见证数据的折扣纠正了可能无意中在UTXO集合中造成更多膨胀的激励偏差。