作者:Pieter Wuille & Andrew Poelstra

来源:https://medium.com/blockstream/miniscript-bitcoin-scripting-3aeff3853620

img

引言

比特币脚本

比特币一直都有一种机制将币的花费条件变得更加复杂(而不是只要你有私钥就一路畅通):Script(脚本)机制。虽然 Script 主要用于单密钥支付,它也是许多技术(比如多签名钱包、原子化互换结构以及闪电网络)的基础。

然而,这还不是它的极限。Script 可以用来表示构造一笔交易所需的复杂条件 —— 例如(A、B、C 三把密钥中的任意两个)以及(D 或者(E、F 一道)) —— 还有哈希原像检查、时间锁以及少量更奇特的结构。

难以验证

我们远远没有释放 Script 的全部潜能的一个原因是,想要为一些不简单的场景构造脚本并不容易:验证脚本的正确性和安全性很难,而想要发现最经济的编写方法就更难了。

问题的一部分在于:即使你知道正确的脚本,设计出用户可以参与的应用和协议、构造出使用这些协议的交易,在每次设计出新的构造时都需要大量的专门开发工作。任何形式的灵活性通常都诉诸丑陋的模板匹配来检测兼容的脚本,而每一个增补项都需要更多工程资源用于分析和质保。

能不能让比特币应用可以跟任何脚本一起工作,而不是仅仅能运行少数为之设计的脚本呢?这样我们就不会限于只能使用一次性的设计,并且可以开始设计那些构造和使用实时生成脚本的应用,只看用户的专门需求是什么。钱包的开发者也可以引入更多基于脚本的选择,同时维持与其它钱包的互操作性。

Miniscript

下面我们要介绍 Miniscript,这是一种以结构化的方式来表示比特币脚本的编程语言,从而,它能为高效分析、组合、通用签名等等前景赋能。

比如有这样一个比特币脚本:

<A> OP_CHECKSIG OP_IFDUP OP_NOTIF OP_DUP OP_HASH160 <hash160(B)> OP_EQUALVERIFY OP_CHECKSIGVERIFY <144> OP_CSV OP_ENDIF

这里的 A 和 B 是公钥。这个脚本可以转化成 Miniscript 记法:

or_d(c:pk(A),and_v(vc:pk_h(B),older(144)))

这种记法使之更加清楚:脚本的语义想确保这笔资金在花费时要么得到了 A 的签名,要么是在 144 个区块后得到了 B 的签名。一大部分有意义的脚本都可以用这种办法来编写。

让脚本编程人类可读(至少工程师可读)只是 Miniscript 的特性之一。它的主要目标是在 Script 上实现自动推理(automated reasoning),就如下面的使用场景所示的一样。

使用场景

如今,使用比特币脚本构造复杂花费条件的难度高得没有必要,因为它需要为每一个新的用途开发、测试和部署专门的软件。Miniscript 可以通用化任意的花费条件集合,从而覆盖这些应用场景;而且,通常比专门的解决方案更加简单也更可靠。

最优脚本

其中一种用途是为实现给定的一组花费条件找出最优的脚本。在比特币的 Script 中,它有许多不同的方法来要求一个签名、描述和运算(conjunction)以及或运算(disjunction)、实现一个门限签名。即使是对资深的比特币 Script 开发者,正确的选择也依赖于不同条件被满足的相对概率,而这本身是难以计算的。我们的在线编译器,也可以作为 C++ source 来使用,也是 rust-miniscript 库的一部分,可以即时找出对应于给定的一个花费方式的最优 Miniscript 实现。

引言中的示例可以通过编译下面的条件来获得:

or(99@pk(A),1@and(pk(B),older(144)))

这种方法表示,or 的左分支(A 签名)有 99% 的概率出现,而右分支(B 在 144 个区块后签名)只有 1% 的概率出现。

通用的花费条件

对 Script 的许多应用来说,找出有效率的脚本之后,还有一个常见的障碍是不同的花费机制之间缺乏互操作性。希望实现长期时间锁或者复杂多签名要求的用户,可能因为担心交易对手没有(或者不愿意)使用合适的软件来理解并花费这些条件所控制的资金而却步。

