作者:Anony

一. 引言

长期以来,人们对比特币的可编程性存在许多误解。人们大概知道比特币的可编程性具有许多的局限性,但并不理解边界在哪里,因此既难以把握什么是可以编程的,也不知道什么是不可能编程出来的。

本文的目的正是应对这些挑战,阐释比特币的可编程性的特点及其局限性。为此,我会先解释比特币的一些基本概念、比特币编程的基本工具,以此提炼其特点和局限性。然后,我将介绍两个范例 —— 闪电网络(LN)和谨慎日志合约(DLC)—— 展示它们是如何利用比特币可编程性的特点以及突破其局限性的。

虽然本文的目的是勾勒比特币可编程性的边界,但这并不意味着我认为这是一种缺陷、比特币的可编程性应该往另一个方向改进,相反,从链协议的角度看,形成这样的特性是有理由的。介绍这些理由,也是本文的任务之一。

这里并没有什么创见,有的只是总结。

二. 基本模块与特性

(一)交易、UTXO 和脚本

众所周知,比特币的运行基于两个基本的概念:交易和 UTXO。你可以把比特币理解成一种可以熔化的金属,UTXO 就是一个一个的比特币块,而交易的作用就是把一些比特币块熔化、铸造成新的一些比特币块 [1]。也就是说,UTXO(未花费的交易输出)本身是交易的输出;交易一边消耗以前的交易铸成的 UTXO(以这些 UTXO 为输入),一边形成新的 UTXO;一个 UTXO 一旦被交易花过了,就不存在了。UTXO 有两个基本的属性,一是面值(以 “聪” 来计量);二是解锁条件(熔化条件),由 “锁定脚本”(或者叫 “脚本公钥”)来表达。当一笔交易要花费某个 UTXO 时,就必须提供一些数据(称为 “解锁脚本” 或者叫 “脚本签名”),使之能通过锁定脚本所指定的验证程序,否则就无法花费。

以我们最常接触到的一对一转账为例,其原理大概是这样的:假设你要给 Alice 支付 1 BTC,于是你使用自己可动用的一个或一些 UTXO 作为交易的输入,形成一个面额为 1 亿聪的 UTXO,并在该 UTXO 的锁定脚本中放入 Alice 公钥(并加上检查签名的操作符)。因为没有私钥就无法提供对应公钥的签名,所以这个 UTXO 成为了只有 Alice (的私钥)才能解锁的资金(其他人无法解锁),支付就这样完成了。

由此我们可知,若要编程比特币,使之能为不同的应用场景提供基于技术本身的保障,不外乎要在交易层面或者脚本层面做文章。

(二)工具箱

那么,在比特币编程中,我们经常用到哪些工具呢?

  • 多签名(Multisig)

    • 其锁定脚本形式为 M <PUB-1> <PUB-2> ... <PUB-N> N OP_CHECKMULTISIG ,意思是,在它所记录的 N 个公钥中,需要提供 M 个公钥的签名,才能解锁此 UTXO
    • 常见的应用场景如:个人金库(可用来对抗部分私钥 丢失/暴露 的风险)、家庭联合基金、机构资金管理。闪电网络的基础 —— 支付通道 —— 实际上就是 2-of-2 的多签名输出
  • 时间锁

    • 交易层面和脚本层面都可以安排绝对时间锁和相对时间锁 [2],用来控制交易的有效性以及一个输出什么时候可以被花费
    • 交易层面的绝对时间锁(nLocktime)的意思是说:仅在某个时间点后,这笔交易才会被视为有效交易,才能打包到区块中;而交易层面的相对时间锁(nSequence)实际上是在输入的层面设置的:仅当创造这个输入的交易(即前序交易)得到区块链确认后经过一段时间,这笔交易才是有效的
    • 脚本层面的绝对时间锁是 OP_CLTV 操作码,很直接:在某个时间点之后,才能解锁这个 UTXO;脚本层面的相对时间锁是 OP_CSV,意思是:在创建这个 UTXO 的交易得到区块确认的一段时间后,才能解锁这个 UTXO
    • 时间锁的用途非常广泛。比如你可以基于时间锁和经济激励,让交易 “预约” 在未来的某个时间段上链 [3]。闪电网络中也有许多时间锁的应用
  • 哈希原像检查(也叫哈希锁)

    • 其锁定脚本形式为 OP_HASH160 <hash> OP_EQUAL ,就是检查一个数据的哈希值是否等于锁定脚本中的这个(解锁脚本中的数据是不是锁定脚本中的哈希值的原像)
    • 大名鼎鼎的 “哈希时间锁合约(HTLC)” 就是哈希锁和时间锁的结合:收到支付的一方若不能在一定时间内提供一个哈希值的原像,这笔资金就可以被支付方收回
    • 哈希时间锁合约也有很多应用。假设你要跟对手跨链互换一笔资金,你可以在甲链上给他支付一个 HTLC;他同时在乙链上给你支付一个 HTLC;两个 HTLC 使用同一个哈希值(由先支付 HTLC 的一方给出);那么,这两笔支付就成为原子化的了:一笔支付的成功必将导致另一笔支付的成功,一笔支付的失败必将导致另一笔支付的失败。也就是说,使用 HTLC,我们可以让多笔交易实现原子性。闪电网络中跨越多条通道的支付正是这样实现的
  • 流程控制(并列的解锁条件)

    • OP_IFOP_ELIF 这样的操作码,可以在锁定脚本中安排多种解锁路径,只要任一路径的条件得到满足,就可以解锁这个 UTXO。上文提到的哈希时间锁合约,也是利用了这样的流程控制操作码。HTLC 的完整脚本形式 [4] 是这样的:
    # 检查解锁脚本所提供的 R 是否为 H 的原像
    HASH160 <H> EQUAL
    IF
        # 检查公开 R 的人是否为事务最初的接收者
        <Payee Public key> CHECKSIG
    ELSE
        # 检查时间锁是否已终止
        <locktime> CHECKLOCKTIMEVERIFY
        # 检查请求返回资金的是不是事务最初的发送者
        <Payer Public Key> CHECKSIG
    ENDIF
    
    • 在 taproot 升级 [5] 之后,MAST(默克尔化的抽象语法树)特性允许我们将不同的解锁路径放到不同的默克尔树叶子中,因此不必再显式使用 OP_IF 这样的流程控制操作码。

