作者:Anony

前篇文章见此处

在上一篇文章中,我们介绍最常用的比特币复杂脚本模块:多签名(multisig)。在介绍了多签名可以应用的诸多场景之后,我们还介绍了比特币脚本的流程控制功能(条件语句),这种功能让我们可以在同一笔资金上施加多个并列的解锁条件。如果我们只有多签名这种模块,能够运用条件语句来组合的东西也将是十分有限的。但是,如果我们还有别的模块,它们就可以组合出更加奇妙的东西。

在本文中,我们将学习另一种人们耳熟的模块:时间锁。

“时间锁” 简介

“时间锁” 就是在某一个时间事件发生后才能打开的锁,即,为了通过这样的操作码的检查,由某种方式取得的当前时间已经越过了脚本预先指定的时间。

比特币系统可以实现两种时间锁 1:一种称为 “绝对时间锁”,在一个 “具体” 的时间点(比如某个日期或某个区块高度)后解锁;另一种称为 “相对时间锁”,在一定的时延(比如区块的数量)之后解锁,时延的起点就是该时间锁所在脚本(输出)得到确认的时间。

举个例子,绝对时间锁的意思是:“这笔钱在区块高度 350000 后才可花费”;而相对时间锁的意思是:“这笔钱在得到区块确认的 2 个区块之后才可花费”。

至于时点和时延的描述方式,有两种:(1)区块高度(区块数量);(2)秒钟(或以 512 秒为单位的区间);绝对时间锁可以精确到秒(它是一个 unix 时间戳),相对时间锁则以设定的数值乘以 512 决定(以秒计算的)时延的长度;在检查使用这种度量方式的时间锁时,需要确定一个 “网络当前时间”,而比特币使用的是 BIP 113 2 所描述的 “过往中值时间”,即过去 11 个区块的时间戳的中间值。

不论是设定时点还是设定时延,都有自己的用途;不论使用区块高度还是秒,也都有自身的复杂性和不确定性,但是,当我们接触到具体的应用之后,我们会发现这种复杂性是无伤大雅的,绝大部分应用都不需要依赖于绝对精确的时间。

比特币可以在交易层面设置时间锁,也可以在脚本中设置。出于本系列的目的,我们只介绍脚本层面的时间锁:OP_CLTV 3 和 OP_CSV 4。值得一提的是,两者都是在 2016 年 7 月的软分叉 5 中引入比特币的。

时间锁脚本示例

在本系列的上一篇文章中,我们介绍了两种 Policy 函数:thresh(),用于设定门限(阈值)条件;pk(),用于指定公钥和签名检查。

在这里,我们要引入以下几种 Policy 函数:

  • after(NUM)older(NUM),分别对应我们上文说的绝对时间锁和相对时间锁;
  • and(,),表示在括号里的条件都需要满足,才能花费;各条件以逗号分隔;
  • or(,),表示在括号里的条件只需满足一个,即可花费;各条件以逗号分隔;没错,这里的 and 和 or,就对应于我们在上一篇文章中提到的流程控制语句(条件语句);随着我们掌握的脚本模块越来越多,我们也越来越需要它们来帮我们实现更复杂的花费条件。

那么,我们先来编写这样一种脚本:它意味着,我(作为资金的主人),光凭我自己就可以花一笔钱,但不能立即花费出去,必须在区块高度 800000 之后才能花费。用 Policy 语言来描述一下这个花费条件:

and(after(800000), pk(Alice))

使用 Miniscript 编译器 6,我们将它编译成 Miniscript 代码:

and_v(v:pk(Alice),after(800000))

而它对应的 Script 代码是:

<Alice> OP_CHECKSIGVERIFY <00350c> OP_CHECKLOCKTIMEVERIFY

我们再想一个更复杂的策略:我可以随时花费一笔钱,但如果这笔钱很长时间(比如 3 个月)都没有动用,则我指定的另外三个公钥中的其中两个,可以花费它。描述这种条件的 Policy 语句是:

or(pk(Alice), and(older(12960), thresh(2, pk(Bob), pk(Carol), pk(David))))

编译成 Miniscript 代码:

or_d(pk(Alice),and_v(v:multi(2,Bob,Carol,David),older(12960)))

再编译成 Script 代码:

