0%

闪电网络:技术与用户体验(二):通道与支付

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

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

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

路由

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

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

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

公开信息

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

闪电网络中有两种通道,一种是公开的通道,另一种则是不公开的(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 支付的路径:

1
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,每一条都在所需要的方向上都有一定的支付能力。如果只能走一条路径,就没有用尽闪电网络的潜能,可允许的支付额度会更小,或只能采用更远的路径而使费用变高。

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

1
2
3
4
5
甲  <-------->  乙  <-------->  丙  <-------->  丁
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 的关键区别在于,它使用了另一种检查秘密值的方式:椭圆曲线点乘法(私钥与公钥的对应)。而它是 “可加的”。即:

1
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

闪电网络:技术与用户体验(三):路由

在上一篇文章中,我们了解到,在最初设想的闪电网络支付中,收款方应该向付款方发送一个 “闪电网络发票(invoice)”,使后者能够在闪电网络中找出收款方的位置并通过 HTLC 和中间节点送达支付。作为一段数据,闪电网络可以有各种各样的形式,它可以是一串字符,也可以编码成一个 QR 码。

在支付方和接收方能够面对面的情形中,将闪电发票编码成 QR 码会更加便利,而且跟当今用户习惯的互联网支付没有什么分别 —— 支付方拿出手机,打开闪电 App,然后扫描接收方的 QR 码,App 在后台处理之后,点击确认,完成支付。

但是,这样的 “收款码” 却有一个奇怪的特点:它只能用一次。闪电发票本质上是一次性的,一旦支付成功,或者超时失败,就不能再次使用。

对于做生意的商家来说,这有一点点麻烦,就是每次都必须为用户生成一个新的收款码,但还不算是不能接受。但是,对另一些场景来说,它就很不便利了。比如说:互联网打赏;熟人间的多次来回支付;捐赠。在这些场景中,让接收方手动一一为支付方生成并提供发票,要么在时机上不现实,要么在规模上不现实。

为了实现 “静态的收款码”,人们提出了许多方案。我们先来看第一种。

Keysend

“Keysend” 的想法是:因为节点的 node_id 是不会改变的,而且在给出发票之后就会向支付方暴露,所以,可以用它来作为一个静态的端点。

具体来说,当一个支付方在知道接收方的 node_id 之后,想要再次给 TA 支付,又不便于获取发票时,就这样做:支付方自己生成一个秘密值,并计算出其哈希值;然后,将这个秘密值放在给接收方的支付转发消息中,同时,使用其哈希值来构造传递支付的 HTLC。当加密的消息被一跳一跳传递给接收者节点时,TA 会解密消息,然后知晓其中的秘密值,然后就可以像常规的闪电网络支付那样回传原像。(支付转发消息在闪电网络中传递的详情,请见上一篇文章。)这样一来,支付方无需接收方出示原像,就能在闪电网络中完成一次多跳支付。

Keysend 有许多有趣的用法。比如,你可以用它来实现对一些通道的内部余额的打探(probing):发送方向接收方发送一次 Keysend,并要求接收方不要接收支付,而回传错误;当这样的支付逐步加大额度,从而在某一跳断开时,发送方也就知道了该通道在该方向上的余额。打探可以变成对隐私性的攻击,但是,也可以用来提前打探路径,从而避免支付失败。还有人尝试用 Keysend 来发送消息,也即借助闪电网络实现加密的即时通讯 1。

Keysend 还有一个优点:它完全不依赖其它协议,而只依赖闪电网络自身。

不过,作为一种支付,它还是有一个令人难以接受的缺点:它不能得到收据。由于用来构造 HTLC 的哈希值是由支付方自己指定的(而发票中的哈希值是由接收方指定的,并具有接收方的签名),他从一开始就知道这个哈希值背后的原像,所以,即使全部 HTLC 顺利结算,也不能认为自己得到了收据。

另一个同样不容小视的缺点是,如果以 node_id 来接收支付(比如在捐赠或打赏场景中),接收方的隐私会有非常大的缺陷。接收方的节点、通道、通道 UTXO,都会暴露。

当前,大多数闪电网络客户端都已经实现了 Keysend 的功能,不过,在运行的时候可能需要用户手动打开这个功能。

Keysend 的介绍还可见这篇文章 2。

在上一篇文章中,我们介绍了一种叫做 “AMP(原子化多路径支付)” 的技术,并表示它其实更像一种 Keysend 方案。其中的缘由,在了解 Keysend 是什么之后,当不难理解。读者可以看看 Lighting Labs 为 AMP 编写的介绍 3,看看其特性与 Keysend 有多么相似。

LNURL 与 Lightning Address

从 Keysend 的案例中我们可以知道,尽管发票令人觉得麻烦,却是不可或缺的一种东西,因为它可以让支付方获得收据(支付证据)。换言之,合理的方案应该是能够自动化地请求发票,而不是完全绕过发票(像 Keysend 那样)。

而这就需要一种方式,能够触达接收者节点、请求一个闪电发票,并且发票能恰当地回传到支付方处。

那么,如果接收者节点有一个可被外部访问的网络端点(比如,一个可以访问的网络域名或者 IP 地址),那么支付方就可以先访问这个网络端点、请求接收者的发票;获得发票之后,再启动常规的支付流程。

LNURL 就是在这个想法上产生的解决方案。接收者节点额外运行一个 LNURL 服务端,并将该服务端的网络端口(例如 https://lnurliscool.com/receiver)编码成一个 QR 码。然后,在支付中:

  1. 接收者出示这个 QR 码;
  2. 支付者使用支持 LNURL 协议的闪电 App 扫描这个 QR 码;
  3. 支付者的闪电 App (LNURL 客户端)依据 QR 码中的访问 LNURL 服务端,请求一定数额的发票;
  4. 接收者的 LNURL 服务端回复发票;
  5. 支付者的闪电 App 根据发票,启动常规的闪电支付流程。

由于 QR 码所编码的不是发票,而是一个网络端口,当然可以是稳定不变的:支付者可以向服务端多次请求发票并发起支付。这种收款码的一种有趣的形式是 XXXX@YY.com 这样看起来像电子邮件地址的 “Lightning Address(闪电地址)” 4,前缀是一个自定义的标识符,后缀则是 LNURL 服务端的域名。支付者的闪电 App 会根据 LNURL 规范 #16 5,组合出一个网络端口。

LNURL 是一套多特性的协议,除了上述特性(称为 “LNURL-Pay”)之外,还有:

  • Auth(身份验证)特性,为一个闪电网络用户生成不同的公钥,作为登录不同网站的身份 6;
  • Withdrawal(取款),在扫码之后可以获得支付(取款),而不是发起支付 7;(这就实现了 “付款码” 的功能)

到目前,规范基本上已经稳定下来了 8。你可以在他们的规范库中看到哪些钱包已经支持了 LNURL。

值得一提的是,在当前,你会发现,实现 LNURL 的钱包大多是托管钱包,它们会给每一个用户分配一个 Lightning Address,以允许使用 Lighting Address 收取支付,使用 LNURL 的其它功能更不在话下。但自主保管钱包往往只允许用户向 Lightning Address 发送支付,而不会为用户提供 Lightning Address。

其原因可能是,LNURL 自身其实并不解决用户的可访问性问题 —— 用户必须先有一个可访问的互联网端口,然后才能运行 LNURL 的服务端。为用户提供这样的互联网服务(“内网穿透”),超出了自主保管钱包的开发目标,也面临许多用户体验上的挑战 —— 最好的办法还是让有技能的用户在自己的设备上自主选择或搭建一些互联网服务。而托管钱包就不存在这些困扰,其用户并没有真实的闪电网络客户端,也没有复杂的网络问题要处理。

托管钱包,顾名思义,资金被托管在服务商处,只是用户可以使用这些名义余额来发起闪电支付。它在各个方面都更接近于如今大多数人接触到的互联网支付产品(比如支付宝、微信支付乃至 PayPal),体验也相似。但是,因为用户和服务商之间并没有真实的通道,因此也无法得到密码学和比特币网络的保护 —— 用户需要信任服务商。在使用这样的服务商时,用户无需考虑在线要求,也无需担心收支额度的问题,一切皆由服务商在后台处理。
自主保管钱包则依据我们系列第二篇中所述的原理开发出来,因此用户无需信任对手方。

不管怎么说,LNURL 代表了使用其它网络协议来改进闪电网络用户体验的尝试,这可以说是一种特性,也可以说是一种缺点,完全看你持有哪一种视角。因为使用的是额外的网络协议,它有巨大的开发空间。但因为利用了其它协议(而不是仅依赖于闪电网络本身),自主保管的闪电钱包无法直接运用其所有特性,尤其是移动端的用户,几乎只能在托管钱包上享受其所有特性。

接下来,我们进入一种仅依赖于闪电网络,但所实现的最终功能与 LNURL 非常相似的协议。

BOLT12

如何能够仅依赖闪电网络,来提供网络触达、往返通信的功能呢?

在前面关于 Keysend 的介绍中,我们已经提过,我们可以通过用来传递支付的 “洋葱路由” 协议,向一个节点递送消息;消息就藏在给该节点的加密数据包中。在常规的、成功的支付中,接收者节点会回传一个原像,这个原像是一个 32 字节的随机数,并通过支付路径上各通道的更新,一路回传到支付者节点。而在专门为了传递消息的 Keysend 中,支付者可以在加密数据包中放置消息,并要求接收者节点回传错误(表示支付失败),支付路径上的各通道也会更新,但不返回原像。

如果,我是说如果 —— 如果 A 节点传给 B 节点的是一张发票呢?那么 B 节点就可以依据这个发票给 A 节点支付了!如果 A 节点传给 B 节点的是自身的位置信息呢?那么 B 节点就可以通过 Keysend 给 A 节点发送发票了!

这就是 “BOLT12 Offer” 协议背后的洞见。给定我们可以用闪电网络触达一个节点,自然就能在此基础上实现通讯。BOLT12 增加了一种数据格式,称为 “Offer(要约)”,还有一种叫做 invoice_request 的消息类型。

在支付场景中:

  1. 接收方出示 Offer(作为收款码);
  2. 支付方根据 Offer 的信息向接收者节点发送 invoice_request,其中包含了愿意接受的支付条件以及 reply_path(回复路径)
  3. 接收方节点构造发票之后,通过 reply_path 发送给支付方节点;
  4. 支付方节点根据发票,给接收方支付。

同理,Offer 也可以用于退款:要求退款的一方根据退款方的 Offer 发送发票,退款方就根据发票发起支付。当交互流程反过来(支付者出示 Offer,接收者发送发票)时,它就变成了一种 “付款码”。

由于 Offer 所提供的主要是自身位置的信息,以及其它一些跟支付条件有关的信息,但自身并不是发票,所以,它可以长期使用 —— 支付者可以多次向接收方请求发票。

在 2019 年刚刚提出的时候,BOLT12 就使用上述类似于 Keysend 的做法来传递消息。但随后就有开发者提出 9,这种 “messages-over-payments(通过转发支付的协议来转发消息)” 的做法有很大的开销,沿途的节点必须更新通道状态,还必须保存关于这些承诺交易的信息,而且在回传失败消息时还必须加以处理 —— 与其如此,还不如设计一套新的协议,让支持这种协议的闪电节点能够直接通信,这就是 “洋葱消息” 在闪电网络语境下的含义 10。

在使用洋葱消息之后,节点就可以直接转发消息,转发消息的节点也无需再记忆跟这些消息相关的信息。这也是上述 reply_path 的由来 —— 响应 Offer 的一方规划好从自身出发、触达 Offer 方然后再触达自身的环状路径,并将后半段路径放在 reply_path 中告诉 Offer 方。

在 2023 年 8 月 11,“洋葱消息” 合并进入了 BOLT7,这意味着以后所有的闪电节点客户端都会实现这种特性。而洋葱消息也会使用我们在上一篇文章中介绍的 “盲化路径”。原本在原理上,BOLT12 是不依赖于盲化路径的,但如今在实现中,变成了需要盲化路径作为前置技术。不过,这也意味着,出示 Offer 的接收方将默认具有更好的隐私性。

BOLT12 尚未合并到 BOLT 中,但这个想法得到了大多数开发者的支持。关于 BOLT12 的特性,这个页面提供了基本的介绍 12。至于其起源和变革,Optech 的主题界面提供非常详尽的参考 13。

相比于 LNURL,BOLT12 最大的特点是,它可以在闪电网络协议内实现,而不需要依赖于其它网络协议和通讯方式。这有它的好处,但是,它也意味着要使用闪电网络中的资源。如今,因为洋葱消息的采用,其开销被进一步降低。我们有理由期望,BOLT12 会给我们带来用户体验的变革。届时,自主保管的钱包,也能提供收款码、付款码这样当前专属于(支持 LNURL 的)托管钱包的体验。

结语

在这篇文章中,我们介绍了实现可复用的闪电网络收款信息的尝试。最初的 Keysend 虽然直接,却无法提供支付证据。LNURL 有很大的灵活性,在自主保管的闪电钱包中却难以做到 “开箱即用”。而 BOLT12,通过利用闪电网络自身的特性,最有可能革新自主保管的闪电网络用户的体验。

在下一篇文章中,我们将介绍帮助用户应对通道用户体验最难解问题 —— 收款额度 —— 的解决方案。

下一篇: 闪电网络:技术与用户体验(五):流动性获取

脚注

  1. https://www.btcstudy.org/2022/03/09/on-lightning-messaging-apps-emerge-as-growing-use-case/

  2. https://www.btcstudy.org/2022/01/14/technical-series-what-is-keysend/

  3. https://docs.lightning.engineering/lightning-network-tools/lnd/amp

  4. https://www.btcstudy.org/2022/12/22/how-lightning-address-works/

  5. https://github.com/lnurl/luds/blob/luds/16.md

  6. https://www.btcstudy.org/2022/11/30/lightning-authentication-lnurl-auth/

  7. https://www.btcstudy.org/2024/02/02/the-past-present-and-future-of-offline-payments/#LNURL-Withdraw

  8. https://github.com/lnurl/luds

  9. https://bitcoinops.org/en/newsletters/2020/02/26/#ln-direct-messages

  10. https://bitcoinops.org/en/newsletters/2020/04/08/#onion-messages

  11. https://bitcoinops.org/en/newsletters/2023/08/09/#bolts-759

  12. https://bolt12.org/

  13. https://bitcoinops.org/en/topics/offers/

闪电网络:技术与用户体验(四):收款码

在本篇中,我们将介绍让节点获得入账流动性(收款额度)的方案,并介绍这些方案在闪电网络终端用户所用的实现上的演化。

早在本系列第一篇中,我们就提到,出于 “通道(共有资金)” 的特性,“收款额度” 的概念是无法逃避的。而每个用户都有可能遇上想要收款却没有额度的情形。

这个问题因为闪电网络早期实现的一个设计而变得更加严峻 —— 单向注资。

单向注资

虽然在我们之前使用的例子中,都把通道双方都有余额作为通道的初始状态,但实际上,最早实现的通道注资方法是 “单向注资” —— 在参与通道的双方中,仅有一方为通道提供资金。如此一来,在一开始,双方得到的都是不平衡的通道:一方仅有支付能力(而无收款能力),而另一方又仅有收款能力(没有支付能力)。

这种设计有一些优点:易于实现、安全性检查更加简单(双向注资所需的额外安全性检查可见 1);尤为重要的是,它更适合网络的启动。在一个新节点要加入网络时,它可能会选择跟一个已知的老节点开设通道,然而,对后者来说,却难以决定要不要在其中锁入流动性:如果这个新节点只是尝试一下,很少在线,那么,锁入其中的资金就无法获得收益。而单向注资使得该节点根本不必有此担忧:新节点会自己锁入资金,而不要求老节点提供任何初始资金。

但是,这也意味着,收款额度的获取成了所有新节点的难题 —— 不论你想做一个转发节点,还是将闪电网络用于日常使用。

截至本文撰写之时(2024 年 2 月),已经有两个闪电网络客户端(Core Lightning 和 Eclair)实现了 “双向注资(dual funding)”,也即在创建通道时,双方都可以为通道提供资金。在一些条件下,这可以节约一个节点要创建平衡的通道的步骤。

“双向注资” 的详细介绍可见这个页面 2。

接下来我们要介绍的是获得入账流动性的方案。概要来说,这样的方案可以分成两大类:一类的原理是向外支付;另一类的原理是请求他人向自己开设不平衡的通道。

潜水艇互换

“潜水艇互换(submarine swap)” 是第一类方案的代表。其想法是,需要入账流动性的节点将自己在闪电通道中的资金通过 HTLC 置换成链上资金;在向外支付的同时,该节点也就获得了入账流动性。

用户在闪电网络中给互换服务商支付,互换服务商在链上给用户支付,这就是潜水艇互换;而 HTLC 可以保证互换的免信任性。不过,由于这个过程涉及链上支付,它在实现中会遇到一些难题,从而一定程度上需要一些信任。我们用详细的互换流程来分析:

  1. 用户向服务商请求互换;服务商提供自身的闪电网络位置;
  2. 用户生成一个原像及其哈希值,使用该哈希值构造用于闪电支付的 HTLC;当支付转发消息到达服务商时,服务商使用相同的哈希值,在链上创建一个 HTLC 输出;
  3. 用户发现该链上 HTLC 输出得到确认之后,就使用哈希原像申领其中的资金;该原像也就暴露给了服务商;
  4. 服务商使用原像领取闪电网络中的 HTLC。

从第二步开始,服务商需要信任用户,因为 TA 需要创建一个链上输出。如果这时候用户突然反悔,或本来就有意作恶,不在链上释放原像,那么服务商就亏掉了为创建这个链上输出而需支付的手续费。也正因此,在一些服务中,服务商会要求用户预付一定的费用,在完成互换之后退款,但从原理上说,这又变成了用户需要信任服务商。

潜水艇互换在实践中的另一个困难是,它需要用户的钱包也有相应的支持。用于给用户支付的 HTLC 并不是常见的单签名输出,用户的钱包必须能够理解 HTLC 的脚本、懂得为其构造见证数据(witness),才能花费其中的资金。当前,将自身定位为 “比特币钱包” 的钱包软件几乎都无法提供这种支持,但一些 “闪电钱包” 则可以做到。未来,这需要依靠 Miniscript 这样的技术来提供更好的互通性 3。

潜水艇互换可能是最早出现的入账流动性获取方案,也启发了许多后来者。其中一种是 “PeerSwap”,直接跟你的通道对手实施潜水艇互换 4。还有一种叫 “环路支付(circular payment)”,就是规划出一条环状的路径,将自己在 A 通道中的余额转移到 B 通道 5。

潜水艇互换还有一种有趣的用法:你可以用闪电网络中的资金给他人支付链上比特币。此外,它也可以反向运行:将链上的比特币 “充值” 到自己的闪电通道中,以增加支付能力,无需 关闭通道-重新打开通道。

当前,如果你使用 LND 客户端来运行闪电节点,你可以使用搭配的 Loop 客户端来使用潜水艇互换功能。

通道租赁

获得入账流动性的另一种办法是租赁通道:请求他人用自己的资金与你开设一条通道(当然,你需要支付一些费用)。对方投入的资金即是你立即获得的收款额度。

请注意 —— 你并没有借他的钱,钱依然在他手上,只不过形式从链上资金变成了链下(某一条通道中的)资金;当你在这条通道中收到支付时,他必定在另一条通道中得到了 相同数额+转发费用 的支付。

显然,这种通道租赁需要让通道保持一段时间,否则出租方就可以收钱不办事 —— 在收到租金之后立即关闭通道。但是前面介绍的闪电通道构造似乎并没有对通道存续时间的保证。难道又只能引入信任了?

事实上,如果你了解比特币的脚本,你会发现,有一种很简单的办法可以保证出租方会让通道持续一段时间。假设租赁方 Alice 要求出租方 Bob 与自己开设一条通道,那么,Alice 可以在自己签名的承诺交易中,为所有给 Bob 支付的输出(包括普通的输出和 HTLC 输出的相应花费分支)都加入绝对时间锁,时间锁的过期时间是双方约定的合约结束时间。也即,即使 Bob 拿着最新的承诺交易退出通道,如果未过合约期限,这些资金也会一直锁定、无法动用。这就完全打消了 Bob 提前关闭通道的激励 —— 资金放在通道中还有可能产生收益,但关闭通道会导致字面意义上的 “冷藏”。早在 2018 年,就有开发者提出了这样的想法 6。

通道租赁的想法简单又直接。甚至移动端的自主保管钱包 Breez 也在 App 中提供了向著名的服务商租赁通道的入口。

最初,提供此类服务的都是著名的公司,而且流程中很可能需要信任。于是,人们想出了更多方法来降低参与的门槛、增加市场的竞争。

Lightning Pool

Lightning Pool 是 Lightning Labs 推出的一种通道租赁拍卖市场。用户可以使用 LND 客户端和搭配的 Pool 客户端来参与。其基本特性是:

  • 市场需要一个中介(或者说拍卖行),来保证各方诚信行事。
  • 流动性的需方和供方,都要各自与拍卖行创建一个 “账户”,并把钱存进去。这样的账户在比特币链上是一个输出,它有两个花费分支:一是户主与拍卖行的 2-of-2 多签名;二是户主单签名 + 绝对时间锁。
    • 这样的输出保证了:拍卖行无法独自转移户主的资金,而且资金的所有行动都要经过户主的同意,并且,在一段时间之后,户主总能单方面转移其中的资金。所以,这样的账户是免信任的。并且,当户主提出自己的订单要求时,拍卖行可通过要求户主的签名,来保证这样的 出价/要价 是诚实的。
  • 拍卖是供方和需方各自叫价、但叫价仅向拍卖行公开的盲拍。拍卖行会在确保供需匹配之后,以区块为时间间隔清算市场;并且,在同一区块中成交的所有订单,每一单位的资金将收到相同的租赁价格。
  • 拍卖行在链上会用一笔交易,批量处理该区块内所有成交的订单。
  • 订单成交之后,供需双方创建闪电通道,并且,将使用上文所述的技巧来保证通道会在合约期内持续存在。

Lightning Pool 是一种有趣的尝试。它用一个半公开的市场解决了流动性供需匹配的问题,还为比特币资金提供了一种按区块计量的利息率(当然,所有的通道租赁解决方案都有这个效果)。

Lightning Pool 的技术详述可见他们的论文 7。

Liquidity Advertisement

“流动性广告(Liquidity Advertisement)” 是 Blockstream 提出的一种技术,其核心是让流动性的供方可以用闪电节点的 gossip 消息(本来用于宣布新通道、新节点的消息,相当于整个闪电网络的公开频道)来播报自己的要价以及通道的持续时间。接受这个价格的闪电节点可以与之用双向注资方法开启通道、获得入账流动性。租赁方为通道提供的输入会立即用来支付租赁费。

当前,仅有 Core Lightning 客户端为 Liquidity Advertisement 提供了实验性支持。

零确认通道

上述两种方案,显然更适合于网络中的转发节点,以及有经验有技能的用户,却不适合于移动设备的用户(可以操作的界面更为有限),更不适合刚解除闪电网络、对技术了解不多的用户。因此,我们要专门一些适合刚入门用户的方案(并且假设他们会使用移动端 App,而不是电脑软件)。

“零确认通道(Zero-conf channel)”(也称 “零配置通道”、“涡轮通道”)的想法是,通过引入有限的信任,一举解决入门用户的两大问题:(1)创建通道需要等待;(2)新建通道没有入账额度。

办法如下:用户将比特币资金发送给服务商,服务商负责对用户开启通道(单向注资),并在通道内将扣除服务费之后的剩余资金转移给用户;并且,双方都视这条通道为立即可用,而无需等待通道注资交易得到区块确认。

显然,这个过程需要用户信任服务商:假定用户将资金转移给服务商之后,服务商拒绝执行后续步骤,那么用户并没有什么办法(只能到社交媒体上哭诉);其次,在通道注资交易获得区块确认之前,用户在通道内获得的支付都是不可靠的,因为服务商可以重复花费注资交易的输入。

但是,它解决了刚进入闪电通道的用户的一些痛点:首先,常规的闪电通道,在创建时需要让注资交易获得 3 次区块确认,用户才能开始支付,而零确认通道可以让用户立即开始闪电支付(用户的闪电支付,只要能获得收据,是可靠的);其次,这种通道由服务商创建(可能还加入了服务商自己的资金),所以用户从一开始就可以获得收款额度。因此,还是可以给用户带来显著的便利的。

而且,一旦零确认通道的注资交易获得区块确认,它就会逐渐转变成常规的闪电通道:用户拥有承诺交易所提供的保护,不再需要信任服务商。

显然,要求用户先将资金交给服务商,是双向注资技术缺位之时的无奈之举。有了双向注资之后,我们就可以优化它,让服务商和用户直接执行双向注资程序。这并不能缩减用户创建闪电钱包的时延,只是降低了所需的信任。至于 “零确认” 这段时间所要求的信任,则是无法消除的 —— 但我们都知道,这个环节所要求的信任,是有限的。

更富细节的描述可见此篇 8。

许多移动端的闪电钱包都实现过零确认通道。不过现在,它已逐渐让位于一种更精致的零确认通道 —— JIT 通道。

JIT 通道与闪电网络服务商(LSP)

“JIT 通道”,顾名思义,是一种 “按需开设的通道”,它在零确认通道的基础上更进一步,但又可以说更加简单。

其思想是:仅当用户要接收支付、现有的收款额度又不够时,由服务商向用户开设新的通道,用户在新的通道里领取支付;并且,这条新通道也是 “零确认通道”,可以立即使用。

其技术实现也非常直接:当一个 HTLC 即将经由服务商到达用户,而用户的收款额度不够时;服务商就跟用户创建一个新通道,并用这个新通道内的资金来创建所需的 HTLC;此外,服务商还可以要求用户在某一条通道中创建一个使用相同哈希值的 HTLC,以支付服务费。一旦用户在新通道中用原像领取支付,服务商就不仅能获得该笔支付的转发费(像常规的闪电通道一样),还能获得用户支付的服务费。

相比于原本的零确认通道,JIT 通道的流程大大减少,但又保留了零确认通道对服务商的好处:甄别用户。因为需要在通道中锁定资金、为用户提供收款额度,服务商最担心的就是遇上试水的、不真实的用户。或者说,服务商需要甄别用户,以决定自己锁定的流动性的数量。而用户预先付出的资金数量、即将收取的资金数量,都为服务商提供了重要的参考。

除去 “零确认” 过程中的信任,JIT 通道也未完全消除信任需要,但它跟潜水艇互换一样,更偏向要求服务商信任用户:服务商必须垫付创建通道的区块确认费,但如果这笔支付是虚假的、用户不会放出原像,服务商就面临亏损。

在业内,有一个 “闪电网络服务商(LSP)” 的概念。其意思是,当用户与这样的服务商建立通道时,服务商可以自动地帮助用户管理通道(包括备份)和流动性,无需用户为之烦恼;此外,由于 LSP 时刻在线,也能为用户异步接收支付提供帮助 9 。即使这个概念不是由 JIT 通道激发的,JIT 通道也必定极大地完善了这个概念。

当前,许多自主保管钱包都提供了 LSP,包括 Phoenix 钱包和 Breez 钱包。其中,Phoenix 钱包还有专门的一个页面,让用户自己配置为接收一笔支付而愿意支付的服务费上限。当开设通道所需的实际费用超过这个限度时,LSP 将不会行动。

现如今,人们已经在讨论为 LSP 制定一套技术规范 10,从而允许人们使用相同的软件来获取不同 LSP 的服务,以强化 LSP 的市场竞争、保证用户的体验。

结语

目前,大部分移动端闪电网络用户的选择依然是托管钱包 —— 用户不掌握私钥,资金也得不到真实通道的保护,但无需关心收款额度、定期在线要求以及通道资料备份等所有事情的,闪电支付服务。显然,自主保管钱包的体验还没有说服足够多的人。但是,我们有理由相信它会继续进步。上一篇文章所述的收款码的进化是一个例子。本篇所述的零确认通道的进化也是一个例子。

在下一篇文章,我们将介绍让用户能够更自由地应对支付和收款需要,不必关心资金形式的技术。它们改善了用户的支付能力,并且直观地展现了形式不同、本质相同的资金的深层一致性 —— 只有一种比特币。

下一篇: 闪电网络:技术与用户体验(六):只有一种比特币

脚注

  1. https://bitcoinops.org/zh/newsletters/2024/02/07/#txid-segwit

  2. https://bitcoinops.org/en/topics/dual-funding/

  3. https://www.btcstudy.org/2022/06/26/hidden-power-of-bitcoin/

  4. https://www.btcstudy.org/2022/04/27/peerswap-a-p2p-btc-ln-balancing-protocol/

  5. https://www.btcstudy.org/2022/06/14/rebalancing-in-the-lightning-network-circular-payments-fee-management-and-splices/

  6. https://lists.linuxfoundation.org/pipermail/lightning-dev/2018-November/001555.html

  7. https://github.com/lightninglabs/pool-paper/blob/main/liquidity.pdf

  8. https://www.btcstudy.org/2022/08/25/what-are-turbo-channels/

  9. https://www.btcstudy.org/2024/02/02/the-past-present-and-future-of-offline-payments/

  10. https://www.btcstudy.org/2023/12/01/lightnings-future-a-new-era-of-interoperability-with-lsp-specs/

闪电网络:技术与用户体验(五):流动性获取

在前面的文章中,我们一直在讨论,如何优化闪电网络,并通过技术的进步来为用户提供更好的体验。但有一个 “简单的” 问题我们一直没有触及:在闪电钱包用户的日常生活中,可能既需要发起闪电支付,也需要发起 “链上支付” —— 具体来说,就是让一笔交易获得区块确认,或者说,在比特币网络中创建一个新的 UTXO;既需要利用闪电网络来收款,也需要接收链上支付。

最初的闪电网络客户端实现(包括:Eclair、Core Lightning 和 LND)都同时管理链上资金和闪电通道资金。而且那时候,“链上资金” 与 “闪电通道资金” 是泾渭分明的,互相不能直接用于对方的支付:如果你要发起一笔链上支付,你无法直接动用闪电通道内的资金,你必须先退出通道,然后再发起链上支付(这其中的时延高得令人发指)。链上资金自然更无法直接用于闪电支付。也正因此,当时的用户界面(第三方的钱包软件以及操作工具)也往往将链上余额与通道内余额分开展示。

显然,这很不便利,是不理想的。它会在用户的同一个钱包(使用同一批私钥可控制的资金)内部形成流动性的分割、影响用户的支付能力,并且制造很大的困惑 —— 这世界上难道存在两种比特币吗?为什么它们都在我的钱包里,我却不能用它们来支付?

但是,如何打破这种区隔呢?如何让闪电通道内的资金也能发起链上支付?如何用链上资金触发闪电网络内的支付?

在本文中,我们会介绍一系列的技术和解决方案,它们改变了 “两种资金有区隔” 的不完善状态,有力地证明了闪电钱包不必因为这种认识上的人为区别而裹足不前。最终,它们允许闪电钱包只保留一种形式的资金、只展示一种余额,同时保留跟一切钱包交互的能力。

潜水艇互换

第一种使我们可以不必顾虑资金的形式、径直发起支付的技术就是上一篇文章里我们介绍过的 “潜水艇互换”。正向的潜水艇互换使我们可以用通道内的资金创建链上输出(也即发起链上支付),而反向的潜水艇互换使我们可以用链上输出换取闪电支付的能力。

不过它也有些不便利之处。在潜水艇互换中,接收者的钱包软件必须支持这种脚本,而且必须在一段时间内领取支付。这都会对接收方提出一些要求。

Splicing(通道拼接)

“通道拼接” 则比潜水艇互换更进一步。它利用了一种根本上的洞见:所谓的 “链上资金”,不过是 “放在只有自己能控制的 UTXO 中的资金”;而 “通道资金”,不过是 “放在与其他人共同分享的 UTXO 中的资金” —— 它们只是操作方法不同,本质并没有什么分别。只要通道双方达成一致意见,他们就能以通道 UTXO 作为输入,发起链上交易(“拼出”),并在同一笔交易中创建一条新的通道,不必先关闭通道、完成支付之后又开启;同样地,只要双方遵循一定的程序,同样能将当前的 UTXO 以及其它资金作为输入,形成新的通道(“拼入”),而不必先关闭通道然后再开启。

并且,通道拼接还能保证,在等待拼出或拼入交易获得区块确认的过程中,通道的功能可以不受影响,依然能即使确认支付。这是怎么做到的呢?

以通道拼出为例,我们假设双方要用通道资金发起一笔链上支付,其输入和输出如下:

1
2
3
当前通道 UTXO(前身 UTXO) --------- 新通道 UTXO
|
—--- 接收者 UTXO

在这笔交易等待区块确认期间,双方可以对 “前身 UTXO” 和 “新通道 UTXO” 同步签名意义相同的承诺交易,区别只在于对 “前身 UTXO” 签名的交易将始终有一个交给接收者的输出。也即,如果拼出交易得到了确认,这些对前身 UTXO 签名的承诺交易就自动作废,而基于新通道 UTXO 的承诺交易如实地反映了双方的交互;如果拼出交易得不到确认,基于前身 UTXO 的承诺交易也如实地反映了双方的余额变化,将最新的承诺交易广播到比特币网络中依然会触发对接收者的链上支付,而不会允许通道任何一方占便宜。拼入交易也是同样的道理。

通道拼接大大改变了闪电通道发起链上支付的能力和体验。并且,它使我们可以在维持闪电支付能力的同时调整通道的大小,而不必经历关闭又开启的繁琐操作。甚至有人提出可以让通道也参与 coinjoin(一种混淆支付真正收发者的交易) 1。可想而知它有多么强大。

相比于潜水艇互换,通道拼出不会对接受者的钱包提出任何要求,最普通、最常见的钱包软件就可以用来收款。不过,通道拼出无法用来获得入账流动性。

截至本文撰写之时,Eclair 客户端已经支持通道拼接,Core Lightning 客户端也提供了实验性的支持。详细解释和支持进度可见这个页面 2。

Swap-in Potentiam

除了优化通道资金发起链上支付的能力,人们也在思考如何加快链上资金进入闪电网络的速度。上一篇文章介绍的 “零确认通道”,也是这方面的努力。“Swap-in Potentiam” 则提议闪电网络的用户应该用这样一种脚本来接收链上支付:该脚本有两种花费分支,一是用户与某个闪电网络服务商(LSP)的 2-of-2 多签名;二是用户单签名 + 一个相对时间锁。

这种脚本有非常有趣的特性:(1)时间锁分支决定了其中的资金的最终归属;假设其双签名分支不被动用,在相对时间锁过期后,用户就可以单凭自己的公钥和签名来花费它;(2)在时间锁还未解锁的时候,不论 LSP 还是用户都无法单方面使用其中的资金;当用户使用其中的资金给 LSP 支付(以承诺交易的形式),LSP 是可以立即就确认的。—— 没错,在时间锁解锁之前,可以把它当成是用户对 LSP 的单向支付通道。在这样的单向通道内,用户给 LSP 的支付,与闪电通道内的支付没有什么区别 —— 所以它可以立即用来闪电支付。

对用户来说,这是一种免信任,但又可以获得 LSP 服务的资金形式。在收到链上支付之后,只需资金得到区块确认,立即就可以进入闪电网络,而无需再经历开启通道的流程。

“Swap-in Potentiam” 提议的原始文本可见这里 3。

截至本文撰写之时,移动端钱包 Phoenix 已经实现了 Swap-in Potentiam。

Swap-in Potentiam 已经走到这条路的终点了吗?不。从通道拼接的角度看,最佳效率的做法是支付者、闪电钱包用户、LSP 三方一起构造、签名交易,使得支付者的链上支付在单笔交易中直接拼入通道,而不是进入 Swap-in Potentiam 脚本之后再拼入。
显然,这需要支付者的钱包采用一种更为抽象的支付的概念 —— 为一笔交易贡献一定数量的输入,然后确保该交易有一个归属于自己的找零输出,并且输入和输出的差额恰为(支付额 - 应负担的手续费额) —— 而不再要求交易完全由自身来构造、不再要求交易的输入仅有自身可控制的资金。在这种情况下,接收者仅提供自己的收款脚本是不足够的,两方可能要经过多次通信往返,才能安全地构造出最终的交易并广播出去。
当前,以 payjoin 为名的技术,最契合我们这里谈到的概念 4。

接下来,我们要分析两种钱包的用户体验。它们各自使用上述的技术,达成了 “向用户展示一个余额” 的目标。

Muun vs. Phoenix

Muun 钱包可能是最早尝试克服 “两套余额” 问题的钱包。它的做法是只维护链上资金,然后以潜水艇互换来获得 闪电支付/闪电收款 的能力。

这样做的结果是简洁得惊人的架构和用户体验 —— 它将 闪电通道/闪电网络 完全抽象掉了。软件无需实现完整的闪电网络协议、钱包里没有通道、没有流动性问题,也没有在线要求。用户也完全不必了解闪电通道。无论是常规的链上支付还是闪电支付,体验都相当一致。而且,其安全保管操作与常规的 “比特币钱包” 没有什么区别。

但这样做也有一个明显的缺点:它并不能享受到闪电网络的所有好处,主要体现在手续费上。即使在支付闪电发票,实际上其操作也是在链上发起交易,也要支付链上确认的手续费;反之,接收闪电支付的时候也是如此,同样涉及链上交易。一旦比特币网络的交易确认需求高涨、手续费率提高,用户就会发现自己要支付的手续费高得可怕,一点也不像在使用闪电钱包。

Phoenix 则跟 Muun 钱包朝着完全相反的方向走 —— 它激进地要让用户的钱包里只有通道资金,并且只有一笔。它是这样做的:

  • JIT 通道。当用户要接收闪电支付而无足够的收款额度时,就由 LSP 向用户开启通道,让用户获得收款额度;
  • 通道拼接。当用户要发起链上支付时,就发起通道拼出交易。结合 JIT 通道,意味着在 LSP 提供收款额度时,也会采用通道拼入的形式,以维持仅有一条通道的状态;
    • 此外,用户也可以提前请求收款额度,LSP 会在后台完成操作;
  • Swap-in Potentiam。当用户要接收链上支付时,就使用 Swap-in Potentiam 这样的脚本;一旦资金得到 3 次确认,此时的区块确认手续费又低于用户设定的限值,钱包会自动与 LSP 合作,将资金拼入通道;并且,它是 “零确认的拼入”,也即,即使拼入交易尚未获得网络确认,双方也将新通道中的资金视为立即可用。

Phoenix 综合了我们在这两篇里提到的许多技术,持之以恒地打造了最友好的移动端自主保管钱包体验。

不论是 Muun 钱包,还是 Phoenix 钱包,它们所应用的技术并没有抹去 “区块确认” 和 “通道确认” 在速度(和经济开销)上的区别,也不可能抹除,但它们已经是两个容易解释得多的概念 5,而且只要保证了支付能力,这些细节就不会太让用户困扰。它们都为钱包软件的设计提供了许多启发和应该思考的问题。Muun 基于跟常规的 “比特币钱包” 完全相同的架构,提供了支付闪电发票的能力;Phoenix 则激进到让用户所有的资金都进入通道,但完全不影响用户的链上支付能力与体验。它们都在抹除 “比特币钱包” 和 “闪电钱包” 的界限,逼迫所有人正视技术和基础设施的进步所带来的潜能,并要求钱包的开发者们为用户释放这种潜能。

最终,用户也会知道,世界上有且只有一种比特币,它每一天都会比昨天更容易在网络中穿梭。

脚注

  1. https://www.btcstudy.org/2023/03/01/lightning-privacy-research-channel-coinjoins/

  2. https://bitcoinops.org/en/topics/splicing/

  3. https://www.btcstudy.org/2023/03/06/swap-in-potentiam-moving-onchain-funds-instantly-to-lightning/

  4. https://www.btcstudy.org/2023/11/10/payjoin-for-a-better-bitcoin-future/

  5. https://phoenix.acinq.co/faq#what-is-inbound-liquidity

闪电网络:技术与用户体验(一):用户体验的基本元素

在上一篇文章中,我们介绍了闪电网络用户体验的基本元素,包括:在线收款、收款额度以及支付成功率。这些概念都可以从我们对闪电网络基本概念的分析中得出,也是对入门用户最有用的概念。

在这一篇文章中,我们将具体解释,在当前的比特币上,闪电通道是如何构造的、通道内的交易是如何实现的、这样的 “支付” 有什么样的特点。要而言之,我们要解释的是,为什么闪电通道是一种 “免信任” 的构造:你不必信任你的通道对手,也可以确定对方给你的支付是真实的、可以花费的。也可以认为,这是在讨论闪电通道的 “安全性” 概念:一个闪电通道的内部状态,是如何安全由比特币协议来表达,从而可以保证它符合人们的使用预期的。在我们解释完所有这一切之后,读者自然也会明白,为什么闪电网络是一种 “扩容方案(扩大比特币吞吐量的方案)”。

最后,我们也会指出,构造闪电通道的技术还有哪些进步空间。

本文绝大部分内容跟技术有关,如果你对它不感兴趣,可以跳过这一篇。但如果你对比特币的可编程性感兴趣,迄今为止,闪电通道依然是最好的思维材料。

比特币的形式

一些读者可能已经知道,比特币的存在形式是 “未花费的交易输出(UTXO)”1。一种形象的理解方式是把它当成某种金属块:每一块都是相互独立的,但它们可以一起熔铸;熔铸之后可以切分成不同的块;然而一旦被熔铸,就不可能再找到原来那一块2。而比特币交易,就是这样的熔铸过程,或者说使用票据而签发新票据的过程。

每个 UTXO 都附带了两种信息:(1)面额,即其比特币价值(以 “聪” 为单位);(2)锁定脚本,也称脚本公钥,为花费该笔资金设置条件。我们可以用一种编程语言 Bitcoin Script 3 来编程锁定脚本。

在最常见的单签名钱包中,锁定脚本中放置的是一个公钥,用来检查花费交易所提供的签名是不是一个有效签名。除了签名检查之外,Bitcoin Script 还可以提供哈希原像检查(根据哈希值检查原像)、花费时间检查。

关于 UTXO 结构以及比特币脚本的工作,笔者的这篇文章 4 提供了更细致的介绍。这里仅着重指出的是:Bitcoin Script 中还存在一种流程控制操作码,例如: OP_IF。其作用是将一笔资金的多种花费方式并置,例如,我们可以让一笔资金既可以被某两个公钥一起花费,也可以被另一个公钥以及一个哈希原像花费。这些不同的花费方式,我们称为 “花费分支”。每一个分支都是花费资金的充分不必要条件。这意味着,我们可以这些流程控制操作码来组合花费条件。

承诺交易

一般来说,比特币交易总是花费一些 UTXO,然后形成新的 UTXO —— 这就意味着,比特币交易所给出的不是关于操作的指令,而是处理的结果。这是比特币交易的一个重要特点。同时,因为每个 UTXO 都有自身的锁定脚本,通过对锁定脚本的编程,我们就可以反过来约束一笔交易的意义。

这里还要介绍的是 “承诺交易” 的概念:在一定条件下,我们可以用比特币交易来表达一种可信的承诺 —— 即使这笔交易没有得到比特币区块链的确认,但因为它是有效的(随时可以得到确认),所以,该交易(它所指明的结果)也是有意义的。

举个例子:两个好朋友 Alice 和 Bob 一起用各自的公钥控制一笔资金;当 Alice 签名这笔资金的花费交易,向 Bob 支付一笔钱(产生由 Bob 能够独自支配的 UTXO)时,这笔交易对 Bob 来说就是一个可信的承诺 —— 虽然它还没有得到区块确认,但因为它是有效的,而且 Alice 没有办法独自把钱转走,所以 Bob 可以保证这笔交易是随时可以得到区块确认的(只需加上自己的签名就是有效的比特币交易),所以,其支付效果是真实的。

闪电通道:基于惩罚的可撤销承诺交易

接下来,我们要结合上述洞见,构造一种比特币上的免信任双向支付通道。免信任,意味着每一次支付都有自身的安全性保证,你不必信任对手,就可以确定所得的支付是真实的。这就是让上一篇文章中的 “算珠轴” 成为现实的东西。

假定 Alice 和 Bob 有一笔共同控制的资金 —— 这个 UTXO 的锁定脚本包含一个 2-of-2 多签名检查,必须是 Alice 和 Bob 都提供签名才能花费它。Alice 和 Bob 用来签名花费交易的公钥,记为 AB;而各自用来收款的公钥,记为 AliceBob。他们还需生成一对长期使用的密钥,分别记为 SASB,这个公钥需要分享给对方。

假定现在,Alice 和 Bob 在通道中各有 5 BTC,而现在 Alice 要给 Bob 转移 2 BTC。这时,Alice 向 Bob 提供请求一个新公钥 P1B,然后以自己的公钥 SA 构造一个新公钥:

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

然后用 A 签名这样一笔交易并提供给 Bob:

1
2
3
4
5
6
7
8
9
输入 #0,10 BTC:
A-B,2-of-2 多签名输出(即通道)

输出 #0,3 BTC:
Alice 单签名

输出 #1,7 BTC:
要么,RA1 单签名
要么,两周后,Bob 单签名

可以看出,这里的 RA1 暗藏机关。

假设这笔交易得到区块链确认,它会熔掉双方共同控制的资金,立即将 3 BTC 交给 Alice;但剩余的 7 BTC,则会在两周以后才交给 Bob —— 前提是没有人知道 RA1 的私钥。如果 Alice 知道了 RA1 的私钥 —— 只需 Bob 向 Alice 暴露了 P1B 的私钥 —— 那么,Alice 就有两周的时间窗口,可以取走这 7 BTC。

对 Bob 来说,尽管这笔交易看起来对他不利,但它却是一个可信的承诺:
(1)Alice 为这笔交易提供了有效的签名;
(2)Bob 知道 SA 和自己的 P1B,可以验证 RA1 的构成;他知道,只要自己还未暴露 P1B 的私钥给 Alice,Alice 就无法动用这个分支;
(3)Alice 无法独自花费通道资金。也即,它实在地支付了原本属于 Alice 的 2 BTC 给 Bob。而这笔交易的两个输出的面额,表达的是支付完成之后的结果(Alice 3 BTC、Bob 7 BTC)。

同理,Bob 也向 Alice 请求一个新公钥 P1A,然后用 SB 构造一个新公钥:

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

然后用 B 签名一笔不对称的交易并交给 Alice:

1
2
3
4
5
6
7
8
9
输入 #0,10 BTC:
A-B,2-of-2 多签名输出(即通道)

输出 #0,7 BTC:
Bob 单签名

输出 #1,3 BTC:
要么,RB1 单签名
要么,两周后,Alice 单签名

对 Alice 来说,这同样是一笔虽然有不利条件,但可信的承诺。

那么,我们要问的是,在这两笔承诺交易的输出 #1 中加入这样的公钥,是为了什么呢?答案是,它们是为了让这样的承诺交易变得 “可以撤销”!

现在,假设 Bob 要给 Alice 支付,那么,Bob 就向 Alice 请求一个新的公钥 P2A,构造新的撤销公钥 RB2,然后向 Alice 提供新的一笔承诺交易。 这笔承诺交易将同样使用双方的共同资金作为输入,而输出将表达此次支付完成之后的结果(比如,Alice 4 BTC,Bob 6 BTC,表示 Bob 支付了 1 BTC 给 Alice)。Alice 得到新的承诺交易之后,就给出 P1A 的私钥给 Bob;同理,当 Alice 签名新的、不对称的承诺交易给 Bob 之后,Bob 交出 P1B 的私钥。

如此一来,双方就安全地交换了一个私钥,从而 “撤销” 了上一笔承诺交易。这些承诺交易,从表面上看,依然是有效的比特币交易,但持有这些承诺交易的相关方,却再也不敢让这笔交易得到区块确认。比如,假设 Bob 将上一笔承诺交易提交到区块链,该交易将立即支付 3 BTC 给 Alice,同时,打开为期两周的时间窗口,允许 Alice 动用 RA1 的私钥来取走输出 #1 的 7 BTC —— 而 Alice 此时确实已经知道了 RA1 的私钥!

回顾一下我们前面提到的概念:UTXO 的锁定脚本可以使用多个花费分支;在一定条件下,有效的比特币交易可以成为可信的承诺;同时,交易的输出又携带了锁定脚本。闪电通道的洞见在于:通过在交易的输出中埋设特殊构造的公钥,并让双方签名意思一致但不对称交易,将这样的承诺交易变成了 “可撤销的”。也即,现在,双方可以几乎无限次地更新通道的内部状态(不断使用通道中的资金给对方支付),既不需要等待区块链的确认,也不需要信任对方,因为他们拥有密码学和比特币网络的保护。双方都可以随时以最新一笔承诺交易(通道的最新状态)退出通道;而一旦对方发布旧的承诺交易(即尝试欺诈),他们有一段时间窗口可以发动反击(惩罚),将所有通道资金全部收入囊中。

关于闪电承诺交易的特性,笔者的的另一篇文章 5 提供了更细致的分析。但是,这还不够,上述构造仅表明,我们拥有一种机制来更新一笔资金(一个合约)的内部状态,它跟现实的 “支付” 还有差距 —— 支付方应该能得到某种证据,证明自己已经支付了。

闪电通道承诺交易的脚本分析也解释了上一篇文章提到的一个细节:闪电网络的用户不能无限期离线。这是因为,旧的承诺交易依然是有效的、可以得到区块链确认的交易(否则它们就不是可信的承诺),没有什么能从技术上阻止你的通道对手将旧承诺交易提交上链。欺诈发生时,你唯一的反制措施就是在脚本允许的时间窗口内让你的惩罚交易得到区块链确认。也就是说,你需要观察到对手的举动,然后反击。但如果你长期离线,你可能不知道对手在尝试发布旧承诺交易。

一种可延长离线时间及帮助优化反击响应速度的解决方案叫做 “瞭望塔”。简单来说,就是将过期承诺交易及其对应惩罚交易的信息交给一个全时在线的节点,由后者在观察到链上出现某一过期承诺交易时就向区块链提交惩罚交易。

还值得一提的是,参与一条通道的双方的第一笔承诺交易,不是在他们为通道注入资金之后才签名的,而是在注入资金之前就签名的,这就规避了进入通道之后对方不再响应、资金卡死的风险 —— 这就是承诺交易的另一个作用:规避合约失控的风险。你肯定想问,这是怎么做到的?被花费的那笔资金不是还不存在吗?这还是跟 UTXO 的特性有关。UTXO 的位置是 “输出点(outpoint)”:它是某一笔交易的某个输出。给定参与通道的双方愿意为通道提供的输入,交易 ID 就是确定的,从而,通道 UTXO 的输出点也是确定的。双方可以用这个输出点来构造交易并签名。这是比特币在 2017 年的隔离见证升级后才明确具备的特性。

HTLC 与 “支付”

幸运的是,Bitcoin Script 恰好可以编程出 “原子化交换” 的一种形式:哈希时间锁合约(HTLC)。HTLC 是一种带有两个花费分支的锁定脚本,协助一方向另一方购买一个秘密值。假设 Bob 要向 Alice 购买一个秘密值,Alice 表示该秘密值的哈希值是 H,那么,为了一手交钱一手交货,Bob 可以向 Alice 提供一个这样的 HTLC:

1
2
要么,Alice 可通过揭晓 H 的原像以及自己的签名花费这笔资金
要么,24 小时后,Bob 可独自取回资金

当 Alice 使用第一个花费分支时,Bob 就知道了 H 的原像(目标秘密值);如果 Alice 不愿意揭晓 H 的原像,24 小时之后,Bob 就可以独自取回资金。

在通道中,我们可以为承诺交易增加带有 HTLC 的输出。其作用也很简单:让支付的一方可以获得支付成功的证据 —— “我不是不愿意给你支付,只求给个收据”。以上文 Alice 给 Bob 支付为例,Alice 可以请求 Bob 给出一个哈希值 HB1,然后在承诺交易中提供一个价值 2 BTC 的 HTLC 输出:

1
2
3
4
5
6
7
8
9
10
11
12
输入 #0,10 BTC:
A-B,2-of-2 多签名输出(即通道)

输出 #0,3 BTC:
Alice 单签名

输出 #1,2 BTC:
HTLC

输出 #2,5 BTC:
要么,RA1 单签名
要么,两周后,Bob 单签名

此处没有给出 “输出 #1” 的锁定脚本构造、仅以 “HTLC” 代指,是因为,在实践中,其形式并不像上文表示的那么简单。笔者会在注释中提供一些说明,但对技术细节不太感兴趣的读者来说,不了解这些细节并不影响理解 HTLC 的功能 6。
当 Bob 给出 HB1 的原像之后,双方就可以使用新的一笔承诺交易来表达支付完成之后的状态:Bob 7 BTC、Alice 3 BTC。区别在于,Alice 不仅完成了支付,还获得了收据 —— HB1 的原像。

更有趣的是,HTLC 还可以将多个通道内发生的支付 “粘合” 在一起:给定在每一条通道内,都使用相同的哈希值来构造 HTLC,那么,这些支付只会一起成功,或者一起失败。

假设 Alice 要通过 Bob、Carol 给 Daniel 支付,那么,Alice 先向 Daniel 请求一个哈希值,然后让每一条通道都创建一个使用相同哈希值的 HTLC:

1
Alice -- HTLC --> Bob -- HTLC --> Carol -- HTLC --> Daniel

每一个中间节点(Bob、Carol)都会在自己的一条通道中收到一个 HTLC,并需要在自己的另一条通道中给出一个 HTLC,而两者面额的差值,就是支付成功时该节点可以获得的转发费收入。

当 Daniel 获得 Carol 给出的 HTLC 之后,就揭晓原像,从而领取 HTLC 的资金;每一条通道都发生相同的事情,原像便一路回传交给 Alice:

1
Alice <-- 原像 -- Bob <-- 原像 -- Carol  <-- 原像 -- Daniel

这就是上一篇文章中我们提到的 “转发支付”(也称 “多跳支付”、“路由支付”)的基础,也是闪电网络能成为一个网络的原因。由支付的接收者给出一个秘密值的承诺(在这里是哈希值)、支付的发送者据以构造原子化互换合约,并通过不同的通道一路转发的方法,定义了闪电网络中的 “支付” 的概念:它是可以转发的、免信任的,此外,还可以让支付者获得支付证据(收据)。

在后续文章中,我们还会了解到一种无法获得支付证据的支付,其基本概念叫做 “Keysend”。比较之下,我们将理解,为何我们此处介绍的用法是一种 “标准用法”,是一个我们希望继续开发下去的方法。

Taproot 与闪电网络的改进

如上所述,闪电通道是免信任的:每一种操作的每一步,都可以由比特币协议来表达,并获得密码学和比特币网络的保护。这同时也意味着,比特币协议自身的进步(例如,脚本编程能力的改进),也将为闪电通道带来改进的空间。

2021 年,比特币网络激活了 Taproot 升级 7,它为比特币带来了两项重要升级:可以验证 Schnorr 签名;将完整的锁定脚本转化为默克尔树(MAST),在花费时仅暴露需要用到的花费分支(而无需曝光所有分支)。

相比于 ECDSA 签名,Schnorr 具备一系列的优点:体积更小、验证起来更快、安全性证明更清晰、易于实现聚合签名。“聚合签名” 意味着,你可以将来自多个公钥的 Schnorr 签名聚合成一个恰当构造的公钥的 Schnorr 签名 8。再换句话说,(举例而言),原本的 2-of-2 ECDSA 多签名脚本,可以替换成 Schnorr 单签名脚本 —— 只需这个公钥是恰当构造的、双方共有的公钥。

如前所述,闪电通道就是双方共同控制的资金,并使用多签名检查来保证每一步操作都经过了一致的同意。有了 Schnorr 签名之后,我们可以在保证这一点的同时,让链上需要暴露的公钥和签名都只有一个。

这带来了效率和隐私性的双重提升。在需要将交易提交到区块链的情形中,原本的 “两个公钥、两个签名” 都可以被替换成 “一个公钥、一个签名”,体积缩减了,对区块空间的占用更少了。此外,在 “合作式关闭通道”(双方同时在线,一致决定关闭通道,立即分割资金,而不为交易输出设置时间锁)中,交易在链上的表现就像一个人作了一笔普通的支付,而不是两个人在分割资金。这提高了隐私性。

这是否意味着,使用 Taproot 之后,闪电通道的关闭将跟普通的支付完全无法分别?答,仅在双方合作式关闭通道时,仅在只观察链上数据时,是的。但是,

  • 在非合作式关闭的情形中,一方会提交最新的承诺交易,而这笔交易可能会暴露相当多信息,从而表明这是一条通道,而非个人资金。
  • 如果一条通道在闪电网络中公开宣告了,那么,它也会公开自己的通道 UTXO,从而让闪电网络中的第三方观察者知晓这个 UTXO 不是一笔个人资金,而是一条闪电通道。

这两者都会打消签名聚合带来的隐私性好处。

幸运的是,从上一篇文章中我们可以知道,闪电网络中有两类节点:转发节点和用户节点。对于无意参与支付转发的用户节点来说,完全不必公开自己的通道,也能正常使用闪电网络,他们将有很大概率,能享受到聚合签名带来的隐私性改进。
MAST 也同样提高了效率和隐私性。闪电承诺交易的输出可能会带有多个分支,这些分支的体积甚至很大(比如 HTLC 输出)。MAST 允许我们仅暴露一个真正用到的分支,而不暴露其它分支。这就减少了花费交易的体积。此外,它也掩盖了完整脚本的全貌、模糊了不同脚本的区别,使人们更难辨别一个输出是否来自闪电通道(或是其它类型的合约)。

关于使用 Taproot 构造闪电通道脚本的详情,可见这篇文章 9。

截至本文撰写之时(2024 年 2 月),LND 客户端已支持 “简单的 taproot 通道”。

PTLC

Taproot 所带来的一种更大胆的升级可能性,是以 PTLC(点时间锁合约)来替代 HTLC。它可以带来隐私性和安全性的提升。

在原本的比特币脚本中,我们可以使用哈希值来检查秘密值(其原像);而有了 Schnorr 签名之后,我们可以用一种叫做 “适配器签名(Signature Adaptor)” 的技术 10,用一个椭圆曲线点(公钥)来检查秘密值(其私钥)。

在基于 HTLC 的转发支付中,整条路径上的每一个通道的相关 HTLC 都使用同一个哈希值,这意味着,如果路径上有某两个节点属于同一个控制者,他将有更大概率推断出谁在给谁支付;此外,当原像回传到其离接收方较近的节点时,他完全可以跳过中间的节点,由离发送方较近的节点继续回传原像,从而 “盗取” 一笔与这个 HTLC 相同的价值。

而有了 PTLC,我们可以让每一条通道中的支付都需要揭晓不同的私钥,从而让这些支付完全无法关联起来,但依然能保证原子性。

PTLC 的构造方法可见这篇文章 11,此处不述。

截至本文撰写之时,PTLC 尚出于规范的讨论阶段,尚未听闻有客户端开始实现。

LN-Penalty 及其改进

我们在上面介绍的机制被称为 “LN-Penalty”,它是一套基于惩罚、允许更新一个合约内部状态的机制。但它也有一个明显的缺点:为了保证惩罚能力,节点必须保存过去每一次更新通道状态时候的资料:自己签给对方的承诺交易的交易 ID,该交易所对应的 惩罚 私钥。这就构成了一种存储负担。

此外,LN-Penalty 还意味着,双方每次都要签名不对称的交易,这给输出脚本的设计带来了复杂性,使其更难分析。

到目前为止,人们其实已经找出了在当前的比特币上改进后者 —— 允许双方都签名完全相同的承诺交易 —— 的办法 12。该方法同样只需要 “适配器签名” 元件。但是,这种方法无法改进前者 —— 存储负担。

然而,早在 2018 年,人们就已经提出了一种比特币协议可能的升级(SIGHASH_ANYPREVOUT),来实现一种叫做 “eltoo” 的方案,以同时消除存储负担和不对称的交易。其基本思想是,让更新的承诺交易(称为 “状态更新交易”)总能花费较旧的承诺交易(而反之不行),然后用 “结算交易” 来真正触发资金分割。由于新的承诺交易总能花费旧的,因此,用户只需保存最新一笔承诺交易及其结算交易,即可;即使对手尝试欺诈,也只需发布最新一笔承诺交易,便可使用最新状态来结算通道。

Eltoo 的更详细介绍可见此文 13。

截至本文撰写之时,SIGHASH_ANYPREVOUT 还未激活;而且,人们已经指出,有其它提议 14 可以模拟 SIGHASH_ANYPREVOUT 的特性,从而实现 eltoo。这是关于 “限制条款(covenant)” 的讨论的一部分。也印证了我们前面说的:闪电通道是可以完全由比特币协议表达的机制,因此,比特币协议自身的进步,也将带来闪电通道的进步。

在这方面的改进上,我们依然处于方法的讨论阶段。

值得指出的是,人们发现仅需适配器签名就可以构造出对称的闪电承诺交易是在 2020 年。而人们也早已经发现,ECDSA 同样能实现适配器签名(虽然 Schnorr 签名依然有自身的优势)。但是,迄今为止(Taproot 升级带来 Schnorr 签名的两年后),似乎依然没有人尝试以此路径实现使用对称承诺交易的闪电通道。也许是因为,要实现它并保证兼容现在的闪电网络规范,是一件需要大量努力而收效又不够吸引人的事情。

但是,eltoo 又持续地吸引人们,甚至影响了人们在比特币协议改进中的想法和态度。

聚合 HTLC 输出

如前所述,在当前的闪电通道实现中,每一笔支付都使用一个单独的 HTLC 输出来表达。这就意味着,在一条通道中,每多一笔正在发送的支付,其承诺交易都会多一个交易输出 —— 其体积会增大。闪电通道的免信任性来自其承诺交易总是可以得到区块链的确认,这就意味着承诺交易的体积不能超过一定的限度 —— 如果它大到无法塞进区块内,就不可能得到区块确认了。这就意味着,交易输出的数量不能超过一定的限度,也意味着正在发送的支付的数量不能超过这个限度。同时,它还意味着,恶意攻击者可以通过要求一条通道转发支付(本就不会成功的支付)来阻塞掉一条通道:假定一条通道同时最多只能转发 16 笔支付,那么,最小只需 8672 聪(16 * 542),就可以让这条通道不再能转发其他人的支付 —— 这被称为 “占位阻塞攻击(slot jamming attack)”。

但是,我们已经知道了,一个输出可以使用多种花费条件,这意味着,我们可以将多个 HTLC 合并到一个输出中;每揭晓一个原像,就能取走相应的数额;这种对数量和取款之后剩余资金的花费条件的约束,也是可以通过承诺交易来实现的。

这样的聚合 HTLC 输出有许多好处。一方面,它意味着,当一名用户同时取走多笔 HTLC 资金时,无论是通过原像分支还是通过超时分支,可以节省需要进入区块的交易的数量,从而节约手续费;另一方面,占位阻塞攻击将被大大缓解。

然而,这种 “多个 HTLC 在一个输出中并存” 的结构,将要求我们为所有的资金提取顺序安排可花费的脚本/交易。举个例子,假设我们聚合了 A、B、C 三个 HTLC 到一个输出中,那么,我们需要考虑 74 种取款可能性(例如,在 A 先被揭晓原像并取走的情形中,剩下的资金应该进入一个输出,允许 B、C 任意一个被先行揭晓原像取走,也允许 B、C 同时取走);如果我们要用承诺交易的方式来实现这样的聚合 HTLC,我们就需要预先签名 30 笔交易。可以看出,随着同时聚合的 HTLC 数量增加,需要考虑的取款顺序可能性会爆炸式增长。为了避免这方面的开销,需要有合适的操作码来帮我们实现更精巧的计算。

截至目前为止,“聚合 HTLC 输出” 同样仍处于方法的讨论阶段。详细介绍可见这篇文章 15。

结语

本文至此就要告一段落了。我们从基础的概念出发,解释了闪电通道的构造,闪电支付的概念,以及,其构造方法如何可以随着比特币协议的升级而升级。我们还介绍了尚在讨论阶段,无法当下就实现的可能性。

本文覆盖了一般的闪电网络概述的内容,但是,它依然未全面展现闪电网络的 “网络” 特性 —— 没错,HTLC 可以将多条通道内的支付 “粘合” 在一起,但问题是,发送者根据什么信息,找出这样的路径?这些节点,依据什么指令,开始向通道对手提供 HTLC?这些问题,我们将在下一篇解答。它们的答案,也将真正体现 “网络” 的特性,并越过一般的闪电网络概述止步之处。

下一篇: 闪电网络:技术与用户体验(三):路由

脚注

  1. https://www.btcstudy.org/2020/08/25/bitcoins-utxo-model-by-river-finance/

  2. https://www.btcstudy.org/2022/07/19/the-words-we-use-in-bitcoin/。又或者,可以理解成一种票据:每一张票据都是相互独立的,而一旦被使用,该票据就会作废,但可能形成数量不定的新票据 —— 它不是一种 “账户”。 ↩

  3. https://www.btcstudy.org/2022/09/24/script-a-mini-programming-language-by-Greg-Walker/

  4. https://www.btcstudy.org/2023/04/18/interesting-bitcoin-scripts-and-its-use-cases-part-1-introduction/

  5. https://www.btcstudy.org/2023/04/24/interesting-bitcoin-scripts-and-its-use-cases-part-5-lightning-channels-and-lightning-network/

  6. 通道内 HTLC 输出的锁定脚本:(1)也带有 RA1 单签名 这样的 惩罚 花费分支;(2)在剩余的两个分支中,其中一个分支(对于给出承诺交易的一方来说是对方取款的分支,而对获得承诺交易的一方来说是给自己取款的分支)将需要双方的签名(并且要求双方提前签名承诺交易),而不是仅一方的签名。比如,在这里,Alice 给 Bob 支付,那么,在 Alice 签名的承诺交易中,HTLC 输出的哈希锁花费分支就需要双方的签名,而非仅需 Bob 的签名;预先签名的、花费该哈希锁分支的交易称为 “HTLC-成功” 交易。而在 Bob 签名的交易中,HTLC 输出的超时分支就要求双方的签名,而非仅需 Alice 的签名;预先签名的、花费该超时分支的交易就称为 “HTLC-超时” 交易。这些交易都会将资金转移到一个带有相同 惩罚 花费分支的输出中,要求资金的名义主人(比如,在成功揭晓哈希原像的场景中是 Bob,而在超时撤回资金时是 Alice)在一个时间窗口之后才能取走资金。使用这样复杂、不对称的构造,是为了贯彻惩罚的时间窗口一致性:无论一笔旧的承诺交易在得到确认时,其 HTLC 允许该欺诈方取款的分支是否可以使用,被欺诈的一方都有相同的时间窗口可以发起反制。更细致的推理见此篇:https://ellemouton.com/posts/htlc-deep-dive/ 。 ↩

  7. https://www.btcstudy.org/2021/09/29/bitcoin-taproot-a-technical-explanation/

  8. https://www.btcstudy.org/2021/11/29/schnorr-applications-musig/

  9. https://www.btcstudy.org/2023/05/12/taproot-channel-translations-how-it-works/

  10. https://www.btcstudy.org/2021/12/02/schnorr-applications-scriptless-scripts/

  11. https://www.btcstudy.org/2021/10/26/payment-points-part-1-replacing-HTLC/

  12. https://www.btcstudy.org/2023/05/04/an-introduction-to-generalized-bitcoin-channels/

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

  14. https://delvingbitcoin.org/t/lnhance-bips-and-implementation/376/

  15. https://www.btcstudy.org/2023/11/15/htlc-output-aggregation-as-a-mitigation-for-tx-recycling-jamming-and-on-chain-efficicency/

本系列文章旨在填补关于闪电网络的文献资料的一些空白。对闪电网络和闪电通道的整体性介绍一般都撰写于几年前 1,因此,它们往往只及于闪电通道和闪电网络的基本技术概念,而:

  • 没有将技术原理与用户体验 —— 尤其是用户体验中的挑战 —— 关联起来,因此无法为读者上手使用闪电网络提供心智基础;
  • 没有体现闪电网络上已经出现的技术改进和解决方案,以及还可以探索的优化空间。一些解决方案是专为优化终端用户的体验而提出的,并且也成功地改善了用户体验;还有一些方案,虽然难以为终端用户所知,但也大大提高了闪电网络的支付效率。
    • 难以理解和思考闪电网络的优化空间的部分原因在于对其 “网络” 特性的认识不足。

有鉴于此,本系列文章希望给出一份新的介绍,将闪电网络(钱包)的使用体验(尤其是其挑战)与技术概念关联起来,并基于这些概念以及闪电网络的 “网络” 特性,介绍闪电网络上已经发生的技术改进,以及还可以采用的改进。

本文是这个系列的第一篇文章。与一般的介绍不同,本文不会涉及构造闪电通道的技术以及形成闪电网络的元素(这是后续篇章的内容),相反,我们将先了解闪电网络的使用体验(准确来说,是自主保管的移动端闪电钱包的体验)。我们将从闪电网络的基本想法出发,推理出它的使用体验中的关键部分。往后,我们将通过 “体验-技术” 的视角不断解释形成这些体验的技术原因,并相应呈现改进的空间。

本文不要求读者对比特币的可编程性有任何理解,但要求读者至少了解并能够辨识出比特币的使用体验 —— 在比特币网络上发送交易并使之得到区块链确认 —— 中的关键元素:地址、手续费、确认时间。

闪电网络的想法

闪电网络的想法非常简单:

  • 假设有一笔资金,是由甲乙双方共同支配的,也即只有双方一致同意,才能花费它;那么,当其中的甲方希望用这笔资金中属于自己的部分来给乙方支付时,这就不需要获得其他任何人的见证 —— 只需要乙方确认即可。甲乙双方可以在很长一段时间之后结算双方的余额,完成资金分割。
    • 这就像商业往来很密切的两个商店互相写欠条,仅在双方要对账结算的时候(比如月底或年底)才需要转移现金。
    • 这样的共同资金,称为 “通道”。
    • 一种直观的理解通道的方式是把一条通道想象成算盘 2 上的一根轴,轴有两端,轴上有一些算珠。当一方要给另一方支付时,就把自己这一端的一些算珠拨到对面去。abacus


图片来源

  • 假设一些人不止有一条通道,那么,他们就可以为其他人 “转发支付”。比如,甲与丙没有通道,但是甲跟乙有通道、乙跟丙也有通道,那么,甲就可以借助乙,给丙支付。例如,甲把钱交给乙,乙再把钱交给丙,就像接力跑一样。只需要每一条通道的双方彼此确认,就可以完成支付,也不需要其他任何人的见证。
    • 当许许多多的 “通道” 将许多的节点(用户)连接在一起,这些节点就形成了一个 “网络”,使得参与其中的一个节点无需跟其他每一个节点相连接,也(大概率)可以给这些节点支付。
      这就是闪电网络的想法:用一对一的通道来实现即时的支付确认;用通道组成的网络来最大化单个节点的支付能力。

这里,我们暂不讨论如何安全地实现 “通道” 和 “转发支付”,这会是后续章节的内容。我们需要知道的仅仅是,用户最常接触的比特币的保管形式是 “单签名钱包”,也即只需一把私钥就能花费的资金,显然是不能用来充当通道的,因为其控制是排他的。

重点是,光凭这段概述,我们就可以发现闪电网络使用体验中的几个关键元素。在使用闪电钱包之前,必须先理解这几个元素,才能顺利使用。

闪电网络的使用体验

在线收款

在闪电网络中,当你要收款的时候,你必须在线。从上面的推理来看,这是很容易理解的:收款涉及更改通道中的资金的状态,而通道是你与他人共享的,所以你必须在线,与对方一同更改资金的状态。

这既不同于比特币的使用体验(只要他人知道你的一个比特币地址,就随时可以给你支付,不需要你在线),也不同于绝大部分支付系统(比如银行、互联网电子支付系统)的使用体验。

不过,在当前的移动端闪电钱包中,这一点已经不会让用户产生太多感知:联网的手机都有通知系统,钱包的服务端可以向你发送通知,要求你打开 App,也即上线,以接收付款。但是,如果太长时间不处理这样的通知,可能会导致支付退回。

对这种特性的一种误解是,认为闪电网络钱包的用户必须全天候在线。这是不对的,因为不在线仅仅意味着你无法收款,并不代表你在通道中的资金立即变得不安全。但另一种正确的理解是,闪电网络钱包的用户不能无限期离线,你必须隔三岔五上线检查自己的资金状况。具体可容许的离线时间视用户所用的软件实现而定,一般来说,离线三天五天是没有问题的。其中的缘由我们会在后续章节中解释。

一定程度上,在线收款的特性也意味着,发送支付和收取支付的双方必须同时在线。但如上所述,这不难解决,关键是让接收支付的一方即时上线。在未来,这可以通过增加 “异步支付” 技术来解决这种同时性问题。

收款额度

类似地,因为收款涉及通道,所以,它的收款能力是有上限的。

我们以上面的那张图来举例。现在,算珠都位于 Alice 这边,因此,Alice 无法再用这条通道来接收支付了 —— 因为 Bob 在这条通道中已经没有余额了!但与此同时,Bob 使用这条通道来接收支付的能力,则是 Alice 在这条通道中的所有算珠。

这就是 “收款额度(inbound liquidity,入账流动性)” 的概念。这也是一个迥异于比特币和其它支付系统的概念。在其它支付系统中,接收支付的能力在大多数时候是没有限制的。

这种特性给闪电网络的用户带来了大量的不解和困扰。当你没有足够多的收款额度时,要么你的收款会失败(他人无法给你支付),要么,会导致新通道的创建(因此你需要支付在比特币链上确认一笔交易的手续费,这时候的手续费会远远高于你平时发起闪电支付的手续费)。

这些问题在刚入门的用户这里往往会更加严重,这跟闪电网络当前的实现方式有关:在当前的闪电网络中,往往仅由发起通道开设请求的一方向通道注入资金(这被称为 “单向注资”),但这就导致了,在刚刚创建好的通道中,一方(比如用户)将没有任何收款额度 —— 在你要接收支付之前,你必须先花掉一些钱。

现在,关于终端用户的收款额度问题,一种解决方式已经逐渐获得采用:安排一种服务商(称为 “LSP”),每当用户要收取支付、收款额度又不足时,就主动与用户开设新的通道,为用户提供收款额度(当然,也要收取开启通道的手续费)。这种办法同样也可以解决上述新用户的收款额度问题:当一名新用户第一次接收闪电支付时,LSP 与该用户打开通道,让该用户能够收取支付。唯一的问题是,它将对用户的第一笔闪电收款的数额提出要求,但无疑已经方便很多。

因此,本文推荐新用户在上手使用闪电网络钱包时,总是使用闪电钱包(而不是比特币地址)来接收第一笔支付。这样会更加便利。

此外,“双向注资” 技术也已经在主要的闪电网络客户端软件中实现,有望为进一步解决新用户的收款额度问题提供帮助。

对于非移动端用户,比如全时在线节点的运营者来说,有更多的方法可以获得收款额度,比如:“潜水艇互换”,我们将在后续篇章中介绍。这些方法在一定程度上也可以为移动端用户所用,但从便利性来说,都不及 LSP 的上述服务。
但总的来说,“收款额度” 会始终伴随你的闪电网络使用历程,并且也是闪电网络与其它支付系统最显著的区别。它造成了许多困惑,这也意味着,如果我们能理解它,就能少去很多困惑。

支付成功率

与 “收款额度” 相对的是 “支付额度(outbound liquidity)”。但是,这个词并不能体现闪电网络的特殊性,因为我们的每一种支付工具都有余额(支付能力的上限)。

我们回到上面的转发支付的例子。我们扩展成四方,并假设他们在彼此的通道中的余额如下图所示:

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

甲在 “甲-乙” 通道中有余额 500 聪,乙则有 300 聪。其余通道以此类推。在这里,虽然甲拥有 500 聪,但如果他要给丙或者丁支付,则支付额无法超过 200 聪。

无法给丁支付超过 200 聪的数额,显然不能归因于丁的收款额度不够 —— 在 “丙-丁” 通道中,丙还有 500 聪。问题在哪儿呢?在于 “乙-丙” 通道中,乙只有 200 聪。

也就是说,成功的支付,要求在支付所途径的每一条通道中,转发支付的一方都有足够多的余额(支付额度)。在甲给丁支付的过程中,如果支付数额超过 200 聪,这笔支付将无法通过 “乙-丙” 通道。

这就是为什么本文要使用 “支付成功率” 的概念。这也是为什么人们常常说闪电网络比较适合小额支付而不适合大额支付 —— 成功的支付不仅要找出一条由通道连成的路径,将支付者与接收者连起来,还要求组成通道的每一条通道,在传递支付的方向上都有足够多的余额。

“聪(satoshi)”是比特币的最小单位。

在后面的章节中,我们会了解如何可以优化支付的成功率。简单来说,我们需要做的是将一笔支付打散成多个碎片,让每一部分途径不同的路径,从而最大限度利用不同路径的支付额度。

结语

在本文中,我们了解了闪电网络的基本思想,并从这些基本思想出发,解释了闪电网络用户体验中的一些关键元素。这些描述无法涵盖今时今日移动端自主保管闪电钱包的全部体验,但勾勒出了其中的一部分。在往后的章节中,我们将越来越多地了解这些体验,也越来越多地解释其背后的技术或设计成因,并指出哪些技术进步已经改变了或有望改变这些体验。

闪电网络是一个网络 —— 对这个事实,再怎么强调都不过分。这种 “网络” 特性,既形成了其使用体验中的一些明显的特点(也许有人将它们视为一些缺点),也指明了这些体验的优化空间。

在下一章,我们将介绍闪电通道在比特币上的构造方法。

下一篇: 闪电网络:技术与用户体验(二):通道与支付

脚注

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

  2. https://medium.com/breez-technology/understanding-lightning-network-using-an-abacus-daad8dc4cf4b

1.address类型的初始值是:

  • 0
  • 0x0
  • ✅0x0000000000000000000000000000000000000000
  • 0x000000000000000000000000000000000000dEaD

2.bool类型的初始值是:

  • true
  • ✅false
  • 1
  • 0

3.bytes1的初始值是:

  • 0
  • 1
  • 0x0
  • ✅0x00

4.

1
2
3
4
5
string  public _string = "true";
function d() external returns(string memory){
delete _string;
return _string;
}

调用函数d,将返回:

  • true
  • false
  • 0
  • ✅””

5.

1
mapping(address => uint256) private _balances;

这是ERC20合约中的一行代码,其中未记录的用户的_balances值是:

  • ✅0
  • false
  • 无法判断
  • 依据整体代码而定

1.下面定义变量的语句中,会报错的一项是:

  • ✅uint256 public constant x1;
  • uint256 public constant x2 = 10;
  • uint256 public immutable x3;
  • uint256 public immutable x4 = 10;

2.下面定义变量的语句中,会报错的一项是:

  • string constant x5 = “hello world”;
  • address constant x6 = address(0);
  • ✅string immutable x7 = “hello world”;
  • address immutable x8 = address(0);

3.下面哪一项不符合对 constant 和 immutable 的描述?

  • constant 变量初始化之后,尝试改变它的值,会编译不通过
  • immutable 变量初始化之后,尝试改变它的值,会编译不通过
  • constant 和 immutable 的使用可以增强合约的安全性
  • ✅constant 和 immutable 的使用并不会节省 gas

4.在如下的合约中,我们定义了四个 immutable 的变量 y1, y2, y3, y4。

uint256 immutable y1;
address immutable y2;
address immutable y3;
uint256 immutable y4;
constructor (uint256 _y4){
y1 = block.number;
y2 = address(this);
y3 = msg.sender;
y4 = _y4;
}

其中,确实有必要在构造函数 constructor 中才赋值的一项是:

  • y1
  • y2
  • y3
  • ✅y4

5.下列哪一个变量不适合用 constant 或 immutable 来修饰?

  • 合约的部署者地址
  • 合约的部署时间
  • ✅合约中的 ETH 数量
  • 合约本身的地址

1.如果我们要声明一个mapping变量,记录不同地址的持仓数量,应该怎么做?

  • mapping(uint => address) public balanceOf
  • ✅mapping(address => uint) public balanceOf
  • mapping(address, uint) public balanceOf

2.不可以作为mapping中键(key)的变量类型的是:

  • ✅struct
  • string
  • address
  • 以上均不可

3.可以作为mapping中值(value)的变量类型是

  • struct
  • string
  • address
  • ✅以上均可

4.Mapping的存储位置可以是:

  • ✅storage
  • memory
  • calldata
  • 以上均可

5.给映射变量map新增键值对的方法:

  • map(_Key) = _Value;
  • ✅map[_Key] = _Value;
  • map.push(_Key, _Value);

6.mapping变量是否存长度信息?

  • ✅否

1.引用类型(Reference Type)包含以下:

  • 数组(array)
  • 结构体(struct)
  • 映射(mapping)
  • ✅以上全部

2.solidity数据存储位置的类型不包含以下:

  • storage
  • ✅stack
  • calldata
  • memory

3.合约中状态变量默认的存储位置类型为以下的:

  • ✅storage
  • calldata
  • memory

4.不同类型的引用变量相互赋值时,修改其中一个的值,不会导致另一个的值随之改变的是以下哪种情况:

  • 合约中的storage赋值给本地的storage
  • 合约中的memory赋值给本地的memory
  • ✅合约中的storage赋值给本地的memory
  • 以上全部

5.Solidity中变量按作用域划分,可分为:

  • 状态变量(state variable)
  • 局部变量(local variable)
  • 全局变量(global variable)
  • ✅以上全部

6.消耗gas最多的变量类型为:

  • ✅状态变量
  • 局部变量
  • 全局变量

7.下列表示“请求发起地址”的为:

  • msg.sig
  • ✅msg.sender
  • msg.data
  • msg.value

8.下列表示“当前区块的矿工地址”的为:

  • ✅block.coinbase
  • block.gaslimit
  • block.number
  • block.timestamp