作者:Anony

前篇文章见此处

在前面几篇文章中,我们学习了常用的比特币复杂脚本模块:多签名、时间锁、哈希锁。但是,对这些模板的静态分析只是基础,从中我们并不能直接得出某一个场景应该如何编程的答案,也不知道某个场景依据现有模块能否编程出来。为了追寻这些问题的答案,我们需要更仔细地理解场景并回头检查我们的工具。

在本篇中,我们将学习闪电支付通道和闪电网络是如何实现的,并在此基础上了解其它的以脚本实现的特性。

闪电支付通道为我们提供了一个绝佳的例子,展现了比特币脚本编程的灵活性以及应然形态。

需求

作为一种处理系统,区块链有自身的局限性:放在区块中的交易必须在整个网络的所有节点处验证、保存副本,这意味着,它天然存在吞吐量不足的问题:假如我们允许在单个区块内放入更多的交易,就会提高每一个节点的负担,从而削弱网络的生存能力 —— 能够运行节点的人会更少。

那么,如何解决这个吞吐量问题(也称为 “可扩展性问题”)?

如果不介意为此作出的牺牲,则答案是显而易见的:使用托管式交易所,让大量的交易在这样的托管者内部完成,而不发生在区块链上。但是,这就牺牲了免信任性。这是一个非常严重的问题。

想要这个免信任性,就意味着我们需要基于比特币脚本自身的特性,来构造出一种合约:它允许双方无限次地相互支付,而不会占用主链的空间;换句话说,即使他们之间发生了交易并且这些交易没有得到区块确认,双方也知道这些交易在一定情况下是安全的,他们不必信任对手方。

我们能够实现这样的合约吗?

假设合约只需协助一方向另一方持续支付,不必考虑双向支付的可能性,那么这样的合约是很容易实现的,我们在 “多签名” 一章中已经了解过了,就是简单的 2-of-2 合约 1。在签名承诺交易之后,支付的一方签名花费这个 2-of-2 输入的交易,并产生两个输出,一个输出给对方,面额是完成支付后对方应得的数额;另一个输出给自己,面额是自己还剩余的资金。图示:

State 0: Alice 向合约注入 1000 聪(双方需预先签名花费这个 2-of-2 输出、将资金原路返回的承诺交易)
pk(Alice), 1000 satoshi ===> 1000 satoshi, and(pk(Alice), pk(Bob))(下面简称为 “2-of-2”)

State 1: Alice 向 Bob 支付 200 聪
2-of-2, 1000 satoshi ===> 200 satoshi, pk(Bob)
                     ===> 800 satoshi, pk(Alice)

State 2: Alice 向 Bob 支付 400 聪
2-of-2, 1000 satoshi ===> 600 satoshi, pk(Bob)
                     ===> 400 satoshi, pk(Alice)
                     
Bob 知道,即使不广播到区块链上、获得整个网络的确认,这样的支付也是安全的,因为 Alice 已经签名了相关的交易,只需加上自己的签名就可以广播到区块链上,而且 Alice 无法独自花费这个 2-of-2 输出。

显然,这种构造不能满足我们的需求,因为 Alice 无法接收支付:当 Alice 收到 Bob 发来的表达 State 3 的交易、给她支付 100 聪之后,她需要担心 Bob 会不会抢先把 State 2 广播到区块链上,从而赖账。因为这些交易花费的是同一个输出,所以,一旦 Bob 把 State 2 提交到区块链上,State 3 就无法提交上去了(它在花费一个已被花费过的输出)。

也就是说,我们需要实现一种交互方式,让双方都能安全地作废上一个状态、推进合约到新状态,不论上一个状态表示的是谁给谁支付

闪电支付通道

我们依然从上面这个案例的 State 0 开始:

State 0: 在通道中,Alice 具有 1000 聪,Bob 具有 0 聪

假设现在 Alice 要给 Bob 支付 200,他们先互相向对方请求一个哈希值,Alice 给出的记为 H1-A,Bob 给出的记为 H1-B,然后,他们各自签名这样的一笔交易并交给对方:

为推进到 State 1,Alice 签名这样一笔交易并发送给 Bob:
2-of-2, 1000 satoshi ===> 800 satoshi, pk(Alice)
                     ===> 200 satoshi, or(and(pk(Alice), sha256(H1-B)), and(older(1440), pk(Bob)))
                     