而 Miniscript 因为直接呈现花费条件,可以将任意的条件(policies,后文也译为 “花费方式”)表达得使任何人都可以:

  1. 计算出脚本相应的地址
  2. 确定哪个签名者是签名时候的 必要/充分 参与者
  3. 具有充足的签名时,产生出有效的交易

用户不需要担心所有参与者是否都使用了兼容的软件,以及这个软件是否能存活到资金的时间锁解锁的那一天。

他们也不需要担心自己的需要可能会变化然后跟当前所用的软件不兼容,因此用户可以得到保证,比特币的 Script 不会捆住他们的手脚。

储备金证明

与签名问题相关的问题是储备金证明问题,就是一个公司如何证明自己 可以 花费一组比特币资金而又不必实际上花费它们。虽然这样的工具已经有了,比如 Blockstream 的 Proof of Reserves Tool,但业内还未形成通用的标准。没有 Miniscript,也可能无法形成标准,因为当前大家使用的托管方案太多样了。

花费方式的组合

我们先来看一个比特币 Script 开发者被互操作性的需要困住的例子。设想一个公司,托管着大量的比特币,并且希望这些资金只能在获得大多数董事的同意之后才能花费。但是,一些独立董事希望使用自己已有的钱包软件和硬件,而这些软硬件有复杂的签名设置,要动用放在多个不同位置的私钥;而且他们不想要一种只能使用特定设备来签名的方案。

没有 Miniscript,想要生成可以兼容所有签名者的要求、同时向所有签名者保证所生成的脚本可靠又完整、并且它们的钱包软件会兼容这个结果,将变成一个无法解决的问题。

使用 Miniscript,这些董事可以简单地在自己的钱包软件中组合这些花费方式,从而建构出一个描述了一个门限要求的花费方式,然后将它编译到 Miniscript。他们可以直接验证这个编译器的输出与最初的花费方式相匹配,而且最初的花费方式满足了每个人的要求。然后,他们可以使用任何兼容 Miniscript 的软件来计算接收资金的地址、在花费时收集签名、将这些签名组合成一笔有效的交易。

一个简单的理论案例:一个公司正在设置一个由两位董事参与的定制化多签名存储方案。一个董事已经在用 Blockstream Green 钱包(账户初始配置为 2-2,一段时间后会变成 1-2),而另一位董事正在使用 Electrum 钱包(标准的 1-1)。如果没有工具来组合安全的、可以互操作的脚本,我们是不可能让使用 Green 钱包的董事将自己的花费方式变成公司多签名设置的一个 “参与者” 的。但如果 Green 钱包以及构造这个公司多签的软件都支持 Miniscript,那么双方都可以继续使用自己喜欢的钱包,而且都无需知晓背后的脚本是什么。

动态的联盟

Miniscript 威力的一个具体案例可以在 Blockstream 的 Liquid 侧链中找到。一个当前正在开发的技术,内部称为 “ 动态联盟 ”的,允许当前的 Liquid 成员管理新成员的加入,以及更新控制着这个联盟所托管资金的花费条件的脚本。Miniscript 使联盟成员可以快速而高效地建构出这样的脚本 —— 实际上,Miniscript 编译器发现了当前的 Liquid 脚本的一个更短的 22 字节的版本,轻轻松松就比最初我们煞费苦心人工优化的脚本节约了 5%。但更重要的是,它还使得联盟成员可以自动验证目标脚本的重要特性,从而减少联盟成员一个一个相互协作的需要,以及为目标脚本执行昂贵的人工安全审计的需要。

具体来说,联盟成员可以自动化验证目标脚本包括了:

  1. 他们自己的密钥
  2. 一个编写正确且足够远期的带时间锁的紧急花费条件

Miniscript 也使他们能够验证新联盟的参与者是否能够花费旧联盟所控制的资金。这种检查对于保证交易不会导致资金损失是极为关键的,对想要把资金转移到这个系统的用户也是如此;而如果没有 Miniscript,在现实中也几乎不可能。

历史