<Alice> OP_CHECKSIG 
OP_IFDUP OP_NOTIF
  2 <Bob> <Carol> <David> 3 OP_CHECKMULTISIGVERIFY <a032> OP_CHECKSEQUENCEVERIFY
OP_ENDIF

代码中的 OP_IFDUPOP_NOTIFOP_ENDIF 就扮演了流程控制的功能。值得提醒的是,在这两段 Script 代码中,时间锁检查都被放在了后面;而在常见的一些 Script 示例中,作者手写的代码通常会把时间锁检查放在签名检查的前面,就连 BIP-0112 的示例代码也不例外(可能因为我们人类的思维习惯就是这样的,要把最重要的前提写在前面)。这也说明了,不同的 Script 代码也许能实现相同的花费条件,而一旦脚本公钥不同,脚本签名也必须不同。我们需要 Miniscript 这样的工具,来帮我们驯服其中的复杂性。

下面,我们来看看时间锁有哪些用途吧。

时间锁应用场景

个人的强制储蓄

这就是上一章中的第一种花费条件的作用。它可以帮助个人强制储蓄:一旦用户的比特币资金使用这种方式锁定,就不可能在 TA 自己设定的时间点之前花费。资金完完全全属于用户自己,无需托管给任何人,其保管方式跟用户常用的单签名钱包只有细微的差别(需要用户备份赎回脚本,可以通过备份 Miniscript 代码和输出描述符 7 来解决)。

可以协助这样的强制储蓄的产品在比特币世界还不存在,但在现实世界里,强制储蓄服务是有市场的。

灾备措施/意外应对

人总会遇到意外,这些意外可能大到我们无法再跟家人交代重要的事。在保管比特币的同时,我们显然也应该意识到,有一些风险不能仅靠多签名(分散资金的控制权)来消除。时间锁在这里就可以派上用场了:借助条件语句和时间锁、多签名,我们可以实现这样一种花费条件:我(本人)仅凭一个公钥随时可以花费这种脚本锁定的资金;但如果一笔以此脚本锁定的资金长达 3 个月(或 1 年)没有被花费过,则另外三个公钥(我的另一个私钥、我的家人、一个律师所)中的两个可以花费其中的资金。这就实现了一种遗产规划。(注意:这样的条件只能用相对时间锁实现。)

没错,这正是上一章中的第二种脚本可以起到的作用,其 Policy 语句可以稍微修改一下:

or(pk(Alice-1), and(older(12960), thresh(2, pk(Alice-2), pk(Bob), pk(Lawyer-Carol))))

当前尚未有这样的产品。

资金的社交恢复

算是一种特殊灾备措施。使用类似的机制,可以产生这样的效果:假如我弄丢了一把私钥,经过我的 5 个朋友中的 3 个人的帮助,我可以将资金转移到别的地方。跟 “灾备措施” 中的脚本的结构完全一样,都是 “单签名 or 时间锁 + 多签名”。这里的 “单签名” 也可以换成多签名,以减少对朋友的打扰。

跟中心化账户的 “社交恢复” 不一样的地方在于,我们这种社交恢复需要预先指定朋友,而不能等到有需要时再自由指定。

另一个有趣的问题是:假设我们在这里使用单签名,能否应对相关私钥的失盗风险?换句话说,如果有人偷走了我们在这个单签名条款中使用的私钥,但不知道我们有这样的脚本,我们能否放心使用社交恢复机制?

按照我们对 P2SH/P2WSH 的理解,这是不行的,因为 P2SH/P2WSH 输出在花费时需要完整曝光整个脚本;攻击者可以窥伺交易池、等待我们动用社交恢复机制,一旦动用,则整个脚本都会曝光,攻击者就知道了自己手上的私钥可以独自花费这个脚本锁定的资金,然后发起花费相同的输出的交易。在一定条件下,攻击者可以通过提供更多的手续费,抢在我的朋友们的交易得到确认之前拿走资金(假设我的朋友们的交易允许 BIP-125 选择性替换 8 的话);又或者,攻击者可以尝试直接联系矿工。

但是,有没有可能,在我的朋友们动用社交恢复机制的时候,不让外部人知道它还有一个单签名花费条款呢?有的,这就是 Taproot 升级给我们带来的东西,我们在文末揭晓。