以上就是我们在编程比特币时经常会用到的工具。下面是一段脚本,请看看你是否能理解其大概意思:

IF
    <Alice 的公钥> 检查签名
ELIF
    3 个月 OP_CSV
    2 <Bob 的公钥> <Carol 的公钥> <David 的公钥> 3 OP_CHECKMULTISIG
ELIF
    1 年 OP_CSV
    <Vincent 的公钥> 检查签名

还有一个很少被人注意,但确实也属于一种编程工具的东西,是签名。

比特币的交易在签名时允许使用 SIGHASH 标签 [6],这些标签为签名的有效性作了额外的指定,允许我们改动交易的部分而不会使签名作废。现在比特币支持的 SIGHASH 标签有:

  • SIGHASH_ALL 表示这个签名签署的是这笔交易的所有输入和所有输出,为被签名的交易增删任何输入和输出都会使该签名作废。用于我们日常的一对一转账
  • SIGHASH_NONE 表示签名所有的输入,但对输出没有任何指定。意思是被签名的交易可以任意调整输出而不会使签名作废,但输入有所改变就会使签名作废。意思像是:“怎么花我的钱我无所谓,但我要求别人跟我一起花,不然我就不乐意”
  • SIGHASH_SINGLE 表示签名所有的输入,但指定了一个输出(跟签名所在的输入的索引号相同的输出)。改变被指定的输出以外的输出不会使这个签名作废
  • SIGHASH_ALL | ANYONECANPAY 签名所有输出,但输入可以任意增加。
  • SIGHASH_NONE | ANYONECANPAY 仅提供签名,不对输入和输出作任何限制。本质上是把签了名的这个 UTXO 完全交出去
  • SIGHASH_SINGLE | ANYONECANPAY 签名了输入以及与该输入使用同一个索引号的输出,但其余的输入和输出都可以改变,签名不会因此而作废。这个标签非常有趣,举个例子,Alice 可以签名价值 1 BTC 的输入和 100 XX 代币的输出(假设 XX 代币可以在比特币区块链上存在),这就变成了一种互换的邀约。任何愿意以 100 XX 代币交换 1 BTC 的人都可以补完这个交易并使之上链

SIGHASH 提醒了我们,交易的签名也可以附带信息,表达签名人对这个签名的用途的希望;它们同样有可能或直接或间接地用在某个场景中。比如说,你可以用 SIGHASH_NONE 实现委托 [7]。

下文我们还将了解到使用数字签名来携带信息的例子:AnyPrevOut 和 Schnorr 适配器签名。

(三)可编程性的限制

从上文中我们可以看出,比特币系统运作的基本模式是这样的:在一个 UTXO 形成时,其锁定脚本表达花费该 UTXO 的充分必要条件;锁定脚本中的操作符指明了数据的验证方式(签名验证、哈希原像验证,等等),而交易(中的解锁脚本)的作用就是提供一些数据,使之能通过锁定脚本所规定的验证程序。如果你所提供的数据不能通过验证程序,你的交易就是无效的,会被比特币网络拒绝;只要你能提供可通过验证程序的数据,你想怎么花这笔钱就随你高兴。

如果你曾接触过其它链协议(比如以太坊这样以可编程性著称的协议),你会意识到比特币的可编程性在这些方面有明显的限制:

  1. 它允许的验证程序不是任意的,只有有限的几种。
  2. 它不允许我们将资金的内部状态存储在脚本中。即使它规定了多条解锁路径,也无法限制各解锁路径能够动用的资金量,只要能解锁资金,运用任何一条路径都可以使用全部资金。也可以说,脚本是不会计算的。
  3. 它不允许我们限制资金的花费方式。在一个 UTXO 解锁之后,怎么花都可以,脚本完全无法限制资金的花费方式。
  4. 解锁脚本所表达的是完备、独立的解锁条件,它不允许我们表达对另一个 UTXO 的依赖。一个 UTXO 不会(也不能)根据另一个 UTXO 存不存在来决定自己能不能解锁,也不会(不能)根据另一个 UTXO 的锁定条件来决定自己能不能解锁。一言以蔽之,在花费一个 UTXO 时,该 UTXO 的锁定脚本和交易所提供的解锁脚本就是一个独立的宇宙,不受任何外在的东西干扰。