Miniscript 的想法形成于 2018 年夏天,是当时正在开发的多个想法的集大成者。在 7 月中旬,Pieter Wuille 为 Bitcoin Core 钱包引入了输出描述符(output descriptor),这是一种描述 Bitcoin Core 所支持的许多不同地址类型的通用方法。同时,那个夏天在 Blockstream 实习的 Andy Chow 提出了部分签名的比特币交易(PSBT,Partially Signed Bitcoin Transaction,由 BIP 174 详述)协议,它作为一种钱包互操作性协议,在钱包领域获得了越来越多的关注。

PSBT 的一个关键部分是 定稿器,某个参与者要负责收集签名者包含在部分签名的比特币交易中的数据并组合成一笔真正的比特币交易。这个组合过程需要理解被签名满足的脚本,这就意味着启用 PSBT 的钱包,如果要做一些复杂的事情,就必须实现自己的专用的定稿器,而这又需要冗余开发并限制了互操作性。

Blockstream 的 Andrew Poelstra 一直在努力为 rust-bitcoin(Rust 编程语言的一个通用比特币库)接受一种 PSBT 实现。这个实现是由 Carl Dong 提出的,但并未包含通用到足以支持该库的所有用户的定稿器。Poelstra 看到 IRC 频道里面有人说“如果你有一种高度结构化的脚本模板,那你即使并不真的理解脚本也没关系了”,这个想法成了 Miniscript 的内核。

不相关的是,Poelstra 和 Wuille 一直在开发一个跟托管有关的项目,并因为缺乏可用于复杂的多参与者脚本的标准工具而遭受挫折。他们两位在 2018 年 8 月碰面讨论这个问题。Wuille 建议实现这些 “高度结构化的脚本模板” 作为 Bitcoin Coin 的输出描述符的一个插件。

随着这个插件成形,它开始分成两个东西:一个是比特币 Script 的结构化子集,也就是 Miniscript;一个是更简单的 “花费方式语言”,可以直接表示花费条件,并且可以编译成 Miniscript。在 2019 年 1 月,Wuille 在斯坦福区块链大会上演示了初步成果

接下来,5 月,Sanket Kanjalkar 加入 Blockstream 开展三个月的实习,专门为 Miniscript 实现工具,并帮助集成到 Bitcoin Core。有了他的帮助,Miniscript 得以完成并多次重新设计成更小、更高效、更容易分析并且抗变形性(against malleability)更好的形式。最后的一组变更是为了验证一大批随机的 Miniscript 兼容脚本是否符合比特币的共识和标准规则。这些迭代的结果就是今天的 Miniscript。

相关工作

Miniscript 基于并整合了比特币生态系统中的许多其它项目。具体来说,如上所述,Miniscript 延伸了 Bitcoin Core 钱包的输出描述符,并与 PSBT 结合来启用完全通用的更新器和定稿器。

Miniscript 可以Ivy 相比较,Ivy 是另一种为了让比特币 Script 的高级特性更易用而设计的语言。不过,虽然 Ivy 可以编译成 Script,Miniscript 是 Script(的子集)的直接表示,意味着 “miniscript 程序” 的正确性和可靠性可以高效地验证。Miniscript 也支持许多其它形式的静态分析,而 Script 和 Ivy 都不可以。

Miniscript 的花费方式语言跟 Ivy 相似,两者都是抽象,都必须编译成 Script 才能在区块链上使用。不过,Miniscript 的花费方式语言跟 Miniscript 自身的结构相似,所以容易验证编译器的输出是否与编译器的输入相匹配 —— 实际上,你甚至可以手动验证 —— 也容易验证它是否与用户的期待相匹配。

另一种跟 Miniscript 相关的区块链语言是 Blocksteam 的 Simplicity。跟 Miniscript 一样,Simplicity 是一种设计成可以直接嵌入区块链交易中的低级语言。此外,Simplicity 也支持许多形式的静态分析,这在部署区块链合约时是至关重要的(但它的一些替代品,比如以太坊的 EVM,从设计上就使得这一点非常难甚至不可能实现)。