暂无这样的社交恢复产品。最接近的用法是利用支持多签名的软件钱包,自己搭建一个使用了朋友的公钥的多签名设置。这样的做法更接近于我们上一篇文章提到的 “免信任托管”。

(加强)免信任的仲裁

多签名本身已经可以提供免信任的仲裁机制,见上一篇文章 9。利用时间锁,我们可以让第三方仲裁者只能在争议发生的一段时间后(表现为合约双方没有使用自己的 2-of-2 多签名解锁资金)才介入。

实际上,这种用法可能对仲裁商更为有用:一个仲裁商可以公开声明,如果你要使用我作为仲裁,请在我所在的条款中加入一个长达 2 个星期的相对时间锁。意思是说:你们应该先有两个星期的私人协商时间,然后再要求我们的帮助。

资金托管者的灾备

上一篇文章的 “联盟侧链” 一节提到,Liquid Network 侧链使用了 11-of-15 的多签名结构来托管资金。多签名虽然分散了资金的控制权,消除了单一私钥的单点故障,但它的容错能力也是有上限的:当(n-m+1)个私钥 损坏/丢失 之后,资金就完全锁死了。这意味着即使是这样大的多签名结构,也需要灾备措施。

一个直觉是,我们可以用条件语句来叠加又一套多签名;但是,这又面临另一个问题:两套多签名结构的资金处置权限完全相同,如何解决其中的利益冲突呢?答案是,使用相对时间锁,仅在第一套多签名长时间无动作时,才允许第二套时间锁动用。

多签名加时间锁,才是完整的资金托管方案。

免信任的服务商

想象这样一种脚本,它有两个条款:(1)Alice 和 Bob 的 2-of-2 多签名;(2)时间锁解锁后,Alice 的单签名。使用绝对时间锁的话,它的 Policy 语句是这样的:

or(5@and(pk(Alice),pk(Bob)),and(after(800000),pk(Alice-2)))

在这里我们使用了一种新的 Policy 记号,就是 数字@,它用来指明 or 的哪一个分支(条款)更常被使用,比如这里的 5,就表示使用它的概率是第二个分支的 5 倍。

对应的 Mniscript 代码:
andor(pk(Alice),pk(Bob),and_v(v:pkh(Alice-2),after(800000)))

对应的 Script 代码:
<Alice> OP_CHECKSIG OP_NOTIF
  OP_DUP OP_HASH160 <HASH160(Alice-2)> OP_EQUALVERIFY OP_CHECKSIGVERIFY <00350c>
  OP_CHECKLOCKTIMEVERIFY
OP_ELSE
  <Bob> OP_CHECKSIG
OP_ENDIF

如果我们使用thresh(2, pk(Alice), pk(Bob)) 而不是 and() 函数,会怎么样?自己在编译器网站里试试看!

你能想象这种脚本的用途吗?我们逐一来分析:

  1. 第一个条款意味着,不论 Bob 想要怎么花这笔钱,都需要经过 Alice 的同意!
  2. 第二个条款意味着,在某个时间点之后,Alice 就可以独自花费这笔钱!

把这里的 “Alice” 和 “Bob” 分别替换成 “用户” 和 “服务商”!这意味着,在这种脚本中,服务商可以帮我们做一些事情,如果我们同意,就可以使用我们注入其中的资金;同时,如果服务商不按我们的要求行动,我们也完全不用担心,因为这笔钱最终是属于我的!

大量的服务,都可以使用这种脚本来去除信任。例如,一个付费观看电影的网站,可以在每次为我提供新电影时都要求我支付(利用第一种条款的单向支付通道特性),但我不需要让资金进入网站的托管,当我停止观看、停止签名链下的支付交易之后,我的资金就不会再减少,网站将提交我之前签过名的交易,拿走属于 TA 的那一份资金。

甚至,连托管式在线密码货币交易所也可以使用这种脚本:用户存入的资金不是进入交易所掌控的单签名输出,而是进入由这种脚本锁定的输出;每当用户需要换取其它密码货币的时候,就签名花费这个输出的多签名条款的交易,而如果用户没有买卖,则在一段时间后,资金会完全回到用户的掌控中。相当于交易所给了一种可信的承诺:我不会挪用您的资金;如果你不使用这笔资金,它会自动回到您的掌控中,不需要您发起取款操作,自然,也不需要您支付取款费用。