我无法证明这个清单是完备的,也许我们能找出别的一些限制,不能归入上述四个类别。但在目前,我们都很容易理解这些限制何以是限制,或者说,这些缺失的特性在哪些场景下是有用的:

  1. 如果比特币允许实现任意的验证程序,那么我们就能更容易运用新兴的密码学技术,比如零知识证明;而且实现新的验证程序将不需要改变整个网络的共识规则(不需要分叉)
  2. 如果资金的内部状态可以存储在脚本中,用户体验会更好:用户可以直接把状态存储在链上,而不必担心丢失;实际上,这意味着在多方参与的协议中,你可以把任意的中间状态存储在链上,不必强求他们同时在线
  3. 可以限制资金的花费方式将允许我们实现更严格的限制。比如,你可以限制一个输出仅能被某一条交易花费。这可用于创造更安全的资金保管方案。实际上,这正是比特币的开发者正在讨论要给比特币增加的功能(“covenants(限制条款)”[8]),只是我们还不知道什么样的方式是最优的
  4. 在现实生活中,许多行为都是条件式的,依赖于另一个经济行为。比如说,在有担保的借贷中,债权人能不能拿走担保品,取决于债务人有没有还钱。如果允许一个 UTXO 表达对另一个 UTXO 的依赖,比特币将更容易实现这样的合约。类似的情形还有很多

我想提醒各位的是,我们在此接触到的并不是一个简单的概念:从开发的便利性和用户体验来说,可编程性的限制当然越少越好,功能性越强越好,但从链协议的角度来看,所有的特性都是有代价,可编程性也一样;在边界之外,是不可能实现的东西,但在边界之内,却依然有许多空间,允许我们发挥想象力;在理论上想象它的局限性是容易的,但切实地理解它的工作原理和想象空间是困难的。

那么,在这些限制之下,比特币可以如何编程?当前的比特币的可编程性到底有多大的空间?

我准备了两个例子来帮助大家做难的这件事。相信这也是各位阅读本文的目的。

三. 比特币编程的范例

(一)闪电网络

闪电网络是建立在比特币上的二层网络,目标是实现便宜快捷的小额支付。它基于两个概念:支付通道交易路由

支付通道是两个参与者开设的一对一合约,其设计目标是允许双方无限次地相互支付、并且支付无需真正用到区块链(显然,如果需要用到区块链,从概念上来说就做不到 “无限次” 相互支付了)。具体的方法是:让双方把资金锁入一个只有双方都同意才能解锁的保险箱(UTXO),然后,双方通过交换带签名、可被覆盖的交易来表示和更新双方的所有权状态(也即双方的余额),从而实现支付的效果;这些交易都是有效的交易,因此它们可以随时拿到区块链上执行,因此这些交易无需得到区块链的确认,依然能为支付提供安全性;又因为这些交易不会用到区块链,而且发送较旧的交易到区块链上(也即欺诈对手)会遭受惩罚,所以双方可以无限次地相互支付;最后,当双方(或某一方)想退出通道时,只需发送一笔交易到链上,即可结算合约。

而当我们利用哈希时间锁合约,将多笔支付捆绑在一起时,我们就可以实现这样的效果:Alice 仅与 Bob 开设了通道,未与 Carol 开设通道;但 Bob 与 Carol 开设了通道,那么,Alice 就可以借助 Bob 给 Carol 支付。也就是说,当支付通道可以被串联起来、当支付可以跨越多条通道来送达,我们就得到了一个网络。这就是支付路由和闪电网络的概念。

下面我们来看看闪电网络是如何用上文介绍的工具来实现的。

插一句话:比特币的交易本质上就是一条消息,只不过这种消息可以打包到比特币的区块中、得到比特币区块链的确认、触发比特币的转移(UTXO 的熔铸)。理解这一点将有助于理解支付通道的概念和闪电网络的强大之处。

一. 通道开启

如前所述,要让支付在链下发生(让收到支付的一方不需要将交易发到链上也能相信自己得到的支付是安全的、不能反悔的),我们需要约束支付方对资金的支配权。办法就是支付方和接收方进入一个 2-of-2 的多签名 UTXO。因为支付方不能任意转移走资金,所以它花费这个多签名 UTXO 所形成的支付,即使没有得到区块链的确认,对被支付一方来说也是安全的。

问题来了,如上所述,UTXO 的脚本中不允许存储资金的内部状态,双方一旦把资金锁入了 2-of-2 的多签名 UTXO 中,就只有双方都合作才能使用这些资金。如何防止对手方耍流氓,故意不配合、要挟你呢?

这就涉及到一个概念,叫做 “承诺交易”。在我们这个例子中,假定支付方和接收方将提供资金形成 UTXO #1,那么,双方就预先签名花费 #1 的交易,比如将 #1 中的资金原样返回给双方。由于 #1 是一个 2-of-2 的多签名输出,这笔承诺交易需要得到双方的签名(双方可以各自给对方提供签名)。而一旦承诺交易得到签名,双方就可以放心签名为 #1 提供资金的交易并广播到比特币网络中,因为他们知道,即使在形成 #1 之后对方突然玩失踪,自己的资金也不会被锁死,这笔承诺交易发送到链上将导致 #1 被花费、自己的资金原样取回。这种提前确定合约状态推进路径的交易,就像是双方给对方的合作承诺,正是它得名的原因。承诺交易的用途非常广泛,实际上,我认为这也应该被当作比特币编程的一种基本工具。

但是,这还仅仅解决了实现链下支付的问题。如何允许双方无限次地相互支付呢?逻辑上来说,这需要我们能 “覆盖” 掉比较旧的支付交易,或者说保证只有最新的支付(所造成的资金分割效果)才能得到区块链的确认。我们知道,由于 UTXO 的锁定脚本表达的是资金解锁的充分条件,而且 UTXO 不会依赖锁定脚本以外的东西,那么,一笔有效的比特币交易将是永远有效的。如何 “覆盖” 掉它呢?