Miniscript 与 Simplicity 的区别在于,后者很强大,足以表达任何可计算的函数,而 Miniscript 的表达范围非常有限:它可以表达签名要求、时间锁、哈希原像,以及这些元素的任意组合。这种表达范围上的缩减使得 Miniscript 易于为它所覆盖的应用场景开展分析,更重要的是,这允许它在现有的比特币区块链上工作。相反,Simplicity 就跟比特币 Script 截然不同,依然处于其开发的早期阶段。

未来的工作和结论

在设计 Miniscript 时,我们的目标是让比特币 Script 更加易用。虽然许多工作致力于研究和提议 Script 和比特币共识规则的变更以增加额外的特性,我们感觉到,基础设施的缺失使我们甚至无法通用、安全、可组合并且可互操作地使用已经存在的特性。

这部分工作还没有完成。我们有两个功能性的 Miniscript 实现(C++ 和 Rust)以及花费方式编译器,但为了让这些技术便于使用,我们需要将它们整合进常用的软件中。有了兼容 Miniscript 的 PSBT 实现(更新器和定稿器),许多 PSBT 签名器(包括基于硬件钱包的)将可以用在复杂的脚本中,即使没有明确的支持也可以。这个编译器也可以优化,因为在从花费方式到脚本的转换中,还有许多优化方法尚未被考虑。

在这个过程中,我们学到了许多东西:

  • Script 的资源限制了复杂花费方式的优化:大量的共识和标准化所施加的资源限制(操作码数量上限、脚本体积上限、堆栈层级上限,等等)使得,一旦一套花费方式接近触碰到这些限制,为之寻找最优的脚本就变得格外困难。这是一个有趣的洞见,也许它能引导未来对 Script 的优化。有关联的事情是,我们也惊讶地发现,比特币的共识规则实际上会统计参与一个执行的 OP_CHECKMULTISIG(VERIFY) 的密钥的数量,将它算进每个脚本不能超过 201 个非 push 操作码的限制中。
  • 变化的见证数据规模使得手续费估计变得更复杂:在简单的支付和多签名结构中,见证数据的体积与出现的具体密钥组合无关。然而,一旦我们使用更复杂的脚本,见证数据就会变化,而且可能让手续费估计变得更复杂。具体一个交易输出的潜在签名者,可能需要乐观地猜测可采用的便宜路径并构造出相应的交易。如果这条路径上有密钥或者哈希值不可用,这笔交易就必须改变,手续费也将上升。
  • Segwit 的堆栈编码使优化变得复杂:自 Segwit 之后,输入的堆栈不再被编码为一个脚本,而被直接编码为堆栈元素的一个列表。它有一个糟糕的副作用是把 “true” 的编码从 1 字节变成了 2 字节。因为这一点,Miniscript 需要考虑一个子表达式要放进一个 IF/ELSE/ENDIF 结构的哪一边。
  • 不定形(Malleability):自 Segwit 一来,交易不定形已不再是协议正确性的死敌,但它仍有一些讨厌的效果(例如传播交易时的费率不确定性、降低致密区块(compact block)的传播速度、干扰哈希锁作为全局发布机制的用法)。因为这些原因,我们在设计 Miniscript 时就考虑到了抗变形性,并学会了一般化地推理见证数据的抗变形性。为了实现这一点,Miniscript 依赖于特定的 Segwit 专用规则,因为在前 Segwit 的交易规则中,抗变形是非常难以实现的。
  • 消除共用子表达式不容易:虽然有许多尝试,但 Miniscript 并不支持将重复的子表达式优化出去。虽然在特定的场景中这是有可能做到的,但看起来想要在 Script 中实现一般化是非常困难的。推出特定的堆栈操纵操作码可以改变这一点。

开始在 Miniscript 上开发

有兴趣使用 Miniscript 来开发的开发者可以从我们的在线编译器和分析器开始研究 Miniscript 的构造。也许我们的 rust-miniscript 应用案例也能帮到你。

我们欢迎对 C++ miniscriptrust-miniscript 库的贡献,你也可以在 Freenode 上的 IRC 频道 ##miniscirpt 关注我们团队,了解跟 Miniscript 有关的讨论!

(完)