作者:Annoy

前篇见此处

在上一篇文章中,我们了解了如何借助比特币的脚本和交易特性,实现双方可以无限次更新状态的共有资金(“通道”);并在此基础上实现可以获得收据的 “支付”。与链上支付相比,这种基于通道的支付具有低手续费、确认速度快的特点,因此扩大了比特币的吞吐量(“可扩展性”)。

但是,这只表明,通道是可以实现的,跨通道的多跳支付也是可以实现的。具体来说,在一个网络中,到底如何运用这些技术,向另一个节点支付呢?本篇将解决这个问题。

本篇所述的内容,将使读者对闪电网络的 “网络” 特性有更直观的理解,也将意识到,这里有多么大的改进空间。本文也将解释一些已经发生和正在发生的改进。同时,其中的关键事实,也解释了闪电网络当前的一些用户体验。

路由

“路由(routing)” 的字面意思是 “寻找路径”。

假设你要去一个从没去过的地方,你需要哪些信息?首先,你需要一张地图;其次,你需要在地图上找出你身处的位置以及目的地的位置;最后,在两个点之间找出一条可以走得通的线路。互联网导航软件在做的也是同样的事情。

在闪电网络中找出由通道前后连接而成的、给特定对象支付的路径,也是一样的道理。为此我们要借助两部分信息。

公开信息