为推进到 State 1,Bob 签名这样一笔交易并发送给 Alice:
2-of-2, 1000 satoshi ===> 200 satoshi, pk(Bob)
                     ===> 800 satoshi, or(and(pk(Bob), sha256(H1-A)), and(older(1440), pk(Alice)))

来分析一下这两笔交易的特性:

  • 显然,两笔交易的内容并不相同,但结构相似
  • 每一笔交易的第一个输出都是分配给签名者自己的,是可以立即花费的;第二个输出则有两个花费路径:(1)签名者在知晓对手在本轮中的哈希原像之后,可以立即花费;(2)对手只能在一段时间(大约 10 天)之后单独花费
  • 也就是说,签名者签名的,都是优先给自己分配利益的交易;因此,双方得到的对方签过名的交易,都是优先给对方分配利益的交易
  • 如果后来 H1-A 和 H1-B 的原像因为某些原因而曝光,原像主人就不敢再把对手签过名的交易广播出去了:假定 H1-A 被 Bob 知道了,如果 Alice 把 Bob 签过名的交易(加上自己的签名)后广播出去,该交易会产生两个输出,第一个输出立即给了 Bob 200 聪;而第二个输出,Bob 有足足 10 天的时间,可以使用 H1-A 和自己的签名来解锁它;也就是说,Alice 将血本无归。

现在,我们来看看如何推进到 State 2,这一次,是 Bob 要给 Alice 支付 100 聪:

1. 为推进到 State 2,Bob 请求一个新的哈希值 H2-A 并签名这样的一笔交易、发给 Alice:
2-of-2, 1000 satoshi ===> 100 satoshi, pk(Bob)
                     ===> 900 satoshi, or(and(pk(Bob), sha256(H2-A)), and(older(1440), pk(Alice)))
                     
2. Alice 收到 Bob-State-2 交易之后,给 Bob 传递 H1-A 的原像;Bob 检查原像与 H1-A 匹配之后,给出 H2-B

3. 为推进到 State 2,Alice 签名这样一笔交易并发送给 Bob:
2-of-2, 1000 satoshi ===> 900 satoshi, pk(Alice)
                     ===> 100 satoshi, or(and(pk(Alice), sha256(H2-B)), and(older(1440), pk(Bob)))

4. Bob 收到 Alice-State-2 交易之后,给出 H1-B 的原像

在这里,交互的顺序决定了操作的安全性:

  • (支付者)Bob 提供 Bob-State-2 交易之后,(接收者)Alice 就得到了一个新的承诺,因此可以放心给出 H1-A;从动机上来说,Bob-State-2 会给她更大的好处,所以她不会尝试广播 Bob-State-1 交易(该交易使用了 H1-A),交出 H1-A 的原像算是更明确的承诺。(注意:Bob 只能广播 Alice-State-1!)
  • 如果 Alice 先交出了 H1-A 的原像,就丧失了单方面关闭通道的权力:广播 Bob-State-1 会使 Bob 能够拿走通道中所有的资金。
  • 同理,如果 Bob 先交出 H1-B 的原像,也会丧失单方面关闭通道的权力,所以他必须先拿到 Alice-State-2
  • 如果由 Alice 先给出 Alice-State-2、Bob 给出 H1-B 的原像,那么,Bob 一方面不敢再广播 Alice-State-1,另一方面,广播 Alice-State-2 又会使自己处于不利地位,这时候 Alice 就不会再有动机交出 H1-A 的原像,因为她自己已经获得了足够多的保障。

但是,按照我们这里的流程交互完成之后,双方就成功 “撤销” 了自己签过名(对方手上)的上一笔交易,或者说,合约成功地撤销了上一个状态:任何一方都不敢再广播旧的状态,不论这个状态是否给予了自己更大的利益(跟最新状态相比),因为一旦旧的状态得到区块链的确认,对手就有 10 天的时间将第二个输出中的钱拿走 —— 也就是将整个通道的资金都拿走。

在完成一次状态更新流程之后,任何一方,如果遇到对方不在线、不合作的情形,都可以拿对方签过名的最新状态结算通道、拿回资金(承担资金锁定的代价)。而在状态更新流程中,任何一步不能正确执行,受阻的一方都没有损失:只需拿 TA 已经得到的最新状态,广播到区块链上即可。

