在本篇中,我们将学习闪电支付通道和闪电网络是如何实现的,并在此基础上了解其它的以脚本实现的特性。
作者:Anony 上一篇
在前面几篇文章中,我们学习了常用的比特币复杂脚本模块:多签名、时间锁、哈希锁。但是,对这些模板的静态分析只是基础,从中我们并不能直接得出某一个场景应该如何编程的答案,也不知道某个场景依据现有模块能否编程出来。为了追寻这些问题的答案,我们需要更仔细地理解场景并回头检查我们的工具。
在本篇中,我们将学习闪电支付通道和闪电网络是如何实现的,并在此基础上了解其它的以脚本实现的特性。
闪电支付通道为我们提供了一个绝佳的例子,展现了比特币脚本编程的灵活性以及应然形态。
作为一种处理系统,区块链有自身的局限性:放在区块中的交易必须在整个网络的所有节点处验证、保存副本,这意味着,它天然存在吞吐量不足的问题:假如我们允许在单个区块内放入更多的交易,就会提高每一个节点的负担,从而削弱网络的生存能力 —— 能够运行节点的人会更少。
那么,如何解决这个吞吐量问题(也称为 “可扩展性问题”)?
如果不介意为此作出的牺牲,则答案是显而易见的:使用托管式交易所,让大量的交易在这样的托管者内部完成,而不发生在区块链上。但是,这就牺牲了免信任性。这是一个非常严重的问题。
想要这个免信任性,就意味着我们需要基于比特币脚本自身的特性,来构造出一种合约:它允许双方无限次地相互支付,而不会占用主链的空间;换句话说,即使他们之间发生了交易并且这些交易没有得到区块确认,双方也知道这些交易在一定情况下是安全的,他们不必信任对手方。
我们能够实现这样的合约吗?
假设合约只需协助一方向另一方持续支付,不必考虑双向支付的可能性,那么这样的合约是很容易实现的,我们在 “多签名” 一章中已经了解过了,就是简单的 2-of-2 合约 。在签名承诺交易之后,支付的一方签名花费这个 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)))
来分析一下这两笔交易的特性:
现在,我们来看看如何推进到 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 的原像
在这里,交互的顺序决定了操作的安全性:
但是,按照我们这里的流程交互完成之后,双方就成功 “撤销” 了自己签过名(对方手上)的上一笔交易,或者说,合约成功地撤销了上一个状态:任何一方都不敢再广播旧的状态,不论这个状态是否给予了自己更大的利益(跟最新状态相比),因为一旦旧的状态得到区块链的确认,对手就有 10 天的时间将第二个输出中的钱拿走 —— 也就是将整个通道的资金都拿走。
在完成一次状态更新流程之后,任何一方,如果遇到对方不在线、不合作的情形,都可以拿对方签过名的最新状态结算通道、拿回资金(承担资金锁定的代价)。而在状态更新流程中,任何一步不能正确执行,受阻的一方都没有损失:只需拿 TA 已经得到的最新状态,广播到区块链上即可。
综上,我们实现了一种允许双向无限次相互支付的点对点合约 —— 闪电通道!
(这篇文章 为闪电通道的概念提供了更细致的解读,适合难以透彻理解上述内容的读者。)
如上一篇文章 所述,闪电网络中的支付路由是通过 “哈希时间锁合约(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)由于侧链模仿了比特币区块链,这意味着它也需要自身的限制吞吐量的机制,否则,攻击者就可以用很少的经济成本发起大量的交易,让网络中的节点崩溃 —— 节点的崩溃意味着失去制约侧链出块者的力量。但是,在闪电网络中,就不需要这样的机制。确切地来说,由于闪电网络是由许许多多支付通道组成的网络,每个通道都可以有自己的限制规则,每个节点都可以实施保护自己的规则,以拒绝垃圾交易的轰炸。同时,闪电网络意味着每个节点都只需关心自己的通道,但依然能利用整个网络的力量。这种分散保存状态、分散处理的架构,让我们获得了理论上最强的可扩展性,这是别的方案无可比拟的。
如上面所展示的,一个闪电节点发起支付和收取支付的能力取决于其所参与的通道中的资金状态。在许多情况(总是 发起支付/接收支付/向同一个方向路由支付)下,一个节点将丧失发起支付或收取支付的能力,例如:
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 提出了另一种方法:拍卖。也就是让资金的出租方和需求方在一个拍卖方的主持下开展密封拍卖。决出价格后,资金租赁合约的卖方向买方开设通道,让买方获得入账流动性。在这个过程中,保证卖方不能收钱不办事的办法是跟拍卖员建立 2-of-2 多签名合约;同时,为了去除对拍卖员的信任,时间锁到期后,卖方就能凭自己的公钥直接取走资金,无需任何人的帮助。即:
or(and(pk(卖方), pk(拍卖员)), and(after(date), pk(卖方)))
将资金锁入这样的合约以后,在到期日以前,卖方都无法单方面撤出资金;同时,拍卖员的操作,也都要经过卖方的许可。同时,当卖方向买方开启通道之后,通过路由发送给买方的支付,TA 还可以获得一些路由费。从而,这就产生了一种不确定利率的债权,为比特币提供利息。
(这个脚本模板,我们在 “时间锁” 一篇 中介绍过。)
同样的脚本模板,还可以用在另一个场景中。
假设你是一个闪电网络用户,你使用常规的比特币脚本来收取链上支付,这些链上支付无法马上进入闪电网络中:
但是,如果我们使用上述花费条件来收取链上支付,情形将大不相同:
or(and(pk(收款用户), pk(闪电网络服务商)), and(older(360), pk(收款用户)))
在学习比特币脚本的过程中,可能许多人都会疑惑:如此简单的模块,究竟能派什么用场?事实证明,简单的模块也可以组合起来,形成强大的模块。而完整模块的功能,往往是难以从单一模块的功能中看出的。
闪电支付通道,为我们提供了一个绝佳的例子,告诉了我们比特币脚本可以如何协助参与者的互动,以及比特币脚本的编程可以何等灵活。
在闪电通道这个例子中,链上的 2-of-2 合约只是双方互动的基础,这可以从静态分析中理解到;但是,光凭它,是无法实现双向支付通道的,我们还必须使用上述的非对称交易、可以 “撤销” 的状态以及特定的交互顺序。
双方交换的非对称交易,使用带有哈希锁的脚本实现了可以撤销的特性;这种带有哈希锁的脚本在形式上与哈希时间锁合约别无二致,但是,它在这里的作用却跟我们普遍认为的哈希时间锁合约大相径庭。(完整的交易有自己的名称,“RSMC,到期成熟的可撤销合约”。)
静态分析不是全部。不要浅尝辄止。要有勇气运用你自己的想象力!
(完)
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!