7.1介绍
在上一章中,我们介绍了比特币交易的基本元素,并且查看了最常见的交易脚本类型,即P2PKH脚本。在本章中,我们将介绍更高级的脚本,以及如何使用它来构建具有复杂条件的交易。
首先,我们将看看多重签名脚本。接下来,我们将分析第二常见的交易脚本Pay-to-Script-Hash,它开启了复杂脚本的世界。然后,我们将分析新的脚本操作符,它通过 timelocks 字段在比特币中加入了时间维度。
7.2多重签名
多重签名脚本设置了一个条件,其中N个公钥被记录在脚本中,并且其中至少有M个必须提供签名来解锁资金。这也称为M-N方案,其中N是密钥的总数,M是验证所需的签名的数量。例如,2/3的多重签名是三个公钥被列为潜在签名人,其中至少有2个用于给一个有效的交易创建签名,以便花费资金。目前,标准多重签名脚本限制在最多15个可列出的公钥,这意味着您可以用在1-of-1到15-of-15之间的或该范围内的任何组合的多重签名执行任何操作。在本书发布时,15个可列出密钥的限制可能会被解除,因此请检查isStandard()函数以查看当前网络接受的是什么样的。
设置M-N多重签名条件的锁定脚本的一般形式是:
M ... N CHECKMULTISIG
M是花费输出所需的签名的数量,N是列出的公钥的总数。
设置 2-of-3 多重签名条件的锁定脚本如下所示:
2 3 CHECKMULTISIG
上述锁定脚本可由包含若干对签名和公钥的脚本予以解锁,
或者由3个列出的公钥相对应的任意2个私钥生成的签名组合予以解锁。
两个脚本一起将形成一个组合验证脚本:
2 3 CHECKMULTISIG
当执行时,当且仅当解锁版脚本与锁定脚本设置的条件相匹配时,该组合脚本的计算结果才会为true。
上述例子中,即为:解锁脚本是否含有3个公钥中的任意2个公钥相对应的私钥生成的有效签名。
CHECKMULTISIG执行中的bug
CHECKMULTISIG的执行中有一个bug,需要一些轻微的解决方法。 当CHECKMULTISIG执行时,它应该消耗堆栈(stack) 上的M + N + 2个条目作为参数。 然而,由于该bug,CHECKMULTISIG将比预期多弹出一个值。 我们使用前面的验证例子来更详细的看看这个过程:
2 3 CHECKMULTISIG
首先,CHECKMULTISIG弹出最上面的一个条目,即N(在这个例子中N是“3”)。然后它弹出N个条目,这是可以签名的公钥。在这个例子中,是公钥A,B和C。然后,它弹出一个条目,即M,代表需要多少个签名,这里M = 2。此时,CHECKMULTISIG应弹出最后的M个条目,这些是签名,并查看它们是否有效。
然而,不幸的是,CHECKMULTISIG的实现有个bug,导致CHECKMULTISIG会再多弹出一个项目(即总共M + 1个)。检查签名时,那个额外的条目会被无视,因此它对CHECKMULTISIG本身没有直接影响。但是,必须存在额外的那个值,因为如果不存在,则当CHECKMULTISIG尝试在空堆栈上弹出值时,会导致堆栈错误和脚本失败(将交易标记为无效)。因为额外的那个条目会被忽略,因此它可以是任何东西,但通常使用0。
因为这个bug成为共识规则的一部分,所以现在它必须永远被复制。因此,正确的脚本验证将如下所示:
0 2 3 CHECKMULTISIG
这样多重签名中的解锁脚本就不是下面的:
而是:
0
从现在开始,如果你看到一个多重签名解锁脚本,你应该会在开始看到一个额外的0,其唯一的作用是作为解决bug的一个变通方案,这意外地成为一个共识。
7.3 P2SH(Pay-to-Script-Hash)
P2SH在2012年被作为一种强大的新型交易类型而引入,它能大大简化复杂交易脚本的使用。为进一步解释P2SH的必要性,让我们先看一个实际的例子。
在第1章中,我们曾介绍过Mohammed,一个迪拜的电子产品进口商。Mohammed的公司采用比特币多重签名特性广泛用于其公司记账中。多重签名脚本是比特币高级脚本最为常见的运用之一,具有强大的功能特性。Mohammed的公司将多重签名应用在所有顾客的支付中,这在会计名词中叫"应收账款"(accounts receivable, 即AR)。基于多重签名机制,顾客的任何支付都需要至少两个签名才能解锁,一个来自Mohammed,另一个来自其合伙人或其拥有备份钥匙的代理人。这样的多重签名机制能为公司治理提供管控便利,同时也能有效防范盗窃、挪用和遗失。
最终的脚本非常长:
2 5 CHECKMULTISIG
虽然多重签名十分强大,但其使用起来还是多有不便。基于之前的脚本,Mohammed必须在客户付款前将该脚本发送给每一位客户,而每一位顾客也必须使用专门的能创建定制交易脚本的比特币钱包软件,每位顾客还得学会如何利用脚本来创建交易。 此外,由于多重签名脚本包含特别长的公钥,最终生成的交易大小可能是简单支付交易的5倍之多。如此大的交易将给客户造成费用负担。最后,这样大的交易脚本将被携带在所有全节点的随机存储器(RAM)的UTXO集中,直到该笔资金被花费。所有这些问题,导致在实际交易中采用这种复杂解锁脚本变得困难重重。
P2SH正是为了解决这一实际难题而开发的,它旨在让使用复杂脚本与支付给比特币地址一样简单。在P2SH 支付中,复杂锁定脚本被数字指纹所取代,数字指纹是指密码学中的哈希。当一笔交易试图花费UTXO时,除了含有解锁脚本外,它还必须含有与哈希相匹配的脚本。P2SH的含义是,“pay to a script matching this hash, a script that will be presented later
when this output is spent.”。
在P2SH交易中,取代锁定脚本的哈希,也被称为赎回脚本(redeem script)。因为它在系统中是在赎回时出现的而不是以锁定脚本的形式出现。表7-1显示了不含P2SH的复杂脚本,表7-2显示了同样的脚本用P2SH编码之后的数据。
Table 7-1. Complex script without P2SH
Table 7-2. Complex script as P2SH
从表中可以看出,对于P2SH,详细描述了花费输出的条件的复杂脚本(赎回脚本)并没有在锁定脚本中出现。相反,只有它的散列值出现在锁定脚本中,并且赎回脚本将作为解锁脚本的一部分,在之后输出被花费时才会出现。 这使得费用负担和复杂性从交易的发送方转移到了收款方。
让我们再看下Mohammed公司的例子,复杂的多重签名脚本和相应的P2SH脚本。
首先,Mohammed公司对所有顾客的付款采用多重签名脚本:
2 5 CHECKMULTISIG
如果占位符由实际的公钥(以04开头的520字节的数字)替代,你将会看到这个脚本会非常地长:
2 04C16B8698A9ABF84250A7C3EA7EEDEF9897D1C8C6ADF47F06CF73370D74DCCA01CDCA79DCC5C395D7EEC6984D83F1F50C900A24DD47F569FD4193AF5DE762C58704A2192968D8655D6A935BEAF2CA23E3FB87A3495E7AF308EDF08DAC3C1FCBFC2C75B4B0F4D0B1B70CD2423657738C0C2B1D5CE65C97D78D0E34224858008E8B49047E63248B75DB7379BE9CDA8CE5751D16485F431E46117B9D0C1837C9D5737812F393DA7D4420D7E1A9162F0279CFC10F1E8E8F3020DECDBC3C0DD389D99779650421D65CBD7149B255382ED7F78E946580657EE6FDA162A187543A9D85BAAA93A4AB3A8F044DADA618D087227440645ABE8A35DA8C5B73997AD343BE5C2AFD94A5043752580AFA1ECED3C68D446BCAB69AC0BA7DF50D56231BE0AABF1FDEEC78A6A45E394BA29A1EDF518C022DD618DA774D207D137AAB59E0B000EB7ED238F4D800 5 CHECKMULTISIG
整个脚本可由仅为20个字节的加密哈希所取代,首先对其采用SH256哈希算法,然后对结果运用RIPEMD160算法。上述脚本转为20字节的哈希:
54c557e07dde5bb6cb791c7a540e0a4796f5e97e
P2SH交易将输出锁定到一个哈希而不是特别长的脚本,即使用如下的锁定脚本:
HASH160 54c557e07dde5bb6cb791c7a540e0a4796f5e97e EQUAL
正如你所看到的,这个脚本比前面的长脚本简短多了。不用 “pay to this 5-key multisignature script”,而用P2SH表示同样的交易即“pay to a script with this hash” 。顾客在向Mohammed公司支付时,只需在其支付指令中纳入这个非常简短的锁定脚本即可。当 Mohammed和他的合伙人想要花费这笔UTXO时,提供原始赎回脚本(该赎回脚本的hash锁定了UTXO)和必要的签名即可解锁,如:
<2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG>
两个脚本经由两步实现组合。 首先,将赎回脚本与锁定脚本核对以确认与其哈希是否匹配:
<2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG> HASH160 EQUAL
假如赎回脚本与哈希匹配,解锁脚本就可以单独执行,以解锁赎回脚本:
2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG
本章中描述的几乎所有脚本只能以P2SH脚本来实现。 它们不能直接用在UTXO的锁定脚本中。
7.3.1 P2SH地址
P2SH的另一重要特征是它能将一个脚本哈希编码为一个地址(具体见BIP-13)。P2SH地址是脚本经哈希后得到的20字节长度的哈希值再经Base58Check编码后得到的,就像比特币地址是公钥经哈希后得到的20字节长度的哈希值再经Base58Check编码后得到的。由于P2SH地址采用5作为版本前缀,这导致基于Base58Check编码后的地址以“3”开头。例如,Mohammed的复杂脚本,哈希后并经Base58Check编码后得到的P2SH地址是 “39RF6JqABiHdYHkfChV6USGMe6Nsr66Gzw” 。此时,Mohammed可以将该地址给他的客户,这些客户可以采用几乎任何的比特币钱包实现简单支付,就像这是一个比特币地址一样。以“3”为前缀暗示钱包这是一种特殊类型的地址,该地址对应一个脚本而不是对应一个公钥,但是它的使用方式和支付给比特币地址完全一样。
P2SH地址隐藏了所有的复杂性,因此,人们使用它进行支付时将不会看到脚本。
7.3.2 P2SH的优点
与直接使用复杂脚本以锁定输出的方式相比,P2SH的特性具有以下优点:
7.3.3赎回脚本和标准确认
0.9.2版之前的比特币核心客户端,P2SH仅限于标准比特币交易脚本类型,由 isStandard() 函数来确定。这也意味着使用该笔资金的交易中的赎回脚本只能是下面几种标准类型中的一种:P2PK、P2PKH、或者多重签名,而不包括 RETURN 和P2SH。
作为0.9.2版的比特币核心客户端,P2SH交易能包含任意有效的脚本,这使得P2SH标准更为灵活,也可以用于试验多种新的和复杂类型的交易。
请记住不能将P2SH植入P2SH赎回脚本,因为P2SH是不能递归的。虽然在技术上可以将RETURN包含在赎回脚本中,因为规则中没有阻止您这么做,但是这是没有实际用途的,因为在验证期间执行RETURN将导致交易被标记为无效。
需要注意的是,因为赎回脚本在你试图花费一个P2SH输出前是不会出现在比特币网络的,因此如果你用一个无效的赎回脚本的哈希锁定一个交易输出,则它将会被忽略。该UTXO会被成功锁定,但是你将不能使用该笔资金,因为将花费的交易中含有赎回脚本,但是该脚本不会被接受,因为它是无效的。这样的处理机制会造成风险,因为你可能将比特币锁定在一个未来不能被花费的P2SH中。比特币网络本身会接受这一P2SH锁定脚本,即便它对应着一个无效的赎回脚本,因为脚本哈希提供不了它所代表的脚本的任何指示信息。
警告
P2SH锁定脚本包含一个赎回脚本的哈希,该哈希对于赎回脚本本身未提供任何描述。P2SH交易即便在赎回脚本无效的情况下也会被认为有效并接受。你也可能意外地用这种方式将比特币锁住,以后也不能花费这笔比特币。
7.4 数据记录输出(RETURN操作符)
比特币的去中心特点和时间戳账本机制,即区块链技术,其潜在运用将大大超越支付领域。许多开发者试图充分发挥交易脚本语言的安全性和可恢复性优势,将其运用于电子公证服务、证券认证和智能合约等领域。很多早期的开发者利用比特币这种通过创建交易输出从而将数据记录到区块链上的技术进行了很多尝试 ,例如,为文件记录电子指纹,采用这种方式,即任何人都可以通过在特定日期进行交易从而建立关于该文档的存在性证明。
运用比特币的区块链技术存储与比特币支付不相关数据的做法是一个有争议的话题。许多开发者认为这是滥用,因而试图予以阻止。另一些开发者则将之视为区块链技术强大功能的有力证明,从而想要鼓励这种试验。那些反对存储非支付相关数据的开发者认为这样做将导致“区块链膨胀”,因为所有的比特币全节点都将以消耗磁盘存储空间为成本,负担存储此类数据的任务,而区块链本不打算运载这些数据。 更为严重的是,此类交易仅将目的比特币地址当作自由格式的20个字节的字段而使用,进而会产生不能被花费的UTXO。因为比特币地址只是被当作数据使用,并不对应一个私钥,所以会导致UTXO不能被花费,因而是一种伪支付行为。这些交易永远不会被花费,因此永远不会从UTXO集中删除,并导致UTXO数据库的大小永远增加或“膨胀”。
在0.9版的比特币核心客户端上,通过采用Return操作符最终实现了妥协。Return允许开发者在交易输出上增加80字节的非支付数据(nonpayment data)。然后,与伪交易型的UTXO的使用不同,Return操作符创造了一种明确的可查验的不可花费型输出,此类数据无需存储于UTXO集。Return输出被记录在区块链上,它们会消耗磁盘空间,也会导致区块链规模的增加,但它们不存储在UTXO集中,因此也不会使得UTXO内存池膨胀,更不会以消耗价格高昂的内存为代价使全节点都不堪重负。
RETURN 脚本的样式:
RETURN
“data”部分被限制为80字节,且大多时候代表着一个哈希,如32字节的SHA256算法的哈希结果。许多应用都在其前面加上前缀以帮助识别应用程序。例如,电子公正服务 Proof of Existence 采用8个字节的前缀“DOCPROOF”,相应的ASCII码用十六进制表示为 44 4f 43 50 52 4f 4f 46 。
请记住 ,并不存在对应 RETURN 操作符的、能用于花费 RETURN 输出的解锁脚本(unlocking script) 。RETURN 的关键点是你无法花费使用RETURN锁定的输出,因此它也就没有必要保存在可能会被花费的UTXO集中,RETURN 是可证实不能被花费的。RETURN 常作为一个金额为0的比特币输出, 因为这种输出上指定的任何比特币事实上都会永久消失。假如一笔交易的输入引用了一个RETURN,脚本验证引擎将会中止验证脚本的执行,并将交易标记为无效。RETURN的执行过程本质上是使脚本返回一个false并中止执行 。如果你意外地将 RETURN 的输出作为另一笔交易的输入,那么该交易是无效的。
一笔标准交易(通过了 isStandard() 函数检验的交易)只能有一个 RETURN 输出。但是单个 RETURN 输出能在交易中与其他任意类型的输出进行组合。
在0.10版本的Bitcoin Core中添加了两个新的命令行选项。 选项 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纪元时间戳(自Jan-1-1970之后的秒数),并且交易在指定时间之前无效。 带有nLocktime的交易表示该交易必须由原始系统持有到未来指定的块或时间,并且只有在生效效后才被发送到比特币网络。如果交易在指定的nLocktime之前传输到网络,那么第一个节点就会拒绝该交易,认为其无效,并且不会被中继到其他节点。使用nLocktime等同于一张延期支票。
7.5.1.1交易锁定时间的局限性
nLocktime有一个局限就是,虽然它可以允许在将来某个时间花费一些输出,但是它不能确保在那时间之前没有花费它们。我们用下面的例子来解释一下。
Alice花费了其中一个输出签署了一笔交易,支付给Bob的地址,并将交易的nLocktime设定为3个月之后。Alice把这笔交易发送给Bob。针对这笔交易,Alice和Bob知道:
然而:
了解交易的nLocktime的局限性很重要。 唯一的保证是Bob在3个月过去之前无法兑现它, 但不能保证Bob将来会得到资金。 为了实现这样的保证, 时间锁必须放在UTXO本身上,并成为锁定脚本的一部分,而不是放在交易上。这是通过下一种形式的时间锁定来实现的,称为检查锁定时间验证(Check Lock Time Verify)。
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后缀所示,如果结果为FALSE,则CLTV操作符将中止脚本的执行 ,如果结果为TRUE,则继续执行。
为了使用CLTV锁定输出,创建输出时,你将其插入到的交易输出的的赎回脚本中。例如,如果Alice支付给Bob的地址,输出通常会包含一个这样的P2PKH脚本:
DUP HASH160 EQUALVERIFY CHECKSIG
要想锁定一段时间,比如说3个月以后,交易将会是类似下面的一个带有赎回脚本的P2SH交易:
CHECKLOCKTIMEVERIFY DROP DUP HASH160 EQUALVERIFY CHECKSIG
其中
是从交易开始被挖出时起计约3个月时间的块高度或时间值:当前块高度+12,960(块)或当前Unix纪元时间戳+7,760,000(秒)。现在,不要担心CHECKLOCKTIMEVERIFY之后的DROP操作码,下面很快就会解释。
当Bob尝试花费这个UTXO时,他构建一个引用这个UTXO作为输入的交易。他在这个交易输入的解锁脚本中使用他的签名和公钥,并将交易的nLocktime设置为等于或大于Alice在CHECKLOCKTIMEVERIFY中设置的时间锁(timelock)。然后,Bob在比特币网络上广播该交易。
Bob的交易按如下评估,如果Alice设置的CHECKLOCKTIMEVERIFY参数小于或等于这笔交易的nLocktime,则脚本将继续执行(就好像执行“无操作”或NOP操作码一样)。否则,脚本中止执行,并且该交易被视为无效。
更确切地说,如果满足下面任一条件,CHECKLOCKTIMEVERIFY 将失败并中止执行,并将交易标记为无效(来源:BIP-65):
注释
CLTV和nLocktime使用相同的格式来描述时间锁定,无论是区块高度还是自Unix纪元以来所经过的时间(单位是秒)。 最重要的是,在一起使用时,nLocktime的格式必须与输入中的CLTV格式相匹配,即它们必须要么都是区块高度, 要么都是时间(单位是秒)。
执行后,如果满足CLTV,则其前面的时间参数将留在栈顶,并且可能需要使用 DROP 操作符进行删除,才能正确执行后续的脚本操作码。 这就是您经常在脚本中看到 CHECKLOCKTIMEVERIFY 后面跟着 DROP 的原因。
通过将nLocktime与CLTV结合使用,“Transaction locktime limitations”中描述的情况就发生了改变。 因为Alice锁定了UTXO本身,所以现在Bob或Alice都不可能在3个月的锁定时间到期之前花费它。
通过将时间锁定功能直接引入到脚本语言中,CLTV 允许我们开发一些非常有趣的复杂脚本。
该标准在 BIP-65(CHECKLOCKTIMEVERIFY) 中定义。
7.5.3相对时间锁
nLocktime和CLTV都是绝对时间锁定,它们指定一个绝对时间点。接下来我们将要考察的两个时间锁定功能是相对时间锁定,因为它们指定了一个花费输出的条件,该条件是从区块链中的输出确认起经过的时间。
相对时间锁是有用的,因为它们允许将两个或多个相互依赖的交易链接在一起, 比如在一个交易中施加一个时间约束,该 时间依赖于上一个交易确认起所经过的时间。换句话说,在UTXO被记录在区块链上之前,不开始计时间。这个功能在双向状态通道和闪电网络中特别有用,我们将在后面章节“Payment Channels and State Channels”中看到。
相对时间锁,和绝对时间锁一样,同时实现了交易级功能和脚本级操作码。交易级相对时间锁是作为一个共识规则用nSequence的值实现的,nSequence字段在每个交易输入中都会设置。脚本级相对时间锁用CHECKSEQUENCEVERIFY(CSV)操作码实现。
相对时间锁是根据 BIP-68, Relative lock-time using consensus-enforced sequence numbers 与 BIP-112, CHECKSEQUENCEVERIFY 的规范实现的。
BIP-68 和 BIP-112 是在2016年5月升级到新共识规则的软分叉时启用的。
7.5.4 nSequence相对时间锁
相对时间锁可以在交易的每个输入中设置,其方法是在每个输入中设置nSequence字段。
7.5.4.1 nSequence的本义
nSequence字段的设计初心(但是并没有完全实现)是想让内存池中的交易能被修改,这样,一个输入中含有nSequence 值小于2^32 (0xFFFFFFFF) 的交易意味着该交易还没有完成(“finalized”)。 这样的交易将在内存池中保存,直到被另外一个花费相同输入并具有较大nSequence值的交易代替。一旦收到一个交易,其输入中的nSequence值为2^32,那么它将被视为“完成的”并被开采。
nSequence的最初的含义并没有被正确实现,并且在不利用时间锁定的交易中nSequence的值通常设置为232。对于具有nLocktime或CHECKLOCKTIMEVERIFY的交易,nSequence值必须设置为小于232,以使时间锁定器有效。通常设置为2^32 - 1(0xFFFFFFFE)。
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时,则认为是有一个相对时间锁定。这种交易只有输入到了相对锁定时间后才生效。例如,nSequence是30个区块的相对时间锁的一个输入的交易只有在输入中引用的UTXO从被挖出开始起计至少过了30个区块时才有效。由于nSequence是每个输入都有的字段,因此交易可能包含任何数量的时间锁定的输入,所有这些输入都必须具有足够的"年龄"后才能使交易有效。交易可以同时含有相对时间锁定的输入(nSequence <2^31)和没有相对时间锁定(nsequence> = 2^31)的输入。 nSequence 可以指定区块数或者时间的秒数,但与 nLocktime 中使用的格式略有不同。一个类型标志被用来区分 nSequence 值是计算区块数还是计算时间(以秒为单位)。该类型标志设置在第23个最低有效位(即值1 << 22)。如果设置了类型标志,则nSequence值将被解释为512秒的倍数,如果未设置类型标志,则nSequence值被解释为区块数。
当将nSequence解释为相对时间锁定时,则只考虑16个最低有效位。一旦评估了标志位(位32和23),nSequence值通常要与16位掩码进行掩码运算后所得(例如 nSequence&0x0000FFFF)。
下图显示由BIP-68定义的nSequence值的二进制布局图:
Figure 7-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被开采后过了一定数量的块或时间后才能被花费。
与CLTV一样,CSV中的值必须与对应的nSequence值中的格式相匹配。如果CSV是以区块数指定的,那么nSequence也是如此,如果CSV是以秒数指定的,那么nSequence也是如此。
当几个(装链的)交易已经被创建和签名,但还未传播,被保留为“脱链”(off-chain)时,CSV相对时间锁特别有用。在父交易被挖出、被传播,并且经过相对时间锁指定的时间前,子交易都不能使用。这个用例的一个应用可以在 “Payment Channels and State Channels” 和 “Routed Payment Channels (Lightning Network)” 章节中看到。
CSV 细节参见 BIP-112, CHECKSEQUENCEVERIFY
7.5.6中位时间过去Median-Time-Past
作为相对时间锁定启用的一部分,时间锁定(包括绝对和相对)的“时间”的计算方式也发生了变化。在比特币中,现实时间(wall time)和共识时间(consensus time)之间存在微妙但非常有重大意义的差异。比特币是一个分散式网络,这意味着每个参与者都有自己的时间观。网络上的事件不会随时随地发生。网络延迟必须被每个节点考虑进去。最终,所有内容都被同步,以创建一个共同的账本。比特币对于账本已经存在的时间这个状态会每隔10分钟达成一次共识。
区块头中设置的时间戳是由矿工设定的。共识规则允许分散节点之间的时钟精度存在一定范围的差别。然而这导致了一个不幸的诱因,它诱使矿工对于区块的时间进行说谎,以便通过包含进还没到期的交易来赚取额外矿工费。有关详细信息,请参阅以下部分。
为了移除矿工说谎的诱因,加强时间锁定的安全性,在相对时间锁的BIPS上又提出并启用了一个新的BIP。这就是 BIP-113 ,它定义了一个称为“中位时间过去(Median-Time-Past)”的新的时间测量共识机制。
Median-Time-Past 通过取最后11个区块的时间戳并找到它们的中位数来作为“中位时间过去”的值。这个中间时间值就变成了共识时间,并被用于所有的时间锁定计算。通过采用过去大约两个小时的中间点,任何单个区块的时间戳的影响就被减小了。通过包含11个区块,没有单个矿工可以利用时间戳来从时间锁定尚未到期的交易中获取非法矿工费。
Median-Time-Past 改变了nLocktime,CLTV,nSequence和CSV的时间计算的实现方式。由Median-Time-Past计算的共识时间总是比现实时间落后大约一个小时。如果要创建时间锁定交易,那么在nLocktime,nSequence,CLTV和CSV中估算期望的值时,需要计算Median-Time-Past时间。
Median-Time-Past细节参见 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设置为100,001。 在正常情况下,这个nLocktime没有产生任何效果 —— 因为交易无论如何只能包含在#100,001块中,这就是下一个区块。 但是在区块链分叉攻击的情况下,由于所有这些交易都将被时间锁定在#100,001号区块,所以矿工们无法从内存池中提取费用高的交易。他们那时候只能用只要是有效的任何交易重新挖第#100,000号区块,这实质上并不会获得新的费用。 为了实现这一点,Bitcoin Core将所有新交易的nLocktime设置为
,并将所有输入上的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之前,这使得它看起来像“向后的”,如下所示:
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后缀充当守护子句(guard
clause),只有在满足前提条件的情况下才会继续。
例如,以下脚本需要Bob的签名和产生特定哈希的原图像(秘密地),这两个条件都被满足时才能解锁这个脚本:
HASH160 EQUALVERIFY CHECKSIG
为了赎回这个,Bob必须构建一个提供有效的原图像和签名的解锁脚本,:
没有原图像,Bob无法到达检查其签名的那部分脚本。上述锁定脚本可以用IF编写的脚本替代:
HASH160 EQUAL
IF
CHECKSIG
ENDIF
Bob的解锁脚本还是一样的:
使用IF的脚本与使用具有VERIFY后缀的操作码相同,他们都作为守护子句。 然而,VERIFY构造的这种更有效率,它使用了较少的操作码。
那么,我们什么时候使用VERIFY,什么时候使用IF? 如果我们想要做的是附加一个前提条件(保护子句,guard clause),那么VERIFY是更好的。 然而,如果我们想要有多个执行路径(flow control,流控制),那么我们需要一个IF … ELSE流控制子句。
提示
诸如EQUAL之类的操作码会将结果(TRUE / FALSE)推送到堆栈上,留下它用于后续操作码的评估。 相比之下,EQUALVERIFY操作码不会在堆栈上留下任何东西, 以VERIFY结尾的操作码不会将结果留在堆栈上。
7.6.2在脚本中使用流控制
比特币脚本中的流控制的一个非常常见的用途是用于构建一个提供多个执行路径的赎回脚本,每条执行路径都表示一种不同的赎回UTXO的的方式。
我们来看一个简单的例子,我们有两个签名人,Alice和Bob,两人中任何一个都可以赎回。 如果使用多重签名,这将被表示为1-of-2 多重签名脚本。 为了示范,我们将使用IF子句做同样的事情:
IF
CHECKSIG
ELSE
CHECKSIG
ENDIF
看这个赎回脚本,你可能会想:“ 条件在哪里?IF子句之前什么都没有!”
条件不是赎回脚本的一部分,相反,解锁脚本将提供该条件,以允许Alice和Bob“选择”他们想要的执行路径。
Alice用解锁脚本来赎回:
1
结尾的1作为条件(TRUE),将使IF子句执行第一个赎回路径, 因为Alice有一个签名。
Bob为了赎回,他必须通过给IF子句一个FALSE值来选择第二个执行路径:
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设计的实现上述方案的脚本:
IF
IF
2
ELSE
<30 days> CHECKSEQUENCEVERIFY DROP
CHECKSIGVERIFY
1
ENDIF
3 CHECKMULTISIG
ELSE
<90 days> CHECKSEQUENCEVERIFY DROP
CHECKSIG
ENDIF
Mohammed的脚本使用嵌套的IF … ELSE流控制子句来实现三个执行路径。
在第一个执行路径中,该脚本作为三个合作伙伴的简单的2-of-3 multisig 操作。该执行路径由第3行和第9行组成。第3行将multisig的法定人数设置为2(2-of-3)。该执行路径可以通过在解锁脚本的末尾放置 TRUE TRUE来选择:
0 TRUE TRUE
提示
此解锁脚本开头的0是因为CHECKMULTISIG有个bug,它会导致从堆栈中弹出一个额外的值。 额外的值会被CHECKMULTISIG忽略,但是它必须存在,否则脚本验证将失败。 通常往栈中推进一个0是该bug的变通方案,如“A bug in CHECKMULTISIG execution”章节所述。
第二个执行路径只能在UTXO创建30天后才能使用。 那时候,它需要律师Abdul和三个合作伙伴其中之一(a 1-
of-3 multisig)的签名。这是通过第7行实现的,该行将multisig的法定人数设置为1。要选择此执行路径,解锁脚本将以FALSE TRUE结尾:
0 FALSE TRUE
提示
为什么先FALSE后TRUE? 反了吗?这是因为这两个值是被推到堆栈中的,所以先推进FALSE,然后推进 TRUE。 因此,第一个IF操作码首先弹出的是TRUE。
最后,第三个执行路径允许律师Abdul单独花费资金,但只能在90天之后。 要选择此执行路径,解锁脚本必须以FALSE结束:
FALSE
尝试在纸上运行脚本来查看它在堆栈上的行为。
阅读这个例子时还需要考虑另外几件事情。 看看你能否找到答案:
词汇:
corporate governance 公司治理
embezzlement 贪污; 侵占; 侵吞公款; 盗用,挪用;
cumbersome 笨重的; 麻烦的; 累赘的,难以携带的; 缓慢复杂的,冗长的;
be borne by 由…负担
notarization 公证
instantaneously everywhere 随时随地