综上,我们实现了一种允许双向无限次相互支付的点对点合约 —— 闪电通道!

(这篇文章 2 为闪电通道的概念提供了更细致的解读,适合难以透彻理解上述内容的读者。)

2014 年 1 月 26 日更新:

上文所述使用哈希锁来实现可撤销状态的方法,只能用作教学用途,在如今的闪电网络客户端实现中已不再采用(笔者也不确定是否曾经被采用过);这是因为,我们可以用一种更紧凑的、体积开销更小(从而链上手续费也更少)的方式来实现它。你要不要自己想想看?

在上文的例子中,哈希锁的功能有两个:(1)在原像未揭晓之前,它使得惩罚分支不可被对方动用;(2)在原像揭晓之后,惩罚分支可被对方动用,从而约束过期承诺交易的发布。但通过对公钥做一些操作,可以实现相同的行为。

我们仍以上文的 State 1 为例。Alice 和 Bob 先各自生成一对长期使用的密钥,分别记为 SASB;他们都需要跟对方分享这个公钥。每当他们要更新承诺交易时,就生成新的密钥对。比如在此时,他们分别生成 P1AP1B。他们都向对方请求这个 P1 公钥。然后,Alice 用如下方式求得一个新的公钥:

RA1 = SA * SHA256(SA|P1B) + P1B * SHA256(P1B|SA)

同理,Bob 也可以生成一个新的公钥:

RB1 = SB * SHA256(SB|P1A) + P1A * SHA256(P1A|SB)

由于双方都知道所有公钥,所以都可以验证这两个新公钥是正确的。这两个公钥会用在他们的承诺交易的输出中:

为推进到 State 1,Alice 签名这样一笔交易并发送给 Bob:
2-of-2, 1000 satoshi ===> 800 satoshi, pk(Alice)
                     ===> 200 satoshi, or(pk(RA1), and(older(1440), pk(Bob)))
                     
为推进到 State 1,Bob 签名这样一笔交易并发送给 Alice:
2-of-2, 1000 satoshi ===> 200 satoshi, pk(Bob)
                     ===> 800 satoshi, or(pk(RB1), and(older(1440), pk(Alice)))

你发现了吗?这个 pk(RA1) 的行为跟前述哈希锁是非常相似的:(1)在 Bob 没有公布 P1B 的私钥之前,Alice 并不知道 RA1 的私钥,所以无法动用这个分支(因为 Bob 也知晓 SA,所以可以验证这个公钥的构成,知道当前 Alice 也无法动用它);(2)在 Bob 公布这个私钥之后,Alice 就知道了 RA1 的私钥,因此 Bob 不敢再发布这笔交易。而 Bob 只会在更新状态时揭晓 P1B 的私钥;Alice 亦然。

与此同时,这种构造的体积开销更小:在原本使用哈希锁的构造中,使用这个 “惩罚分支” 你需要发布一个签名和一条哈希值;而在这种构造中,你只需要发布一个签名。

闪电网络

如上一篇文章 3 所述,闪电网络中的支付路由是通过 “哈希时间锁合约(HTLC)” 来实现的。

假定 Alice 与 Bob 有闪电通道,Bob 与 Carol 有通道,且 Alice 知晓这个情况,那么,她就可以通过 Bob 的通道来给 Carol 支付:

初始状态:Alice 900 <===> 100 Bob 500 <===> 300 Carol

Alice 给 Carol 支付 300 聪,完成支付后:
Alice 600 <===> 400 Bob 200 <===> 600 Carol

在这里,Bob 在对 Caorl 的通道中余额减少了 300 聪,但在对 Alice 的通道中余额增加了300 聪。我们这里没有考虑 Bob 可以向 Alice 收取的路由费,在现实中,Bob 是可以得到一些收入的。

在实际的运行中,Carol 先给 Alice 一个哈希值以及自己的节点信息(或可以触达自身的路径信息),Alice 根据自身所知的网络情形,找出触达 Carol 节点(或路径入口节点)的路径,并使用像洋葱一样一层包裹一层的加密消息,逐个告知路径上的每一个节点应该把消息传到哪个节点去;最终,当消息传递到 Carol 时,Carol 交出原像,然后整条路径上的所有通道按递送支付的相反顺序更新,最终 Alice 所在的通道也更新,支付完成(她可获得 Carol 的原像作为支付证据)。