这需要我们进一步编程通道内支付交易的模式。

二. 通道状态更新

假设 Alice 和 Bob 开设了一条支付通道,各存入了 5 BTC。现在 Alice 要给 Bob 支付 1 BTC,于是她编写一条这样的交易,签名后发送给 Bob:

输入 #0:表示 Alice-Bob 支付通道的 2-of-2 多签名 UTXO, 10 BTC

输出 #0 <4 BTC>:
    <Alice 的公钥> 检查签名

输出 #1 <6 BTC>:
    IF
        HASH160 <HASH-B1> OP_EQUAL #这个哈希值由 Bob 给出
        <Alice 的公钥> 检查签名
    ELIF
        <100 个区块> OP_CSV
        <Bob 的公钥>
    检查签名

这笔交易花费了 Alice 和 Bob 形成的 2-of-2 多签名输出,并把第一个价值 4 BTC 的输出安排给了 Alice;第二个输出价值 6 BTC,但有点复杂;它既可以在 Alice 揭晓某个哈希值的原像之后(注意,这个原像在 Bob 手里),也可以在该交易得到确认的 100 个区块之后被 Bob 拿走。

Bob 也给 Alice 签名一条交易,跟 Alice 所签名的交易的结果刚好相反:

输入 #0:表示 Alice-Bob 支付通道的 2-of-2 多签名 UTXO, 10 BTC

输出 #0 <6 BTC>:
    <Bob 的公钥> 检查签名

输出 #1 <4 BTC>:
    IF
        HASH160 <HASH-A1> OP_EQUAL #这个哈希值由 Alice 给出
        <Bob 的公钥> 检查签名
    ELIF
        <100 个区块> OP_CSV
        <Alice 的公钥>
    检查签名

交换完带签名的交易之后,支付就算完成了。当两人下一次需要相互支付的时候,也各自签名格式相似的交易,依旧使用 2-of-2 的多签名 UTXO 作为输入,并使用输出的余额变化来表示支付的数额。同时,每当要支付时,双方就交换上一次支付时用到的原像,由新支付的接收方先给对方揭晓原像,支付方在上一次支付时使用的原像则要等收到对方的新承诺交易后揭晓。

就凭这个结构,我们就能实现通道内双方无限次的相互支付!

来详细分析一下这种模式的效果:

  1. 显然,通道的参与者签名的都是对自己有利的交易(Alice 签名的交易会给自己分配立即就能花费的输出,而给对方分配带有相对时间锁的输出;Bob 也是如此);反过来说,任何一方得到的带有对方签名的交易,都是对自己不利的。也就是说,任何一方,如果想用对方签过名的交易来退出这条通道(广播到比特币网络中交由区块链确认),都会优先把属于对方的资金发给对方,而属于自己的资金至少需要等待一段时间才能解锁。
  2. 在刚刚交换完这种交易的当下,支付的效果是有保证的。以我们这里的 Bob 为例,虽然他得到的交易会给对方分配一个立即可花费的输出,而给自己分配一个带有时间锁的输出,但这个输出确实是 Alice 无法花费的,它的 IF 解锁路径带有一个哈希锁,光凭 Alice 的签名是无法解锁这个输出的。
  3. 在新的一次支付完成,双方交换完上一次支付所用的哈希原像之后,旧的交易就算是被 “覆盖” 了,因为任何一方都不敢拿旧的交易广播到比特币网络中。假设第二次支付是 Alice 发起的,但支付完成之后她动了歹念,将第一次支付时 Bob 签过名的交易加上自己的签名之后上链,则(1)这笔交易会给 Bob 分配一个价值 6 BTC 的、立即可以花费的输出;(2)这笔交易的第二输出有两种解锁路径;Alice 可以在 100 个区块之后使用自己的签名拿走第二个输出中的价值,但是,由于 HASH-A1 的原像已经暴露,因此 Bob 可以抢在 Alice 的前面,通过提供 HASH-A1 的原像以及自己的签名来拿走第二个输出中的价值 —— 等于是通道中所有的资金都会被 Bob 拿走,Alice 血本无归。这就是为什么双方都不敢拿旧的交易上链。这套锁定脚本以惩罚为后盾,实现了 “覆盖” 交易的效果。
  4. 由于得到了对方的签名,双方都随时可以拿着把最新一笔交易广播到比特币网络中,不怕对方故意拖延、阻挠;此外,因为这些交易的输入都是 Alice 和 Bob 的通道开启交易所创造的 2-of-2 多签名输出,最新的交易表达的总是通道最新的状态,当某一方想结算一条通道时,不必提供任何中间状态,只需广播最新一笔交易就好。(修订:在双方合作关闭通道时,可以一起签名一笔交易,为双方都提供立即就可以花费的输出,而免去上述的繁琐脚本。)

综上,这种可以覆盖的交易,帮助我们实现了免信任、可无限次相互支付的通道!(在现实中不是无数次,因为双方都要保存旧交易已经对应的哈希原像,来防止对方欺诈。这种存储负担使得实际允许发生的支付不会是无限次。有趣的是,人们已经提出了一种方案来免除这种存储负担,就是 SIGHASH_AnyPrevOut [9])。

三. 支付路由与闪电网络

如果我们只能实现支付通道,那它只能为需要频繁支付的两方提供便利,并没有特别大的应用价值,也不可能帮助比特币解决扩容问题。但是,我们有哈希时间锁合约。