现在,已经有使用这种脚本结构的应用了,就是 Lightning Pool 10。当我们学习闪电网络之后,我们会详细介绍这种脚本在闪电网络场景中的用法。

小结

如果说多签名的作用是分配控制一笔资金(参与一个合约)的权限,那么时间锁的作用就是分配不同的人群在不同时间段的权限。在我们上文的几乎所有案例中,时间锁的作用都是在某个时间之后,启用一种新的资金控制方法,使得这种方法既不会与原有的方法冲突,又不至于让资金在原有的方法失效时就锁死。

用智能合约编程的术语来说,时间锁可以在某个时间点后自动启用一种新的合约状态转换规则。而搭配比特币隐藏合约内容(脚本仅在被花费时才曝光)的特性,这意味着比特币链上输出(合约)可以在外人无法察觉的环境中自动启用一条新规则。这其中的意义,怎么夸大都不为过。

时间锁通过流程控制来结合多签名,其想象空间可以说是无穷无尽的。或者说,正是因为它自身如此简单、抽象,我们才会意识到简单的结构可以用在许多场景中,才会为此惊叹。

下一篇文章,我们将学习第三种常见的脚本功能模块:哈希锁。

题外话

上文提到,有一种办法,可以让我们在花费一笔资金时,仅曝光实际被使用的那个条款,而不必曝光完整的脚本(用上文 “社交恢复” 的例子来说,就是只需要曝光社交恢复条款,而不需要曝光另一条单签名条款)。是什么呢,就是 Taproot 升级引入的 MAST(默克尔抽象语法树) 11

MAST 的想法是在 P2SH 的基础上更进一步:P2SH 的意思是,在形成输出的时候,脚本公钥字段记录的不是赎回脚本,而是赎回脚本的哈希值;而 MAST 的想法是,可以记录多个脚本作为叶子所形成的默克尔树的树根值(本身也是一个哈希值),从而:

  • 单个哈希值可以承诺数量巨大得多的条款
  • 在花费时,只需曝光其中一个条款,而无需让所有条款曝光
  • 因为最终只需曝光一个实际被使用的条款,添加并列的解锁条件不会直接影响交易的体积,也就是交易手续费不会因为使用了更大的脚本而线性提高,人们可以使用整体上更复杂、更大的花费条件,不必担心添加条款带来的经济负担

MAST 不仅继承了 P2SH 的良好特性,还进一步发扬光大,提高了隐私性和经济性,让我们可以更放肆地编程比特币脚本。

此外,本文(包括上一篇)文章所用的 Miniscript,编译出来的其实都是在 P2SH/P2WSH 下经济性更好的 Script 代码,而在 MAST 之后,为了将各条款分割到不同的叶子中,我们需要产生不同的 Script 代码和 MAST,这是当前的 Miniscript 还需要进一步开发的地方。

如果说在 Taproot 升级之前,我们在案例中提到的这些脚本结构需要担心自己的经济性、隐私性,在 Taproot 之后,这样的顾虑就可以进一步打消了。

参考文献

1. https://www.btcstudy.org/2021/10/31/bitcoins-time-locks/

2. https://github.com/bitcoin/bips/blob/master/bip-0113.mediawiki

3. https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki

4. https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki

5. https://www.btcstudy.org/2022/09/05/a-complete-history-of-bitcoins-consensus-forks/#2016-%E5%B9%B4-7-%E6%9C%88-4-%E6%97%A5%EF%BC%88%E7%9B%B8%E5%AF%B9%E6%97%B6%E9%97%B4%E9%94%81%EF%BC%89

6. https://bitcoin.sipa.be/miniscript/

7. https://www.btcstudy.org/2022/05/10/what-are-output-descriptors/

8. https://github.com/bitcoin/bips/blob/master/bip-0125.mediawiki

9. https://www.btcstudy.org/2023/04/19/interesting-bitcoin-scripts-and-its-use-cases-part-2-multisig/

10. https://lightning.engineering/pool/

11. https://www.btcstudy.org/2021/09/07/what-is-a-bitcoin-merklized-abstract-syntax-tree-mast/

续篇见此处