传递支付的每一跳,都是一个通道在内部更新状态,表示状态的交易只需增加一个 HTLC 输出即可,无需人们把交易提交到区块链上。同理,当对手方揭示了原像时,双方就可以更新通道的状态,无需在链上结算 HTLC。

这个过程涉及许多工程上的细节,并指出了许多可以优化的方向。这些内容超出了本系列的范围,在此不表。

闪电网络的优势

除了闪电网络以外,人们还提出过另一种解决这个问题的办法:侧链。也就是建立另一个使用链式结构的执行环境,从而允许人们在这样的环境中交易而不必使用比特币区块链。这种方法可以避免闪电网络的一些缺点:在闪电网络中,你能否给另一个人支付,并不纯粹由你的余额决定,还取决于支付路径上的其它通道的内部状态。但在侧链中,就像在比特币上一样,只要余额允许,任何人都能给任何人支付。

但是,侧链也有自己的挑战:(1)出于比特币脚本现有的功能,我们只能使用多签名合约来托管用户存入侧链的资金,因此,取出资金的安全性依赖于大部分签名成员保持诚实的假设;这意味着,它在免信任性上比闪电通道更差;这一点有可能通过为比特币脚本增加功能来改善;(2)由于侧链模仿了比特币区块链,这意味着它也需要自身的限制吞吐量的机制,否则,攻击者就可以用很少的经济成本发起大量的交易,让网络中的节点崩溃 —— 节点的崩溃意味着失去制约侧链出块者的力量。但是,在闪电网络中,就不需要这样的机制。确切地来说,由于闪电网络是由许许多多支付通道组成的网络,每个通道都可以有自己的限制规则,每个节点都可以实施保护自己的规则,以拒绝垃圾交易的轰炸。同时,闪电网络意味着每个节点都只需关心自己的通道,但依然能利用整个网络的力量。这种分散保存状态、分散处理的架构,让我们获得了理论上最强的可扩展性,这是别的方案无可比拟的

比特币脚本与闪电网络的周边应用

Lightning Pool 与流动性租借

如上面所展示的,一个闪电节点发起支付和收取支付的能力取决于其所参与的通道中的资金状态。在许多情况(总是 发起支付/接收支付/向同一个方向路由支付)下,一个节点将丧失发起支付或收取支付的能力,例如:

Alice 0 <===> 1000 Bob 0 <===> 800 Carol

在这种情形中,由于 Alice 在通道中的余额已用尽,所以 Alice 将不再能通过这条通道发送支付(也不能路由支付);同理,Bob 也将不再能利用这条通道接收支付。在 Bob 的另一条通道中,Bob 无法再发起支付(以及路由支付),但 Carol 也不能再通过 Bob 接收支付了。

这种情形可以通过在上一篇文章中介绍过的 “潜水艇互换” 来解决。例如,Alice 在比特币链上给 Carol 一个价值 500 聪的 HTLC,Carol 在闪电网络中给 Alice 支付 500 聪(也将用到同一个哈希值所构造的 HTLC)。

但还有另一种办法,便是吸引另一个用户 David 跟 Alice 开设通道。Alice 可以向 David 付费,换取 David 主动向 Alice 开启一条通道并注入资金,由此 Alice 就获得了收取支付的能力(入账流动性)。

这样的服务在市场上已经存在了,比如 Bitrefill 提供的 Thor 服务。而 Lighting Pool 4 提出了另一种方法:拍卖。也就是让资金的出租方和需求方在一个拍卖方的主持下开展密封拍卖。决出价格后,资金租赁合约的卖方向买方开设通道,让买方获得入账流动性。在这个过程中,保证卖方不能收钱不办事的办法是跟拍卖员建立 2-of-2 多签名合约;同时,为了去除对拍卖员的信任,时间锁到期后,卖方就能凭自己的公钥直接取走资金,无需任何人的帮助。即:

or(and(pk(卖方), pk(拍卖员)), and(after(date), pk(卖方)))