假设 Alice 要给 Carol 支付,但两人没有直接的通道相连,只是各自跟 Bob 有一个通道。再假设 Alice 在 Alice-Bob 通道中有 3 BTC,而 Bob 在 Bob-Carol 通道中有 2 BTC,那么 Alice 最多能给 Carol 支付 2 BTC。流程如下:

  1. Carol 先生成一个秘密值,哈希计算得到 H,然后她把 H 交给 Alice;
  2. Alice 通过闪电通道给 Bob 支付一个 HTLC,就使用 H 作为锁定条件,并指示 Bob 寻找 Carol;请注意,HTLC 就是一个输出,可以嵌在常规的支付通道状态更新交易中;
  3. Bob 通过闪电通道给 Carol 支付一个 HTLC,也使用 H 作为锁定条件;
  4. Carol 向 Bob 揭示秘密值;Bob 检验秘密值的哈希值是否与 H 一致;若一致,则与 Carol 签名新的通道状态更新交易;
  5. Bob 向 Alice 揭示秘密值;两人签名新的状态更新交易

由此可见,HTLC 可以将通道串联在一起,让没有直接通道相连的用户也能相互支付。它让支付通道可以连结起来形成网络。

四. 闪电网络的优点

  1. 隐私性:在通道中无论发生多少次支付,最终只会有一笔交易被发送到区块链上(加上通道开启交易就是两笔);最大限度地减少了链上足迹
  2. 可扩展性:支付通道内部的交易完全不会用到区块链,也不需要使用区块链来确认,因此,它可以完全不增加主链的负担而实现扩展的效果;此外,在通道余额允许的范围内,支付的速度等同于消息送达的速度(也就是网络连接的速度),双方可以每秒成千上万次支付。

闪电网络的这些特性使之成为了目前为止实现微支付、川流支付的最优方案,任何需要等待交易确认的系统(包括新兴的许多 Layer-2 方案)都无法与之匹敌。

(二)谨慎日志合约(DLC)

谨慎日志合约解决的是如何隐私地使用链外数据的问题 [10]。

如果我们把区块链当成一个执行(结算)经济合约的平台,那我们就必须提供一种方法,使之能使用外部的数据。因为许多有意义的合约都是条件式的:举凡期权、期货、博彩、有担保的借贷,他们都要依赖于一些在合约签订时不可知的数据,来决定合约的结果;总要有人来提供相关的信息,合约才能执行。这些提供信息的人我们称为 “断言机(oracle)”。

在这一点上,比特币可编程性的局限性看起来又阻止了我们。在以太坊这样以链上计算为特点的协议中,断言机可以直接输入数据,并调用链上的计算来决定合约的结果。但这一套在比特币上是不可能做到的,因为脚本操作符给出的是验证方式,我们不能输入任意的数据并要求计算。

有什么办法为合约的执行注入额外的信息呢?有的,上文已经提示过了,我们可以通过签名来注入信息。

一. Schnorr 适配器签名

自 taproot 升级开始,比特币开始支持验证 Schnorr 签名。Schnorr 签名的基本结构是这样的:

Schnorr 签名:
R = r.G  # r 就是这个签名的 nonce(一次性随机数),G 为椭圆曲线的生成点,. 表示椭圆曲线乘法
s = H(R||P||m) * p + r # 表示 R、公钥和被签名消息三者的哈希值乘以签名私钥,加上 r
验签方法:
s.G = H(R||P||m) * p.G + r.G = H(R||P||m) * P + R # P 和 m 都是公开信息,R 是签名者给出的,所以验签者无需知道签名者的私钥,就能验证一个签名 (R, s) 是由某个公钥对应的私钥生成的

Schnorr 签名的结构使我们能够实现一种非常有趣的技术,叫做适配器签名 [11],它有点类似于哈希锁:

假设私钥 p 的持有者生成这样一个签名:
R = r.G + x.G
s' = H(R||P||m) * p + r
显然,这个 (R, s') 不是一个有效的签名。但是,只有有人知道 x,TA 就可以令 s = s' + x,所得到的 (R, s) 就是一个有效的签名。

你可以把它当成一种隐式的哈希锁:只要签名的接收方知道秘密值 x,就能补完这个签名。只不过,这个秘密值是用椭圆曲线点来承诺的,不是用哈希值来承诺的。

Schnorr 签名还有一种有趣的特性:假设你持有私钥 p 并公开了公钥 P,你对外表示将使用 R 生成一条对 m 的签名,这时候,虽然没有人知道 s 是什么样的,但大家都知道 s.G 是什么样的 —— 验签公式就给出了使用 R、P、m、G 计算 s.G 的方法。

二. 谨慎日志合约

假设 Alice 和 Bob 准备各出 1 BTC 打赌后天的球赛的结果,其中,Alice 赌红队赢,Bob 赌蓝队赢。球赛的结果有三种:红赢、蓝赢、平局。对应的赌约结果分别是:Bob 给 Alice 支付 0.5 BTC、Alice 给 Bob 支付 0.5 BTC、资金原路返回就当没事发生过。

假设现在 Carol 公开表示,当球赛结果揭晓时,她会使用自己的公钥 Pc 和 Schnorr 签名算法签名结果,并提前给出了将要使用的 R 值(记为 Rc)。由于 Rc 已知,Pc 对任一种结果的签名 sci 的椭圆曲线点 s_ci.G 都是可知的,当然,sci 本身是不可知的。

Alice 和 Bob 都认为 Carol 是言而有信并且正直的人,于是选择使用 Carol 的签名作为合约结算的条件。流程如下如下:

  1. Alice 和 Bob 先(使用各自的 1 BTC)创建一笔交易,创造一个 2-of-2 的多签名输入(价值 2 BTC);构造好这笔交易之后,他们并不急着签名;
  2. Alice 生成 3 笔交易(称为 “CET”),每一笔交易都花费上述的 2-of-2 多签名输出,但输出各有不同:
    • 第一笔交易对应 Alice 胜出的情形,给 Alice 支付 1.5 BTC、给 Bob 支付 0.5 BTC;
    • 第二笔交易对应 Bob 胜出的情形,给 Alice 支付 0.5 BTC、给 Bob 支付 1.5 BTC;
    • 第三笔交易对应平局的情形,给 Alice 支付 1 BTC、给 Bob 支付 1 BTC。
  3. Alice 分别为上述三笔交易生成 Schnorr 适配器签名,使用 Pc 对各个结果的签名 sci 的椭圆曲线点作为适配点:
    • Alice 为第一笔交易提供的签名是 R_a1 = r_a1.G + s_c1.G, s_a1_ = H(R_a1||P_a||m_1) * p_a + r_a1,其中 m1 就是这笔交易。显然,这不是一个有效的签名,只有为 sa1 加上 sc1,才能使之成为一个有效的 Schnorr 签名。这等于是说,只有当 Carol 签名 “红赢” 并将签名 sc1 发布出来的时候,Alice 的这个适配器签名才能成为一个完整的 Schnorr 签名;否则,这个签名就无法用来解锁 2-of-2 多签名输出
    • 同理,Alice 分别为另外两笔交易生成相应的适配器签名。
  4. Alice 将三笔 CET 以及相应的适配器签名发给 Bob;
  5. (Bob 也依样生成三笔 CET 并构造相应的适配器签名并发送给 Alice);
  6. Alice 和 Bob 将最初创建的那一笔注资交易广播到链上;这笔交易得到区块链确认之后,它会创建 Alice 和 Bob 的 2-of-2 多签名输出,也就意味着两人的合约成立了;(没错,上述两人为之创建适配器签名的交易,在定义上也属于 “承诺交易”)
  7. 后天球赛结果揭晓,Carol 的签名 sci 也揭晓;Alice 和 Bob 任何一方都可以拿着 sci 补完对方所提供的适配器签名,并加上自己的签名使正确的那一笔 CET 成为有效交易,然后将这笔交易广播到比特币网络中,结算这个赌约。

就这样,我们成功把外部的信息,通过 Schnorr 签名和适配器签名注入了合约,使之成为了合约结算的条件。

有了 DLC,许多金融合约都将可以在比特币上实现:比特币的远期合约、差价合约、其它金融资产的期货合约、期权合约,等等等等,这些金融合约实际上都是依赖于价格信息的条件式支付,所以都能用 DLC 来实现。

甚至于,前文提到的有担保借贷,也可以通过 DLC 来实现,只不过要牺牲一些隐私性:债权人向断言机公开自己的还款地址;至还款日时,断言机检查该地址有无余额、有多少余额,并以签名发布结果;债权人凭借这个签名,拿走债务人锁在 2-of-2 输出中的担保品的相应部分并结算合约。(修订:这是一种低效而且缺乏隐私性的方案,可以编程出只需要参与双方互动、更简洁的方案。)

当我们认为锁定脚本无法表达对其它 UTXO 的依赖是一种局限时,DLC 实际上已经突破了这种局限:其它 UTXO(或者地址)的状态,同样是外部信息,与别的外部信息没有什么不同,同样可以借助签名提供给合约。

三. DLC 的优势与不便

DLC 的一个显著的优势是隐私性:只要一个断言机公开了自己的公钥和 R 值,参与合约的双方无需通知这个断言机、无需告知其合约的内容,就可以直接使用这个断言机所提供的数据;此外,当 CET 被发送到链上导致合约结算时,由于其签名就是普通的签名,外人也根本看不出这是个 DLC。这跟当前在其它链上常见的模式 —— 合约的内容完全公开、断言机的身份和影响力公开 —— 完全不同。

而 DLC 最显著的不便在于,它要为每一种可能的结果都生成交易和签名,当合约有非常多种可能的结果时,需要生成和储存的交易和签名的数量也会非常庞大。此外,它还面临激励断言机提供数据和撮合合约参与方的问题。

(三)比特币原生的合约式协议的特点

一如闪电网络和谨慎日志合约,在利用比特币原生的技术编写应用(或者说协议)时,我们一再地接触到比特币可编程性的限制,但也一再地发现在这些限制之下能够编写应用的方法。闪电网络和 DLC 使我们发现,比特币原生的合约式协议有这样一些特点:

  • 我们可以使用交易来表达合约的状态,交易的更替就表示合约状态的变化
  • 如果进入一种状态会面临失控的风险,我们可以通过预先签名承诺交易,来约束进入这种状态之后的走向,从而消除相应的对手方风险。比如,进入一个 2-of-2 多签名输出有可能导致资金被锁定,于是我们先安排一笔花费这个输出的交易
  • 除了退出合约以外,状态的更新总是需要所有参与方在线
  • 表达状态的历史交易需要参与方各自存储(至少在目前是如此)
  • 锁定脚本的主要作用是安排合约参与者的行动优先权,而不是直接产生结果;换言之,基于比特币的协议编程是在运用博弈学

无论你对比特币的可编程性持乐观态度还是悲观态度,想必这份清单想必都会让你产生一种感觉:比特币的编程对应用开发人员和应用参与者都提出了更高的要求 —— 这些限制使得应用开发起来更不直观、更难分析,用户也不能依靠区块链来为他们提供充足的便利,而必须自己承担更多的责任(比如在闪电网络中,你需要自己保管历史状态交易和相应的哈希原像,或者至少要有人负责保管)。

我们不禁要问:这是为什么呢?为什么比特币的开发者不迈出一步,在我们看到的这些方面增强比特币的可编程性,为应用开发者和用户提供更多的便利呢?

或者,我们反过来问:这样限制可编程性,我们得到了什么呢?

四. 可编程性与链协议

无论在哪儿,可编程性始终与链协议(共识规则)息息相关。这不仅是说,链协议决定了可编程性的限制,也是说,为了实现一定的可编程性,链协议也要付出一定的代价(复杂性)。这种复杂性,最终也要由网络中的节点来承担:从短期来看,更复杂的协议需要节点投入更多的资源来运行;从长期来看,更复杂的协议也有更多的失序风险。

假设我们在第二章第(三)节所述的四个维度上全面改进比特币的可编程性,请问我们会得到什么?答案是,我们会得到一个相当接近于当前所谓的通用可编程协议的协议:既可以在链上存储状态,也可以在链上存储控制这些状态的代码,而且这些代码还可以无边界地相互调用 …… 那么,我们需要付出什么样的代价呢?

  • 当一个 UTXO 可以依赖于其它 UTXO 的状态,即同样一笔交易,在系统的状态不同时可能产生不同的结果,因此,这样的交易的长期有效性将得不到保证,很可能系统的状态一改变,一笔交易就从有效交易变成无效交易了;同理,这也意味着,交易的排序将决定交易的经济价值,因为每一笔交易都会改变系统的状态,区块内交易顺序的替换将导致经济利益的再分配,这就会产生所谓的 “MEV(矿工可抽取价值,实则为排序者可抽取的价值)” 问题;
  • 这还意味着,同样的一笔交易(同样的一个操作码),在系统的状态不同时,其执行所需耗费的资源是不同的;我们很可能需要为交易可以耗用的资源施加更复杂的约束,以及持续地调整某一些操作码的资源定价,也即交易的定价体系会变得更加复杂;
  • 链上存储的状态信息一定会变得更多,让节点的负担变得更加重,长期来看对网络的去中心化不利;

如果你觉得这一切似曾相识的话,很遗憾,你的感觉是对的,这就是现在通用可编程的区块链上常常出现的问题。这些问题都与底层的链设计相关。

比特币固守着一些限制,正是为了不走入这些陷阱。比特币的开发者希望比特币交易的有效性是长期有保证的、不希望得到一个复杂的资源定价系统 [12],也不希望链上的状态(UTXO 集)不断膨胀。如果你对比特币的链协议感兴趣的话,我非常推荐你阅读 Jameson Lopp 撰写的文章《比特币最重要的特性是哪些?》[13],他既指出了这些重要特性的相互关系,也指出了这些特性的具体体现以及它们是如何得到维护的;他的文章也为上述所有的局限性提供了链协议设计上的理由:为了让交易能够长期有效、也为了避免争抢进入(race condition),我们把 UTXO 封闭成一个独立的宇宙;为了资源使用最小化,脚本操作符被设计成规定验证方式而非实现计算。这些限制牺牲了应用开发者和用户的便利,但保护了比特币网络的参与者 —— 比特币全节点。

如果用我自己的话来说,比特币链协议的设计目标是 “链上状态最小化(最简化)”。为了防止链上状态的膨胀,脚本被设计成无法存储状态;为了防止状态变得复杂,一个 UTXO 自成一个宇宙,彼此不会依赖;包括使用 UTXO 数据结构而非账户模式,也是因为 UTXO 是可以即用即弃的,它不会像账户那样在弃用之后依然有残留的信息。

最能体现这种思想的是一种输出的类型:P2SH(支付到脚本哈希值)输出 [14]。这种输出将实质上的锁定脚本(称为 “赎回脚本”)作为原像产生出哈希值,在链上仅存储这个哈希值和两个操作码;要解锁这样的输出时,必须提供赎回脚本,以及能够通过赎回脚本验证程序的数据。也就是说,它甚至不希望你把锁定脚本明文存储在链上,因为存储锁定脚本的承诺(哈希值)更加节约。(当然,P2SH 的推出不止这一个原因。)等到隔离见证时,这种思想更加突出:隔离见证输出的锁定脚本字段只余一串乱码,连操作码也完全看不见了 [15]。输出类型上依然有 P2WPKH(支付到隔离见证的公钥哈希值)和 P2WSH(支付到隔离见证的脚本哈希值)的区别,但链上存储的数据(的体积)无疑变得更小了。

讲到这里,我又想插句话,跟上文提到的 “承诺交易(commitment txs)” 有关。

博弈理论也研究 “承诺(promises)” 和 “威胁(threats)” 这样的表态而非实际行动,在博弈中的作用。在博弈中,有些承诺和威胁是 “可信” 的,因为它们的执行成本低于可得到的好处,而有一些承诺是不可信的。博弈理论的创见在于,如果一个承诺(威胁)是可信的,那么承诺的内容不必付诸实践,也能对现实产生影响。这样的例子在生活中比比皆是。人们经常使用威胁来吓阻对手,只要这些威胁有可能实行,不必实行也能起到阻止对方得寸进尺的效果。

我认为,这种观念,最符合比特币开发者对 “可编程性” 和 “智能合约” 的想象。换言之,比特币可编程性的目标,是让交易可以变成一种可信的承诺,来推进双方的博弈动态。承诺交易就是这样的可信承诺:因为它自身是一笔有效的比特币交易,随时可以广播出去、触发资金的结算,因此,它不必真的广播出去,参与的各方也认为这样的交易所造成的状态变更效果,是真实、安全的。

在比特币开发的世界里,被描述成 “智能合约” 的东西(包括哈希时间锁合约 HTLC,闪电通道的状态更新交易 RSMC),实际上都是锁定脚本的模式,它的作用是构造可信的承诺,而不是用于计算。

在闪电网络中利用 HTLC 形成支付路由时,每一个 HTLC 的接收方,都知道这是一种可信的承诺:只要自己能揭开那个哈希值的原像,就能获得支付;因此,这个 HTLC 不必真的上链,接收方也不担心支付方反悔。支付方也知道这个 HTLC 是一个可信的承诺,因此,当接收方能够揭开原像时,支付方会配合地更新通道内的状态,而不是逼迫接收方拿着 HTLC 上链。

下次再有人问你 “比特币是否支持智能合约” 时,请回答 “比特币自始至终都支持智能合约”。但它支持的是基于验证的、无状态的智能合约;而不是以太坊那样基于计算的富状态智能合约。也不必相信未来的某种升级将允许我们在比特币上实现以太坊式的智能合约(在 taproot 升级的时候,经常有人产生这种误解,因为大家都说 taproot “增强了比特币的智能合约功能”),不是技术上不可能,而是那会打破我们一贯以来的共识,不会得到人们的认可的。

五. 结语

本文有多个写作目标,因此叙述显得凌乱。一方面,我希望总结在闪电网络和谨慎日志合约中体现出来的比特币编程的特点,帮助读者了解比特币的编程技巧;另一方面,我也认为,从 “局限性” 的角度来认识比特币的可编程性,也是不可或缺的的,因为这将给我们更清晰的边界感,还能告诉我们比特币链协议演化的方向以及它不会突破的底线;此外,我还希望解释比特币的开发者这样限制可编程性的理由 —— 这事关我们对比特币本身的理解,如果你不能接受这其中的思想,迟早你会站在功能性的角度批评比特币的可编程性。

过去的许多年来,这个行业的发展始终伴随着对功能和性能的强调:人们鼓吹通用可编程的协议,不断推出吞吐量更强的区块链。但是,很少有人来提醒大家,所有的功能和性能都是有代价的。不会有天上掉馅饼的事情。

而比特币,则一贯以 “去中心化” 为理由,限制人们用简单粗暴的方式追求功能、性能和便利性。如果失去了这种坚守,我不知道比特币会变成什么样,也不确定它是否还会得到这么多人发自内心的热爱。

最终,当我们解释了其可编程性的限制背后的理由,我们就得到了一个完整的论述。我们可以说,我们已经找到了比特币稳定不变的核心。而闪电网络和 DLC 则使我们相信,即使它面临这么多的限制,在边界之内,依然有很大的空间,依然可以产生伟大的项目,依然值得我们去探索和开发。

脚注

[1]:这个比喻在某些地方惊人地准确:UTXO 在被交易花费之后就不复存在,就像金属块被熔化之后就不可能再找回完全相同的一块。它来自 Gigi 检讨我们在比特币世界里的用词的文章:https://www.btcstudy.org/2022/07/19/the-words-we-use-in-bitcoin/

[2]:https://www.btcstudy.org/2021/10/31/bitcoins-time-locks/

[3]:https://www.btcstudy.org/2022/08/27/a-protocol-for-having-block-confirmations-in-the-future/

[4]:https://www.btcstudy.org/2021/09/15/lightning-network-in-depth-part-2-htlc-and-payment-routing/

[5]:https://www.btcstudy.org/2021/10/27/taproot-is-more-important-than-your-imageine/

[6]:https://www.btcstudy.org/2021/11/09/bitcoin-signature-types-sighash/

[7]:https://www.btcstudy.org/2022/05/28/delegated-signatures-in-bitcoin-within-existing-rules-no-fork-required/

[8]:https://www.btcstudy.org/2022/05/25/contracting-primitives-and-upgrades-to-bitcoin/

[9]:https://www.btcstudy.org/2022/01/27/breaking-down-the-bitcoin-lightning-network-eltoo/

[10]:https://www.btcstudy.org/2022/08/01/discreet-log-contracts-smart-contracts-for-bitcoin/

[11]:https://www.btcstudy.org/2021/12/02/schnorr-applications-scriptless-scripts/

[12]:在隔离见证之后,比特币交易的定价系统已经变得比原来更复杂了:同样内容和体积的交易,如果使用隔离见证的输入,会比使用非隔离见证输入的版本享有更低的交易费,也即,在众所周知的体积度量以外,它还增加了一个度量。但这样的复杂性在比特币中不是普遍的,它依然要比我们所知的大部分链协议更简单。

[13]:https://www.btcstudy.org/2022/08/05/what-are-the-key-properties-of-bitcoin/

[14]:https://github.com/inoutcode/bitcoin_book_2nd/blob/master/%E7%AC%AC%E4%B8%83%E7%AB%A0.asciidoc

[15]:https://learn.saylor.org/mod/book/view.php?id=36376&chapterid=19010

(完)