作者:Sanket Kanjalkar
来源:https://btctranscripts.com/advancing-bitcoin/2022/2022-03-03-sanket-kanjalkar-miniscript/
本文是作者在 Advancing Bitcoin 2022 上的演讲的记录稿。
开场(Jeff Gallas)
下一个演讲者是 Sanket。他在 Blockstream 工作,主要开发 Simplicity 和 Miniscript。Miniscript 也是他今天演讲的主题。欢迎 Sanket !
引言(Sanket Kanjalkar)
各位早上好!今天我想聊聊 Miniscript,这是 Pieter Wuille、Andrew Poelstra 和我的一项工作,也汇集了来自许多比特币人的想法。从 2019 年夏天以来,它一直在变化。但现在,它已经到了一个非常稳定的状态,可以分享出来、让更多人部署了。我的演讲题目是:“Miniscript:可组合、可分析、更智能的比特币脚本”。这是一个简单的背景介绍,如果你对哪一些内容有兴趣,欢迎在演讲结束后联系我,我们可以聊聊。
比特币 Script 当前的问题
为了介绍开发 Miniscript 的动机,我要先讲讲当前的比特币 Script 语言的问题。从非常抽象的层面来说,在中本聪设计比特币时,“不,我不要给某一个人支付,我要给这个人可以使用的一个基于脚本的程序支付” 的想法可谓石破天惊。
(译者注:“脚本(script)” 和 “Script” 两者时常混用,但仔细区分的话,前者指的是比特币 UTXO 的一个部分,用于为 UTXO 设置花费条件;而 Script 指的是一种特定的编写的脚本的方式 —— 直接使用实际需要执行的指令来编写脚本。)
今天的比特币 Script
给大家一个关于脚本的概念,脚本就是一些可以执行的指令(可以运行的程序),由网络中的所有参与者各自运行、检查某一笔交易是不是有效的。但从原理上说,其设计有一些问题。首先,它很难分析。我知道我们很多的比特币人都正确地宣传了其表达能力有限的事实,但就是这样有限的表达能力,我们也不知道如何分析脚本。除非我们部署像 Miniscript 这样的东西(这是一种更为通用、可组合的框架),不然比特币就还是给某个人支付而已。它很难用,而且几乎所有的工具都难用 —— 从 xpub 钱包到多签名。当我们说多签名的时候,我们还要跟一个复杂的词语联系起来,真遗憾。概念上来说,它其实不那么复杂。而且,在 Miniscript 中你拥有所有的定制化工具。比如你要使用 3-of-5 多签名,你想改变其中的某一些东西,那么你需要一个比特币专家来帮你分析你的改动。这是一个来自 BOLT 3(闪电网络 HTLC 规范之一)的例子。
OP_IF
# 用于惩罚交易
<revocationpubkey>
OP_ELSE
`to_self_delay` # 自主决定的时间锁长度
OP_CHECKSEQUENCEVERIFY
OP_DROP
<local_delayedpubkey>
OP_ENDIF
OP_CHECKSIG
我们后面会讲解这个例子,这是一份原样复制的脚本,你很难看出来它想干什么。专家可以看懂这段脚本想干什么,但分辨起来并不容易。
比特币脚本的用法
比特币 Script 的另一个问题,在你想论证一段脚本的正确性时就会体现出来。我们先从一名用户的视角来看看,你不是一个钱包开发者,你只是一个比特币用户,你想把自己的币交给一个复杂的脚本以获得更好的安全性。你必定希望某些事情有一个清晰的答案。
第一个问题是:“只要我能拿到某一些私钥,比如说我的冷钱包私钥和其它一些私钥,我就能花费这个复杂的比特币脚本吗?”现在,要是我丢给你一些脚本,你很难答得上来。你想确保只要你拿着某一些私钥就没有人能偷走你的钱,即使你是在参与一个复杂的多方设置。但是今天你就是没法分析这些东西。
第二个问题是,你可能想确定 “某一些私钥不能单方面花费我的钱”。如果我跟其他人一起参与一个多方合约,而且他们给出了自己的公钥,给定一段脚本,你希望分析某个人能不能独自花费你的钱。
还有,你可能希望了解熔融性问题。在隔离见证升级中,我们解决了最常见的第三方熔融性问题,但依然有可能写出自身就有熔融性的复杂比特币脚本(如果你没有正确地编写的话)。你想确定是否所有可以满足这段脚本的方法、所有可能的花费路径,都只有一种花费方法,这样它就不能被任何人熔铸。这是一个大问题,举个例子,如果你在花费一笔交易,你设置了一些交易费,假设见证数据是 100 vbyte,但某人熔铸它、使它变成了 10000 vbyte,那么你的交易就没法确认了。如果你这时候是在使用一些 Layer-2 协议,这就突然变成了一个安全问题,因为你的交易没法及时确认。这些东西你都没法分析出来。
要是我只能突出 Miniscript 的一个亮眼之处,我会说是可组合性。当前,你也没法组合脚本方案。假设你有一个 Bitcoin Core 钱包,一个 Green 钱包(这是你最喜欢的一款钱包),还有一个硬件钱包,然后你想制作一个冷钱包方案,使用这三个私钥中的两个才能解锁。假设 Green 钱包有自身的时间锁。当前你没有任何办法,在高层的工具链中正确地组合这些脚本。还是得找一个专家才能理解这些东西并为你写出正确的脚本。然后,专家帮你做了一个只能用于特定脚本模板的一次性工具。
在大部分有趣的应用场景下,如果你有通用的可组合性,你的钱包应该只关心它们在意的部分,所有其它东西都不需要理解。用户只需要在字面上知道做出来的花费方案是可以接受的。硬件钱包只需要知道,没有自己的私钥,这套花费方案、这个脚本是没法使用的;它只需要提供一个签名。你可能还希望检查其它东西,比如找零输出,等等。Miniscript 提供了一种结构化的方式来分析比特币脚本;而在以前,你只能看出它是一些操作码。
重新思考比特币脚本
因此,我们要尝试重新思考比特币脚本来解决这些问题。我们没法抛开比特币脚本,它就在那里,它的共识代码就是我们卡住的地方。但是,我们能不能用我们已有的东西,把事情做得更聪明些呢?
花费方案(Spending policies)
我们先来看看人们用比特币脚本做什么。人们在比特币脚本中最常使用的三个元素是公钥(用来检查签名)、哈希锁(是闪电网络、哈希时间锁合约以及其它所有互换协议的基础)、时间锁(绝对时间锁和相对时间锁,用来指定什么时候才能花费一笔钱)。然后你想组合这些东西,比如说,一个公钥加(AND)一个哈希锁,或者,一个时间锁加(AND)一个时间锁,或者你想要一个阈值机制,只要 5 个条件中的 3 个满足即可。
一些简单的花费方案
为了熟悉这些东西,我们来看一些简单的花费方案。第一个方案, pk(Alice)
,表示 Alice 应该能够花费这个输出。如果你有一个 2-of-3 的钱包,就可以写成 thresh(2,P1,P2,P3)
,表示这三个公钥中需要只少 2 个提供签名。或者,如果你复杂一点,引入一个联合签名人,就像 Green Wallet 那样,你可以用你思考比特币脚本的方式来编码这些不是比特币脚本的东西。只要用户 U
或者 Green 联合签名人 G
签名了,就可以花费这个输出;或者(OR),在 90 天之后,用户 U
可以独自花费这个输出。如下:
and(U,or(G,after(90 days)))
其它一些复杂的东西,比如 Liaquid 侧链的资金控制机制,一些参与者组成了 11-of-15 的多签名方案;而且,如果他们不响应你的请求,你可以等待 4 周时间,然后就可以动用一些紧急密钥。
or(999@thresh(11,functionary_keys),1@and(after(4 weeks),thresh(2,emergency_keys)))
方案就是分析这些东西最基本的方法。
Miniscript 和 Script
我们看看 Miniscript 跟 Script 相比如何。Miniscript 是一种比特币脚本。我们只是想用不同的方式来分析比特币脚本而已。技术上来说,Miniscript 是 Script 的一个子集,但是,Miniscript 带有一些结构。如果只是在基于堆栈的语言中随意放入一些操作码,就没有什么结构可言。但是,如果你有一个程序 —— 编程依赖于尝试组合东西,尝试自己理解一些东西。你可以用更小的模块开发一个更大的、更复杂的程序。你想要通用的签名操作(generic signing),可以放置一些复杂的方案,举个例子,这是一个 2-of-3 多签名脚本,其中 A、B、C 三个公钥中的任意两个的签名可以花费它,或者,在 10 个区块的相对时间锁之后,公钥 E 的签名可以独自花费它。
or_d (multi(2,A,B,C),and_v(vc:pkh(E),older(10)))
使用比特币脚本的操作码,你可以写出一模一样的比特币脚本;但你可以用一种更加结构化的方法来编码它,而不是直接用比特币脚本的裸字节来分析它。因为你有了结构,语义分析就变得更加容易。只要看这段代码,你很容易就能看出,这是一个 OR(或条件),只要我拥有公钥 A 和 B 背后的私钥,我就可以花费它。因为它是结构化的,所以你可以编写运行在这种结构上的软件,还可以回答这样的问题:“如果我只有紧急密钥,这个输出才刚刚形成,我能花费它吗?” 程序就可以为你输出答案。代码中还有一些奇怪的标记 d
、 v
和 vc
,是组合的规则;这里面有一些技术细节,跟你使用 Miniscript 的方式有关,但用户可以直接忽略。这是为想要编写 Miniscript 代码的人准备的,但如果你只是想分析你的 Miniscript 钱包的行为,你可以忽略这些细节。这个 phk
你可以假设是独立的部分。在你分析比特币脚本时,你可以忘记这些东西。
技术上来说,Miniscript 也是比特币脚本。用高级语言来编码的想法并不新鲜。以前人们也想过这样的东西。有人写过编译器(compiler)。但 Miniscript 有一些独特之处。首先,它属于 Script 。它不会编译成另一种语言。如果你有一段脚本,你可以在 Script 和 Miniscript 之间作一对一的映射。只要你得到了相应的 Miniscript 代码,你也就得到了比特币脚本。虽然底层的东西是相同的、解释器也用相同的方式执行,但 Miniscript 代表了一种不同的思考方法。Miniscript 项目的一个很好的愿景是,开发者应该忘记堆栈的存在。在脚本中,你安排自己想要执行的指令,有一个 OP_CHECKSIGADD 操作码,它会读取 3 个参数,如果通过,就会给数值增加 1
。但有了 Miniscript,你应该直接认为这里有一个 multi
函数,你只需要提供一些东西来满足它。我想要思考的是,我需要什么东西来执行这些代码,而不是应该给脚本机器输入什么指令。所有的转换工作,把脚本转成 Miniscript 代码、把 Miniscript 代码转成脚本,都由 Miniscript 技术自身来完成。开发者只需要思考他们在乎的东西。作为一个开发者,你知道自己想处理什么样的威胁模型,那么你就该专注于此,让 Miniscript 来处理你怎么描述它们以及如何编写成脚本这样的问题。与堆栈方法不同,Miniscript 采用了一种函数式、组合式的方法,真正帮我们以结构化的方法完成了编码。
Miniscript 翻译成 Script
检查公钥签名(check(key))
pk(key) # 此为 Miniscript 代码
<key> # 此为转化成的脚本。下文相同,一行 Miniscript 一行脚本。译者注
pk_h(key)
OP_DUP OP_HASH160 <HASH160(key)> OP_EQUALVERIFY
或条件(X or Z)
or_b(X,Z)
[X] [Z] OP_BOOLOR
or_d(X,Z)
[X] OP_IFDUP OP_NOTIF [Z] OP_ENDIF
or_c(X,Z)
[X] OP_NOTIF [Z] OP_ENDIF
or_i(X,Z)
OP_IF [X] OP_ELSE [Z] OP_ENDIF
这里是一些将 Miniscript 代码转化成比特币脚本的基本示例。这些碎片的构造方式使你总是可以在两者间来回翻译,没有歧义。举个例子, pk
只能翻译成 <key>
。而 pk_h
则是众所周知的公钥哈希值片段,就是 OP_DUP OP_HASH160
,这里不检查签名(不使用 CHECKSIG),只是把公钥推入栈中。
至于 “或”,则有不同的实现方式。但这里的关键想法是,如果只是想理解脚本的意思,你可以直接忽略掉 _d
、 _v
这样的标记。Miniscript 编码器会帮助你知晓如何组合这些东西以创建正确的脚本。但如果你只是想分析这些东西的意思,直接分析这些片段就好。
Miniscript:高位视角
从非常抽象的角度看,每个 Miniscript 表达式都有一些基本的类型( B
, K
, V
, W
),以方便跟堆栈交互。它还有一些类型属性(z
, o
, n
, d
, u
…),用于论证正确性和熔铸抗性。它还有一些封装器(a
, v
, c
…),用于在类型和修正属性之间转化。还有一些组合符,用于组合这些类型(使用 and
、 or
和 thresh
来组合 Miniscript 代码)。你可以把它理解成一种编程语言,这是一种非常粗糙的类比。我们有一些数据类型,还有一些规则决定了组合这些功能的方式。这些规则是由 Miniscript 类型系统决定的。为了在这些类型之间转化,就有了封装器。工程师们可以看看这些东西,要弄清楚如何才能组合这些东西并不难。你可以这样编写 Miniscript 代码:先构造一个方案,然后得到一段 Miniscript 代码,这段代码就表示一段比特币脚本,是跟花费方案一对一映射而成的。这样你就知道需要如何组合这些东西了。或者,你可以写一个编译器工具,直接操作方案,然后输出 Miniscript。
可组合性和通用签名程序(generic signing)
给各位一个具体的例子,让各位看看 Miniscript 在你想要通用的签名程序或者说可组合的脚本时,究竟有多大威力。
关于多签名,我们都讲了好多,人们说这是一种复杂的脚本,而且这就是为什么我们使用定制化工具的原因。当前,如果你想参与一个 3-of-4 的门限方案,假设你有一家大公司而且你负责保管资金,我是参与者之一,而且我有自己专门的方案。出于跟你设置多签名方案同样的原因,我也不想让一个写在纸上的私钥决定这么重要的事情,我也想要使用多签名或者别的复杂协议。为了达到这个效果,你使用的所有设备都需要理解这些脚本模板的意思。如果你看过钱包的代码,一些钱包会看数据的索引,然后判断这是一个公钥,等等。这些东西无法直接很好地组合。但如果我想要一种方案,能够允许我这样做:“这是我的门限方案,而且我有自己的安排。A 和所有其他人都不能强迫我使用哪种路径。我自己的方案是,我可以使用 C1
和 C2
来满足(如果我同时持有两者的话);不然,过了 100 个区块,我可以只使用 C1
或者 C2
来花费。” 这就是我们想要的,可组合性。如果我使用某一个私钥来签名,我可以使用带有 PSBT(部分签名的比特币交易)支持的设备来签名,参与者 A 也可以给出一个签名(如果她要参与这个方案的话)。因为 Miniscript 的结构,我们可以理解它,并知道,或者我可以独自花费它,或者 A、B、C 可以一起花费它。只要它想,就可以增加一项检查;不然,也可以只是为这个脚本提供一个签名。Miniscript 定稿器,组装见证数据的 PSBT 组件可以知道如何为这个脚本创建最终的见证数据。你只需要提供 A
、 B
、 C1
和 C2
的签名,然后它会知道它们是放在 tapleaf(taproot 脚本树的叶子)中的,还是别的地方的。如果在另一个子方案中,你拥有所有需要的签名,这时候你不需要任何定制化的工具,定稿器会检查 PSBT、检查签名然后组合出正确的见证数据。钱包开发者可以忘记脚本,直接使用 PSBT 和 Miniscript 就好;把脚本的执行方式抛在一别,只要给出充足的见证数据,Miniscript 的定稿器会帮你搞定接下来的事情。
语义分析
or_d(multi(2,A,B,C),and_v(v:pkh(E),older(10)))
这就是 Miniscript 在文本格式下的样子。这些是描述符(output descriptor),这些是 Miniscript,如果你只想理解语义,你可以忽略这些下标。软件可以看懂它们并轻松分析这些东西。在一段时间之后,我能否用我的紧急密钥花费这个输出?可以。如果我拥有 A 和 B,我可以立即花费。因为这些结构,回答这些问题变得非常简单。
回看 BOLT3:收款用 HTLC 示例
t:or_c(pk(rev),and_v(v:pk(remote),or_c(ln:older(9),and_v(v:hash160(H),v:pk(local)))))
这是收款用 HTLC 的例子。我看了脚本并手动处理了,(……),有一些工具可以帮助你,从花费方案中建构一个 Miniscript。对软件来说,这种形式容易理解得多;对人来说,也更容易分析。它的优势还不止是它有结构,而且你可以编写工具,读取一些花费方案然后输出 Miniscript 或者一个描述符(如果你把它分割成多个脚本并做成一棵 Taproot 脚本树的的话)。好事情是,计算机很擅长优化这些东西。专家们尝试通过减小脚本的重量来优化闪电网络,但一个编译器输出的最优 HTLC 脚本比手写的脚本还要好。Liquid 侧链也是如此,专家们写了一些脚本,但计算机很擅长暴力搜索,可以找出最优的形式。这种方案的平均花费成本要比那种方案好得多。但这里的关键是,因为它成了 Miniscript,所以它可以和所有别的东西一起工作。
你有一个 3-of- 5 多签名,其中一个是由 BOLT3 方案控制的闪电钱包,还有一些其它公钥。只要定稿器和 PSBT 的更新器可以理解 Miniscript,就万事大吉。你不需要关心操作码,也不需要分析脚本。如果你想分析代码的语义,你可以写一个程序,提问:“要是某人广播了上一个状态,我总是能使用我的撤销密钥来应对吗?” 你可以做到,而且很容易理解。
它给予你的不仅是良好的可组合性(这也能帮到所有未来的钱包),还有互通性。如果你有一些定制化的设置,然后你想迁移到另一个钱包,你导入私钥后也许不能工作。或者,你有一个 2-of-3 多签名设置,然后你想把导入别的地方。如果的钱包支持 Miniscript 和描述符,那你只需要导入描述符就行了,软件会知道如何花费它们、如何签名。
再来说说方案编译器(Policy compiler)。使用方案编译器可以做出更复杂的东西。举个例子,在 Jimmy Song 的幻灯片中,你的方案中有 2-of-2 多签名、2-of-3 多签名,你有不同的办法可以花费。那么你只需要给出 Policy,编译器就能输出一个描述符,然后告诉你如何满足它。你不需要担心它是怎么工作的。Miniscript 已经帮你完成了。你只需要知道 “这是我的方案,这是我编译出来的描述符”。如果你不信任编译器,你可以自己检查它的语义,看它是否满足你的要求。当前还没有实现 MuSig 支持,但即便如此,它也能做复杂的事情,比如搞清楚你想把什么东西放在更高层的叶子中、如何安排条件的位置以尽可能降低你在一般条件下的花费成本。如果你添加了 MuSig 支持,那么程序会知道这些公钥兼容 MuSig,会把它用作内部公钥或者放在顶层,而别的条件放在第三层(举例)。因为我们有这样的结构,而且 Miniscript 也是比特币脚本,我们为编写不同的编译器打开了大门。这里有一种 rust-miniscript 编译器,还有一种 C++ 编译器,欢迎你编写你自己的编译器。这里的关键是,在你生成 Miniscript 之后,你可以作完整性检查,可以看看这个方案是不是你需要的。
实践中的 Miniscript
总结一下,Miniscript 是一种更简单的脚本编写和分析办法。(它可以帮你)忘记操作码,忘记脚本的内部工作原理,专注于思考满足条件。它是可组合的,所以你可以拥有通用的签名程序。darosior 在 Bitcoin Core 上的工作完成后,Bitcoin Core 将成为支持隔离见证 v0 的通用签名程序的钱包之一。现在 BDK 和其它一些钱包也在使用 Miniscript。你可以用它们创建复杂的花费方案,是可以工作的。只有定稿器和更新器需要理解 Miniscript。举个例子,不论你使用一个笨笨的硬件钱包签名器(我知道一些硬件钱包关心哪个是找零输出),还是定制化的签名器,都只需要签名 PSBT 就可以了,它们不需要分辨出什么是什么、签名要放在哪里。Miniscript 定稿器足够聪明,可以从所有的签名、所有的哈希原像中组合出一个见证数据。最后,Script 还有一些奇怪的条件,比如对堆栈深度的限制(limitations on the stack limit),还有时间锁混合(timelock mixing)。即使你制作出了一些花费方案,想把它编译成比特币脚本,因为这些奇怪的相互作用,可能也无法表示最终实际执行的是什么。举个例子,你可能使用了超过 201 个操作码;你检查脚本、分析它的语义,结果是 “看起来没问题”,但当你试图花费它的时候,你会突然失败,网络会拒绝你的交易。Miniscript 能在静态上帮你确定你的脚本有没有这些情况。我们在 Tapscript 升级中加入了很多东西,使用 CHECKSIGADD 的理由之一,就是 multi
在静态上处理起来很麻烦。现在,我们有了更好的操作码。就像我上面提出的,我们可以使用花费方案编译器,输出 Taproot 描述符,这样,Jimmy 提到的所有例子,Miniscript 都可以帮你完成。
问答
问:Simplicity 会不会让 Miniscript 过时?还是说,我们预期许多人会继续使用比特币脚本?
答:比特币脚本的优点是我们已经拥有它了。如果你要在比特币中部署 Simplicity,你还需要另一次软分叉。Simplicity 还没准备好。人们对此有不同的观点。如果 Simplicity 能进入比特币,当然可以让 Miniscript 过时,但还没到那个阶段。Simplicity 可以做到 Script 能做的所有事,但不是现在。至于做到了我们想不想要它,那就是另一个问题了。
更多资源
最后,这是 darosior 给 Bitcoin Core 提的一个 PR。你可以看看其中的参考文献,再去提问。有人已经开发了很棒的前端工具来帮你理解脚本,所以你不需要担心这些。你可以看看这些不同的实现,看看能不能用在你自己的项目中。rust-Miniscript 用在了 BDK 钱包项目和 Sapio 中。而且,如果你是一名钱包开发者,你应该使用 Miniscript,因为它能帮你平滑地跟钱包的其余部分交互。如果你在设计一款新钱包,除非你在搞一些很时髦的事情、你写的脚本已经是 Miniscript 了,不然的话,你可以完全支持 Miniscript,这样你的钱包就有了通用签名的能力。当未来的用户使用闪电钱包或者 Trezor 或者 Ledger 或者 Jade 这样的硬件钱包时,你的钱包可以直接跟它们互动。
参考:https://bitcoin.sipa.be/miniscript/
C++ 实现:https://github.com/sipa/miniscript/
darosior 给 Bitcoin Core 提的 PR:https://github.com/bitcoin/bitcoin/pull/24148
Rust 实现:https://github.com/rust-bitcoin/rust-miniscript
min.sc:https://min.sc/
BDK 编译器试玩:https://bitcoindevkit.org/bdk-cli/playground/
miniscript.fun: https://miniscript.fun
(完)