将资金锁入这样的合约以后,在到期日以前,卖方都无法单方面撤出资金;同时,拍卖员的操作,也都要经过卖方的许可。同时,当卖方向买方开启通道之后,通过路由发送给买方的支付,TA 还可以获得一些路由费。从而,这就产生了一种不确定利率的债权,为比特币提供利息。

(这个脚本模板,我们在 “时间锁” 一篇 5 中介绍过。)

Swap-in-Potentiam:便利闪电用户的收款脚本

同样的脚本模板,还可以用在另一个场景中。

假设你是一个闪电网络用户,你使用常规的比特币脚本来收取链上支付,这些链上支付无法马上进入闪电网络中:

  • 支付交易本身要获得区块确认;
  • 得到确认的交易若要花出去、用来创建闪电通道,又需要等待区块确认;
  • 得到确认的交易若要花出去、使用潜水艇互换获得通道余额,也需要等待区块确认。

但是,如果我们使用上述花费条件来收取链上支付,情形将大不相同:

or(and(pk(收款用户), pk(闪电网络服务商)), and(older(360), pk(收款用户)))
  • 获得链上支付且链上支付得到足够多的区块确认之后,其中的资金将变成一个 2-of-2 多签名合约控制的资金;
  • 由于它是一个 2-of-2 多签名合约,所以收款用户无法独自转移资金;在时间锁到期之前,如果用户使用其中的资金跟闪电网络服务商发起潜水艇互换,那么服务商立即就可以执行,不必等待区块确认 —— TA 知道这样的支付是安全的;
  • 在时间锁到期之前,用户可以发起多次互换,服务商需要在时间锁到期前将最终的状态广播到区块链上;
  • 假定服务商拒绝服务,收款用户只需等待一段时间(在这个例子中不到 3 天)即可让资金的控制权完全回到自己的手中(无需发起链上交易将资金转移到别处)。

小结

在学习比特币脚本的过程中,可能许多人都会疑惑:如此简单的模块,究竟能派什么用场?事实证明,简单的模块也可以组合起来,形成强大的模块。而完整模块的功能,往往是难以从单一模块的功能中看出的。

闪电支付通道,为我们提供了一个绝佳的例子,告诉了我们比特币脚本可以如何协助参与者的互动,以及比特币脚本的编程可以何等灵活。

在闪电通道这个例子中,链上的 2-of-2 合约只是双方互动的基础,这可以从静态分析中理解到;但是,光凭它,是无法实现双向支付通道的,我们还必须使用上述的非对称交易、可以 “撤销” 的状态以及特定的交互顺序。

双方交换的非对称交易,使用带有哈希锁的脚本实现了可以撤销的特性;这种带有哈希锁的脚本在形式上与哈希时间锁合约别无二致,但是,它在这里的作用却跟我们普遍认为的哈希时间锁合约大相径庭。(完整的交易有自己的名称,“RSMC,到期成熟的可撤销合约”。)

2024 年 1 月 24 日更新:

如上一条修订所述,RSMC 并未使用哈希锁,而是使用一种密码学操作实现了相同的效果。

静态分析不是全部。不要浅尝辄止。要有勇气运用你自己的想象力!

(完)

参考文献

1. https://www.btcstudy.org/2023/04/19/interesting-bitcoin-scripts-and-its-use-cases-part-2-multisig/#%E7%82%B9%E5%AF%B9%E7%82%B9%E5%90%88%E7%BA%A6

2. https://www.btcstudy.org/2020/08/23/understanding-the-lightning-network-part-building-a-bidirectional-payment-channel/

3. https://www.btcstudy.org/2023/04/23/interesting-bitcoin-scripts-and-its-use-cases-part-4-hash-locks/#%E9%97%AA%E7%94%B5%E7%BD%91%E7%BB%9C%E4%B8%AD%E7%9A%84%E6%94%AF%E4%BB%98%E8%B7%AF%E7%94%B1

4. https://lightning.engineering/lightning-pool-whitepaper.pdf

5. https://www.btcstudy.org/2023/04/21/interesting-bitcoin-scripts-and-its-use-cases-part-3-time-locks/#%E5%85%8D%E4%BF%A1%E4%BB%BB%E7%9A%84%E6%9C%8D%E5%8A%A1%E5%95%86