整个闪电网络的公开通道的连接情形,也即网络的拓扑图,在闪电网络中属于公开的信息。任何一个节点都可以从其它节点处获得该节点所掌握的拓扑信息。每一个节点在新建一条公开通道时,也都会向自己所知的其它对等节点播报(channel_announment1,从而更新这些公开的信息。

闪电网络中有两种通道,一种是公开的通道,另一种则是不公开的(unannounced channel)。如此处所述,前者的许多信息属于全网可得的公开信息。后者的这些信息并不向整个网络公开,但有可能被第三方知晓,比如,节点出于收款的需要而主动暴露给支付方。

另一种公开的信息,是这些公开通道的容量(双方共有的资金总额)。新的公开通道在播报出来的时候,也会公开该通道所使用的 UTXO(被称为 “channel point”),该 UTXO 的面额自然也就是这条通道的容量。通道容量的信息在规划支付路径时显然是有用的,它使我们可以不必尝试那些容量显然不够的通道,或者说,可以不在一条通道上尝试超过该通道容量的支付。

但是,这一信息也常常被诟病为对隐私性的侵害:能够同时观察链上数据和闪电网络拓扑信息的人,将知道哪一个节点在使用哪一条通道(哪一个 UTXO),并猜测汇入这个 UTXO 的哪一部分资金属于这个节点,甚至能从该 UTXO 的后续变化中了解该节点的情况,例如,该节点广播一笔非合作式结算该通道的交易之后,使用自己的另一个 UTXO 来为这笔交易追加手续费。

如果有一种技术,可以在不曝光通道 UTXO 的前提下可信地证明其容量,将是对闪电网络隐私性的重大改进。

还有一种公开的信息,是通道在转发特定方向的支付时的收费条件,以及允许 HTLC 留存的时间。由于一条通道有两个端点,所以它可以媒介两种方向的支付。每一种方向的支付的收费条件,都是由负责的节点自己定义的(通过 channel_update 消息来更新)2。显然,这也是对寻路有用的信息,支付方可以优先选择收费更便宜的路线。

值得一提的是,有一种信息并不是全网公开的信息:每一条通道的双方实时余额。显然,如果有这种信息,寻路问题将变得非常容易解决:一条路径到底能不能支付成功,只需观察路径上的通道的实时余额就可以知道了。但是,每个人都知道,这对隐私性来说是多么大的灾难:第三方观察者将能轻松地从通道余额的变动中找出哪个节点在给哪个节点支付、支付额是多少。这是闪电网络作出的取舍,牺牲支付的效率来提高隐私性。我认为这种取舍是值得的。

除了这些公开信息,我们还需借助一些私人信息。

私人信息与发票

为了发起支付,我们需要知道自己在给哪一个节点支付 —— 这就是我们需要接收者提供的信息:接收方节点在网络中的位置。

在最常规的情形中, 这就是收款方要向支付方出示 “闪电发票(invoice)”3 的原因。发票中包含了接收方节点的签名,可用于复原出接收方节点的公钥,也即其 node_id,从而允许我们定位到闪电网络中的具体一个节点。发票中也包含了支付额以及支付哈希值,允许支付方使用这个哈希值来建立 HTLC。发票还可以包含要求支付方写明的备注、支付过期时间等信息。

依据公开信息(地图)和接收方提供的私人信息,我们就可以尝试在网络中找出一条路径,给接收方支付一定的数额。

路由算法

在网络中找出一条令人满意的路径始终是一个有挑战性的事情,因为网络的情形是不断变化的。具备足够容量的通道可能恰好有节点下线了,或者在支付者希望传递支付的方向上没有足够多的余额。或者,虽然能找出路径,但所需的转发费用超出了支付者愿意负担的程度。

当前闪电网络的两个主要客户端实现 LND 和 Core Lightning 都使用 Dijkstra 算法4 的某个变种5 6。该算法常常用于在固定一个起点时寻找触达其它点的最短路径。

不过,闪电网络规范(BOLT)并不规定寻路算法,也就是说人们可以按自己的想法尝试各种不同的算法,这不会导致你的节点无法参与闪电网络。

当前,改进路由算法最新的一些尝试包括:假设通道总是不平衡的(大部分余额会位于一端),以此来猜测并获得已尝试过的通道的一些信息;在支付之前用失败支付来 “打探” 一条路径是否能成功;等等。

支付转发指令

最后一个问题是,假定一个节点掌握了网络的拓扑图,并从接收者传来的发票中得知了接收者节点的 id,以及本次支付的支付额以及支付哈希值;然后,它使用路由算法,在网络中找出了很有可能支付成功的一条路径;那么,它该怎么告诉路径上的节点、请求他们使用 HTLC 来转发支付呢?

就像接力传递 HTLC 一样,这样的指令也是接力传递的。假设现在 Alice 在收到 Diana 的发票之后,找出了一条给 Diana 支付的路径:

Alice --> Bob --> Carol --> Diana

那么:

  • Alice 向 Bob 表示自己可以提供一个 HTLC,同时发送一个加密的数据包,以及一些元数据 7;(双方会更新承诺交易以增设这个 HTLC 输出)
  • Bob 依靠元数据解密数据包后,得到一段可读的消息以及一个新的加密数据包;该消息指示他向 Carol 提供一个 HTLC,并将这个新的加密数据包转发给 Carol;
  • Bob 向 Carol 提供 HTLC 并发送上一步中得到的加密数据包,以及一些元数据;(双方更新承诺交易)
  • Carol 依靠所得的元数据解密所得的加密数据包,得到一段可读的消息以及新的一个加密数据包;该消息指示她向 Diana 提供一个 HTLC,并将新的加密数据包转发给 Diana。
  • Carol 向 Diana 提供 HTLC 并发送上一步中得到的加密数据包,以及一些元数据;(双方更新承诺交易)
  • Diana 依靠所得的元数据解密加密数据包,并从中知晓自己是最终的接收者。
  • Diana 向 Carol 释放原像,双方更新承诺交易。原像一路回传,导致支付路径上的 HTLC 都得到结算。最终,Alice 获得原像,即支付证据。

可见,一个节点在转发支付时要做什么,完全是由发送者在加密消息中指定的。(转发节点为什么愿意遵从这些指令呢?因为他们期待从中收到转发费用。)

发送者在一开始计算好路径之后,就构造出给每一个转发节点的指令,然后以加密的方式,确保相应的指令只能被相应的节点知晓。因此,每一个节点都不知道后续的节点收到的是什么样的指令。

其次,因为每个节点收到的加密数据包中,都没有此前节点的信息遗存,所以每个节点也都不知道自己之前的节点收到的指令。

最后,因为每个节点在解密、删去自己可读的消息之后,往下一个节点传递加密数据包时,会用垃圾数据将它填充到自己所获得的加密数据包的长度,所以,每一个节点收到的加密数据包的长度都是相同的。所以,没有任何一个节点能从这些数据中知道自己前面有几个节点、后面有几个节点 —— 它只知道自己的上一个节点和下一个节点,而无法知晓整条支付路径。这就实现了相当好的隐私性。

这是使用一种叫做 “Sphinx” 的消息构造技术实现的。其关键是按支付路径的倒序构造支付指令和加密包,并且层层加密:Alice 先构造给 Diana 的指令,填充垃圾数据成特定的长度(1300 字节),执行加密;然后在结果的前面加入给 Carol 的指令、删去后面多余的字节使数据成特定的长度(1300 字节),再次加密;再加入给 Bob 的指令、删去多余的字节,再次加密。

如果我们用括号表示加密的话,最终 Alice 发给 Bob 的加密数据包相当于是这样的:(给 Bob 的指令 + (给 Carol 的指令 + (给 Diana 的指令)))。每个人都只能解开一层加密,了解一些信息,然后将剩余的加密数据包(在填充垃圾数据成 1300 字节后)转发给下一个节点。

与加密包伴随的是一段元数据,可被对应的节点用来解密(但对其他人是无用的)。给下一个节点的元数据总能依据本节点得到的元数据构造出来,所以 Alice 只需构造给 Bob 的元数据就可以了。

最终,每个转发节点收到的消息都是 1366 字节,其中 1300 字节是加密数据包;而剩余的 66 字节则是元数据。

这种在构造时层层加密、读取时层层解密的消息,非常类似于洋葱(onion),这就是为什么整套协议被命名为 “洋葱路由”。但它与 “洋葱消息” 是两种独立的特性。

“洋葱消息” 在闪电网络的语境下有特殊的含义,指的是一种 “闪电网络节点间直接通信” 的协议,我们会在下一篇文章中介绍。

此处所说的加密消息包的构造过程图示,可见这篇文章 8

除此之外,由于给每一个中间节点的指令中都包含了其应在通道中保留 HTLC 的时间以及相应的面额,而这两个数值是逐跳递减的(以保证资金安全性并支付转发费用),为了避免中间节点从中猜出接收者节点与自身的距离,还应该在这两个数额中加入一些冗余,或使用多余的跳,以优化接收者节点的隐私性。

结语

虽然发送者节点需要在拓扑图上运行路由算法、构造给路径上每一个节点的指令、每一个节点都要解密消息并给自己的通道对手更新状态,但是,整个过程的带宽和计算负担都很小。每两个节点之间只需传递两次消息,只需更新两次通道状态(一次是增加 HTLC 输出,一次是结算或表示失败),更新通道状态只需要生成一些签名并发送给对方(而无需实际上发送承诺交易,因为承诺交易的构造是确定性的)。所以,闪电网络的用户会发现,在成功的支付中,它的速度总是相当快,一般几秒之内就能确认支付成功。

接下来,我们探讨一些优化措施。这些优化措施分别优化了支付的成功率、发送者的负担和接收者的隐私性。

多路径支付

在上面的例子中,Alice 找出了一条路径给 Diana 支付。但仔细想想,如果支付只能走一条路径,显然是不够理想的。因为闪电网络是一个网络 —— 这意味着 Alice 有多条路径可以触达 Diana,每一条都在所需要的方向上都有一定的支付能力。如果只能走一条路径,就没有用尽闪电网络的潜能,可允许的支付额度会更小,或只能采用更远的路径而使费用变高。

我们仍以第一篇中的例子举例:

甲  <-------->  乙  <-------->  丙  <-------->  丁
500          300  200        300  500        500

甲  <---------------->  戊  <---------------->  丁
500                 200   300                500

如果只能采用一条路径,甲最多只能给丁支付 300 聪;如果能同时运用两条路径,就能支付 500 聪。

幸运的是,闪电网络的支付形式 HTLC 并不妨碍我们通过多条路径送达支付,反正,每一条路径上的每一个节点,要采取什么样的行动,都是发送者节点可以指定的!发送者可以将一笔支付额拆成多笔更小额的支付,每一笔都使用相同的哈希值建构 HTLC,然后使用不同的支付路径送达。这就是所谓的 “多路径支付(MPP)”。

事实上,在发送者给接收者构造的消息中,有两个字段,分别指明当前这条消息将送达的支付数额,以及发送者意图支付的总数额,这就可以告知接收者,发送者是否在使用多路径支付。

敏锐的读者会立即意识到一个问题:拆开的多笔支付不一定能同时送达,甚至可能只有一些会送达,而另一些会失败。这该怎么办呢?如果接收者在只收到一部分支付碎片时就回传原像,岂不就遭遇了损失?答案是,不怎么办 —— 只要接收者节点不在所有支付碎片都到达之前回传原像,就不会遭遇经济损失,而这是接收者节点出于自己的利益,会自然而然采取的行为。当然,在这个过程中,由于成功的路径不能获得回传的原像,各个通道中都会有一些资金被锁定一段时间,但这是可以优化的,发送者可以及时回传支付失败的信息,从而解除资金占用。

LND 客户端实现了一种叫做 “原子化多路径支付” 的技术。顾名思义,就是可以保证所有支付碎片要么一起成功,要么一起失败 —— 尽管仍可能遭遇一些支付碎片无法送达的情形,但接收者却一定不会遭遇损失,不会在只收到部分碎片的时候就返回原像。这是因为,用来构建 HTLC 的哈希值并不是由接收者给出的,而是由发送者指定的,并且发送者将原像的信息分散在不同路径的支付转发消息中。只有所有消息都到达,发送者才能抽取完整的原像信息,并让所有支付路径都回传原像。

这种技术听起来很美妙,但实际上,它却缺乏一个关键的元素:发送者无法收到支付证据。跟这些支付相关的原像信息,是发送者早就已经知道的。这使它更像一种高级的 “Keysend” 功能 —— 我们会在下一篇文章中介绍。

当前,所有主要的闪电网络客户端(Core Lightning、Eclair、LND)都已经实现了多路径支付。

PTLC 与原子化的多路径支付

在上一篇文章中我们提到,PTLC 可以替代 HTLC 并获得更好的隐私性。但 PTLC 能做的还不止如此。

PTLC 与 HTLC 的关键区别在于,它使用了另一种检查秘密值的方式:椭圆曲线点乘法(私钥与公钥的对应)。而它是 “可加的”。即:

a.G + b.G = (a + b).G = A + B

这就意味着,当支付发送者要用 PTLC 交换秘密值(私钥)a 的时候,也完全可以使用公钥 A + B 来检查,只要发送者以某种方式,将 b 告诉支付接收者 —— 而这正是支付转发消息可以做到的事。

因此,发送者可以创建一种基于 PTLC 的、可以收到支付证据的、原子化的多路径支付:生成 n 个秘密值 s_i,这些秘密值的和为 s;在不同支付碎片的支付转发消息中告诉接收者不同的 s_i,并在支付路径的最后一跳中要求检查 S + A 的秘密值,也即 s + a。 仅当所有的支付转发消息(支付碎片)都送达的时候,接收者才能知道 s,然后才能在所有支付路径中回传秘密值。而发送者也可以收到支付证据 a

蹦床路由

在上文的例子中,支付路径是由支付的发送者 Alice 根据自己所知的网络拓扑图构造的。这意味着,支付者要先有这个拓扑图。但是,拓扑图既需要存储空间,也会时时变化。到了闪电网络用户这里,就意味着,每次上线,都需要先拨出一个时间来获取拓扑图的更新并完成处理,然后才能支付。对于闪电网络的目标用户(使用移动设备的小额支付用户)来说,这种时延可能会构成很大的困扰。

“蹦床路由(Trampoline payments)” 就是一种解决这个问题的技术,其思路是:假定 Franky 具有完整的拓扑图,而发送者 Alice 没有,但是知道 Franky 的闪电网络位置;那么,Alice 先找出触达蹦床节点 Franky 的路径,然后再由 Franky 找出触达接收者 Diana 的路径。这就把保存和更新拓扑图、规划路径的工作外包给了 Franky。

因为 Franky 要寻找触达 Diana 的路径,Alice 当然要把 Diana 的闪电网络位置告诉他,这就影响了接收者的隐私性。对应的一种解决办法是,使用多个蹦床节点,也即,Alice 指示 Franky 寻找的,不是最终接收者 Diana,而是另一个蹦床节点 Gloria。当需要寻找的节点既可能是蹦床节点、也可能是最终接收者的时候,Franky 自然就不能肯定到底是哪一种情况。

别忘了,在支付路径上,一个中间节点要做什么,能够了解什么信息,完全是由发送者决定的。这给了我们相当大的自由来实现蹦床路由这样的技术。

蹦床路由的另一个缺点是,它可能会使用更长的路径,因此支付者需要付出更高的手续费,但这是可以接受的。

迄今,所有主要的闪电网络客户端都已经实现了蹦床路由。值得一提的是,接收者节点并不需要支持蹦床路由,就能接收支付。

蹦床路由的基本介绍可见这两篇文章 9 10

路径盲化

最后一个我们要介绍的改进,与接收者的隐私性有关。

如前所述,因为洋葱路由的采用,闪电支付的发送方享有非常好的隐私性:支付路径上的转发节点并不知道自己前面有多少跳,也就难以确定发送方的位置。但是,接收者的隐私性,相对来说就没有那么好。转发节点可以通过 HTLC 的持续时间和数额猜测自己跟最终接收者的距离;接收者的节点位置首先要暴露给发送者,还有可能被发送者暴露给第三方(蹦床节点)。这些都是接收者隐私性的瑕疵。

那么,有没有一种办法可以不暴露接收者节点的位置,而依然能接收支付呢?

“路径盲化(route blinding)” 就是旨在解决这个问题的尝试。其想法是,接收者向发送者暴露的不是自己的闪电网络位置(node_id),而是一条可以触达自身的路径;并且,这条路径所经过的节点和通道,是不向发送者暴露的(“盲化”);发送者能知道的仅有这条路径的 “入口节点”,即第一个节点。

实质上,这意味着,支付路径不再是全部由发送者决定的了;相反,接收者选择了一段路径,并将使用这条路径的必要信息(比如到达入口节点时应该为 HTLC 设置的存续时间和面额)告诉发送者。发送者只需用常规的支付转发消息触达入口节点,并递送接收者提前构造的加密消息,就能触达接收者;从入口节点开始,盲化路径上的节点会逐一解密安排给自己的消息,并依据其中的信息转发支付。

这再一次体现了闪电网络的灵活性!

关于 “路径盲化” 的详细描述,可见这份提议 11

当前,路径盲化的详述已经在 2023 年 4 月进入了闪电网络规范(BOLT)#4 12。主要的闪电网络客户端都在致力于实现路径盲化。

值得一提的是,蹦床路由和路径盲化实际上可以相结合。Eclair 客户端就实现了将两者相结合的特性 13

结语

在本文中,我们介绍了一种标准的闪电支付流程所涉及的网络通信步骤:支付的发送者需要依据网络的拓扑图以及接收者所暴露的信息,创建支付路径,并借助节点之间的网络连接逐步转发消息并接力支付。这个过程具备相当好的隐私性,也尽可能降低了节点对稳定的互联网位置的依赖。

然后,我们介绍了几种优化措施:多路径支付、蹦床路由以及路径盲化。它们分别优化了支付的成功率、发送者节点的启动负担以及接收者的隐私性。这些优化措施都围绕着闪电网络的 “网络” 特性而展开。

在下一篇文章中,我们将回归用户体验问题,介绍一个普通用户感知很明显的事物:收款码。

闪电网络的发票本身是一次性的,它包含了一次支付的哈希值及其面额,在支付成功或者超时之后就会作废。这意味着,如果仅有发票这个工具,接收者将无法提供一个可重复使用的收款码。那么,我们该怎么解决这个问题呢?

(未完)

续篇见此处

脚注

1. https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_announcement-message

2. https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_update-message

3. https://github.com/lightning/bolts/blob/master/11-payment-encoding.md

4. https://zh.wikipedia.org/zh-hans/%E6%88%B4%E5%85%8B%E6%96%AF%E7%89%B9%E6%8B%89%E7%AE%97%E6%B3%95

5. https://docs.lightning.engineering/the-lightning-network/pathfinding/finding-routes-in-the-lightning-network

6. https://medium.com/@rusty_lightning/routing-dijkstra-bellman-ford-and-bfg-7715840f004

7. https://bitcoin.stackexchange.com/questions/89542/single-hop-payment-vs-multi-hop-payments

8. https://www.btcstudy.org/2023/05/18/what-is-onion-routing-how-does-it-work/

9. https://www.btcstudy.org/2022/03/29/outsourcing-route-computation-with-trampoline-payments/

10. https://bitcoinops.org/en/topics/trampoline-payments/

11. https://www.btcstudy.org/2023/03/10/route-blinding-proposal-by-bastien-teinturier/

12. https://bitcoinops.org/en/newsletters/2023/04/05/#bolts-765

13. https://bitcoinops.org/zh/newsletters/2024/01/31/#eclair-2811