一种通用的支付通道结构及其可组合性
Jan Xie
Nervos Core Team通道网络(Channel Network)可能是 layer2 舞台上最耀眼的角色。一个广泛部署的通道网络可以最大化交易吞吐量(无上限!),可以最小化交易处理延迟(可以和连接各方的网络一样快),可以增强交易的隐私性,甚至可以为区块链提供一定的互操作性。简而言之,如果运用得当,通道网络将会是解决问题的灵丹妙药。
不同于基于零知识证明的协议或者其他 layer 2 链,通道网络的基本思想非常简单:使用直接连接双方的网络进行链下的交易互换,并确保这些交易在区块链上都是可执行的,然后通过这种点对点的连接共同组合成一个网络,在这个网络中的任意两个点之间都有一条或者多条路径连接着它们。尽管通道网络自身也存在一些问题,比如流动性效率和路径问题,但这些问题都是可以解决的,或者说是有近似的最优解的。
相比于 layer 1,layer 2 协议在部署之后相对是比较容易升级的,因此实现通道网络的最佳方案就是先启动它然后再逐步发展改进它。闪电网络就是一个这样的典型案例。本文将介绍一种简单的、可组合性强的基于 CKB 网络的通用支付通道结构,希望能够引起更多的研究和关注,引发更多关于通道网络的讨论,促进 CKB 上通道网络的建设和发展。
介绍
支付通道网络(Payment Channel Network 缩写:PCN)是一个去中心化的支付网络,用来支持全球支付。通道网络的设计主要包含两个方面,第一:通道协议,它规定了两个用户在一个直接相连的通道内的链下信息传递、链上合约和链上链下交互;第二:网络协议,它规定了如何将多个直接相连的通道组合成一个较长的通道,如何在两个用户之间找到一条路径等等。本文主要讨论一种在 CKB 上的通用支付通道结构。它是通用的,因为它基本适用于 CKB 上的任意一种资产。
通道结构最主要的问题在于,如何确保只有最新的通道状态才会在链上被认为是有效的。闪电网络使用的 Poon-Dryja 结构是基于撤销惩罚实现的,这给双方带来了复杂的密钥管理和状态管理,进而增加了正确实现协议的难度和节点操作的风险。Decker-Wattenhofer 结构提供了一种基于时间戳的状态更新机制,缺点是只允许有限数量的状态更新,这意味着 layer 1 和用户都需要付出更高的成本,因为通道必须定时关闭和开启。Decker-Russell-Osuntokun 结构的 ‘eltoo’ 是一个优雅的基于版本(Replace by Version)的解决方案,但是它需要在比特币中引入一个新的操作码 SIGHASH_NOINPUT。以太坊提供一个有状态的图灵完备的智能合约模型,这使得构造基于版本的解决方案更加容易,如 Sprites,ForceMove 以及 Perun。
(译者注,Replace by Version 缩写 RbV,每个状态有一个表示状态版本的单调递增的计数器。在发生争议时,经过授权的具有最高版本的状态被认定为最新状态,如果另一个新的状态具有较大的版本号,那么它将替换先前的状态)
结构
这里描述的通用支付通道结构主要是指一个基于 CKB 的 eltoo 端口。然而,我们将看到这样一个几乎相同的想法在 CKB 上不仅更容易实现,而且还展现出比特币/以太坊的支付通道方案中不存在的可组合性等优点,只是在设计上稍微有一点点谨慎。通道的可组合性是指通道结构可以适用于其他在 layer1 上的资产和 dapps。通过通道的可组合性,我们可以为所有的 layer 1 的资产构建一个通用的支付通道网络。
通用支付通道结构由一个通用支付通道 lock 和三种不同类型的交易组成。
通用支付通道 lock(GPC lock)
GPC lock 是通用支付通道的关键组成部分,它是通道内的 cells 使用的 lock script。它有 5 个参数:
- state:锁状态,一个
OPEN
和CLOSING
- timeout:关闭周期长度
- pubkey_a:Alice 的公钥
- pubkey_b:Bob 的公钥
- nonce:一个单调递增的无符号整数
一个通用支付通道可以被建模为一个简单的有限状态机,包含了三种状态: OPEN
(打开), CLOSING
(关闭)和 SETTLED
(结束)。 OPEN
是初始状态, OPEN
和 CLOSING
分别用 GPC lock 的 OPEN 和 CLOSING 状态进行表示,而没有 GPC lock 就表示为 SETTLED
。
当 GPC lock 被解锁时,状态将会发生转变。当 GPC lock 的状态为 OPEN 时,它可以在两种情况下被解锁:双向关闭和单向关闭(可以暂时跳过以下这部分内容,当你阅读到“关闭通道”章节的时候再回来。)
- 情况 0 双向关闭,当消费交易满足结算交易的条件时,可以解锁:
- 至少有 1 个输入:
- input 0:指向带有 GPC lock 的输出(译者注:每一笔输入,都是之前某笔交易的输出),并且它的 lock 状态必须是
OPEN
。 - witness 0:必须是 0 || sig_1 || sig_2 的格式,其中
- 0 代表情况 0
- sig_1 是 Alice(或者 Bob)签名的消费交易哈希
- sig_2 是 Bob(或者 Alice)签名的消费交易哈希
- input 0:指向带有 GPC lock 的输出(译者注:每一笔输入,都是之前某笔交易的输出),并且它的 lock 状态必须是
- 恰有 2 个输出:
- output 0:属于 Alice 的输出,必须使用和输入 0 相同的 type script
- output 1:属于 Bob 的输出,必须使用和输入 0 相同的 type script
- 至少有 1 个输入:
- 情况 1 单向关闭,当消费交易满足关闭交易的条件时,可以解锁:
- 至少有 1 个输入:
- input 0:指向带有 GPC lock 的输出
- witness 0:必须是 1 || sig_1 || sig_2 的格式,其中
- 1 代表情况 1
- sig_1 是 Alice(或者 Bob)签名的关闭交易的无输入哈希(no-input hash)(稍后详细介绍)
- sig_2 是 Bob(或者 Alice)签名的关闭交易的无输入哈希
- 恰有 1 个输出:
- output 0:与消耗的输出(就是指 input 0)相同,除了:
- 它的 GPC lock 的 nonce 必须大于 input 0 中的 nonce
- 它的 GPC lock 的 state 必须是
CLOSING
- output 0:与消耗的输出(就是指 input 0)相同,除了:
- 至少有 1 个输入:
状态为 CLOSING
的 GPC lock 可以在两种情况下被解锁,超时和单向关闭
- 情况 0 超时,当消费交易满足结算交易的条件时,可以解锁:
- 至少有 1 个输入:
- input 0:指向带有 GPC lock 的输出,并且它的 lock 状态必须是
CLOSING
since
必须设置成和 GPC lock 中的 timeout 一样的值,作为相对时间锁
- witness 0:必须是 0 || sig_1 || sig_2 的格式,其中
- 0 代表情况 0
- sig_1 是 Alice(或者 Bob)签名的消费交易哈希
- sig_2 是 Bob(或者 Alice)签名的消费交易哈希
- input 0:指向带有 GPC lock 的输出,并且它的 lock 状态必须是
- 恰有 2 个输出:
- output 0:Alice 的输出
- type script 必须和 input 0 一致
- 必须使用默认的
secp256k1_blake160
lock,并且地址为 input 0 中的 pubkey_a
- output 1:Bob 的输出
- type script 必须和 input 0 一致
- 必须使用默认的
secp256k1_blake160
lock,并且地址为 input 0 中的 pubkey_b
- output 0:Alice 的输出
- 至少有 1 个输入:
- 情况 1 单向关闭,和状态为
OPEN
下的单向关闭条件相同
在情况 0 的 input 0 中设置一个相对时间锁,用来确保结算交易只能在 timeout 区块之后才能被引用。
交易类型
出资交易 —— 一笔出资交易就是打开了一个新的通用支付通道。它有一个 funding output ,该 output 的 lock 就是一个状态为 OPEN
的 GPC lock。
关闭交易 —— 一笔关闭交易会消费 funding output 或者另一笔关闭交易中的 output。它会有一个 closing output,该 output 的 lock 就是一个状态为 CLOSING
的 GPC lock。它总是会有一笔匹配的结算交易。
关闭交易的独特之处在于它们并不会固定于一个先前的 outpoint。当一笔关闭交易被签名时,它的输入被排除在哈希过程之外,因此签名只会包括 outputs。这个特殊的哈希过程被称为无输入哈希(no-input hash)。例如,当用户想要签署一笔关闭交易时,他/她会序列化输出并对其进行哈希,然后在哈希结果上进行签名。这样在结束交易中的输入不会影响签名交换。正因为一笔关闭交易可以附加到一笔出资交易或者另一笔关闭交易,并且可以在关闭交易签名之后的一段时间后再确认连接。这样的操作可以在 CKB 上直接实现,无需添加新的操作码,因为一笔 CKB 交易的哈希和签名是完全可定制的。
GPC lock 情况 1(单向关闭),它要求关闭交易的 output 0 必须与其 input 0 一致,除了它们 lock 中的 nonce 和 state,这意味着它的 input 0 只能连接到先前的使用类似 GPC lock 的 output 作为它自己的 output 0(timeout,pubkey_a 和 pubkey_b 需要一致)。
结算交易 —— 结算交易消费了一个 closing output 并关闭了一个通用支付通道。它有两个输出,分别将资金给到 Alice 和 Bob。它总是会有一笔匹配的关闭交易。
示例流程
这里我给出一个交互流程的示例。它只是一个用于解释的草拟协议,而不是一个包含确切步骤的真正的协议。草拟和真正协议的关键区别在于在每次状态转换(即每个关闭/结算交易对)上是如何达成一致的。考虑到并发性和公平交换等问题,两方协议的问题要比听起来复杂的多。当然也是存在解决方案的,但是它已经超出了本文的范畴,并且和通用支付通道结构毫不相关。我只是假设在必要时会使用这样一种合适的安全协议,例如在交换签名的时候。
开启通道
假设 Alice 想要与 Bob 开通一个新的通用支付通道:
- Alice 向 Bob 发起一个请求,协商通道参数,包括:输入资金,超时,公钥等
- Alice 和 Bob 使用预先商量好的参数在本地构建一个出资交易,包含双方的 inputs 和一个 funding output:
- output 0:一个由 GPC lock 锁定的 funding output:
- state:OPEN
- timeout:一个协商好的值 T
- pubkey_a:Alice 的公钥
- pubkey_b:Bob 的公钥
- nonce:0
- Alice 和 Bob 构建第一对关闭交易和结算交易:
- 关闭交易 ctx0,按照情况 1 单向关闭,消费了出资交易中的 output 0
- 结算交易 stx0,按照情况 0 超时,消费了关闭交易中的 output 0。stx0 的输出代表了 Alice 和 Bob 的初始余额。
- Alice 和 Bob 交换了 stx0 和 ctx0 的签名
- Bob 签名了出资交易并将签名发送给了 Alice
- Alice 将签名好的出资交易广播出去,一旦这笔交易上链了,Alice 和 Bob 就认为这个新的通道已经打开了。
通道 id 可以定义为 H( funding_transaction_hash || 0
)。如果 funding output 有自己的 type script 集,则该通道被认定为 UDT 通道,否则会被认定为 CKByte 通道。
第一对关闭交易和结算交易会在出资交易之前就被创建和签名好,这样就可以确保初始资金不会永远地被锁在通道内。如果任意一方在通道开启后没有任何反应,另一方总是可以通过单方面关闭通道来索要会他/她的资金。
完成一笔支付
为了完成一笔支付,Alice/Bob 只需要简单地在一对新的关闭和结算交易上进行签名并交换。支付是双向的。
我们假设当前最新达成的一对关闭和结算交易是 ctxi 和 stxi。ctxi 是关闭交易,它的 GPC lock 的 nonce 是 i。stxi 是结算交易,消费的是 ctxi 的 output 0。stxi 的两个输出分别是 A UDTs 和 B UDTs,分别代表 Alice 和 Bob 最新的余额。假设 Alice 想要支付 X UDTs 给 Bob:
- Alice 创建了一对新的 ctxj 和 stxj,其中:
- j = x + 1
- 关闭交易 ctxj 根据情况 1 单向关闭,消费了出资交易中的 output 0
- 结算交易 stxj 根据情况 0 超时,消费了关闭交易 ctxj 中的 output 0。stxj 的两个输出分别为 A-X 和 B+X sUDTs。
- Alice 将 ctxj 和 stxj 发送给 Bob,Bob 验证它们的有效性
- Alice 和 Bob 交换 ctxj 和 stxj 的签名
Bob 通过以下两点验证新交易对的有效性。第一,确保它们是有效的 CKB 交易,例如 lock/type script 可以成功验证;第二,stxj 的输出能够正确反映最新的余额。这里重复使用了 type scripts 中相同的验证规则,以确保每个状态转换都是经过允许的,并且对结算交易的输出进行检查,确保包含状态转换。
每一对关闭和结算交易都表示一个新的通道状态。Alice 和 Bob 通过交换他们的签名来对一个新的状态达成一致,如步骤 3 所示。只有在双方均已签名的情况下,一个新的关闭/结算交易对才会被认定为是有效的。
关闭通道
一个通用支付通道可以通过三种方式进行关闭。
完美的
Alice 和 Bob 都在线,并且他们将通过签名一笔结算交易来协作关闭通道。假设最新达成一致的关闭和结算交易对为 ctxi 和 stxi,Alice 发起一笔双向关闭:
- Alice 按照
OPEN
lock 中情况 0 双向关闭,创建一笔结算交易 stxb(b 代表双向)。stxb 的输出使用和 stxi 的输出相同的 data 和 type。 - Alice 根据自己的选择设置 output 0 的 lock,然后将 stxb 发送给 Bob
- Bob 验证 stxb 的输出是不是和 stxi 的输出具有相同的 data 和 type。如果相符,Bob 根据自己的选择设置 output 1 的 lock,签名交易然后将签名 sig_b 发送给 Alice。
- Alice 验证 sig_b,签名交易并获得 sig_a,然后设置 witness 0 为 0||sig_b||sig_a。
- Alice 广播这笔结算交易
一旦这笔计算交易上链了,双方就认为通道已经关闭。在这种最优路径中,双方都可以在指定的任意地址中立即收到他们的资金。
糟糕的
Alice 想要关闭通道,但是 Bob 没有响应(可能 Bob 有未支付的 VPS 账单,并且他的服务提供商并没有很宽容)。Alice 可以简单地使用最新达成一致的关闭交易 ctxi 来启动单向关闭,并在 T 个区块后关闭这个通道(就是指定的 timeout):
- Alice 设定 ctxi 的 input 0 为 funding output
- Alice 广播这笔关闭交易 ctxi(这是具有最新 nonce 的关闭交易)
- 等待 T 个区块
- Alice 广播结算交易 stxi
一旦关闭交易上链了,通道(以及它的 GPC lock)就处于 CLOSING
状态了。关闭周期将持续 T 个区块(由 GPC lock 中的 timeout 指定),在此期间,另一方可以通过提交他/她手中的任意 ctxj,只要 j > i。
在这里,我们假设 Alice 是诚实的,并且 ctxi 确实是最新的关闭交易,所以没人能够提交一个具有更大 nonce 的关闭交易来消费这个 closing output。在 T 个区块后,Alice 广播 stxi 来关闭这个通道。双方都会在预先设定的地址中接收到资金。
丑恶的
假设最新的关闭交易是 ctxi,但是 Bob 通过发送一个过时的关闭交易 ctxj 其中 j < i,故意发起单向关闭。一旦 Alice 发现了关闭交易 ctxj,她必须在关闭周期 T 超时之前,用另一个 nonce 更大的关闭交易(在本例中是 ctxi)消费掉 ctxj 的输出来进行响应。一旦 ctxi 上链了,关闭周期将延长 T 个区块。如果存在一个 ctxk 其中 k > i 可以附在 ctxi 上,这样就会再次更新链上的通道状态,否则 Alice 可以在 T 个区块超时之后广播 stxi。假设 Alice 是诚实的,并且 ctxi 确实是最新的达成一致的状态,那么整个流程大概是这样的:
- Bob 广播了 ctxj 然后它上链了
- Alice 发现了 ctxj,她立即将 ctxj 中的 output 0 设置成 ctxi 中的 input 0,然后广播 ctxi
- Alcie 等待 T 个区块
- Alice 广播 stxi
如果 Bob 在链上广播更多过时的关闭交易,比如 ctxj+1,ctxj+2,这会阻止 Alice 通过 ctxi/stxi 关闭通道,整个过程可能会变得很长。然而既然 ctxi 具有最新的 nonce,那么在 Bob 用完所有的过期交易之后,它最终还是会被接受。在最坏的情况下,Alice 需要等待 O(i*T) 个区块,才能最终取回她的资金,但是从交易费用而言,对 Bob 来说是 O(i),对 Alice 来说是 O(1),使得这样的攻击是不经济的。
交易费用
可以在关闭和结算交易中增加更多的输入,这允许参与者能够在广播时间内确认交易费用。
基本原理
在出资交易被签名之前,任何一方如果不想再继续,都可以在没有任何损失的情况下停止。如果出资交易已经被签名,但是通道的最初开启人还没有广播它,则另一方依旧总是可以发送另一笔交易,消费和等待被广播的出资交易相同的输入,从而使之无效。
一旦出资交易上链并且通道已经打开,通道的任何一方都可以发起单向关闭,如“关闭通道:糟糕的”中所述,由他/她自己关闭所有通道,如果对手发起单向关闭,一方可以等待关闭超时或更新 GPC lock,前提是他/她需要有一个 nonce 值更大的余额状态。关闭期为任何一方提供了一个可以提交其最新状态的窗口,在结束时通道将以 nonce 值最大的状态进行结算。
可组合性
通用支付通道结构可以适用于任意一种 CKB 上发行的 token,无论它是原生的 CKByte 还是 UDT。此结构只涉及 cell 的 lock,cell 的 data 和 type 字段将留给智能合约开发者们去进行处理。这个小小的接口使得它可以兼容任何只使用了 data 和 type 字段的合约。sUDT 就是这样的一种合约,未来的 UDT 标准也将是如此。一个 UDT 的开发者将不需要再去考量如何将 UDTs 通道化,因为它随时都可以在不需要做任何改动的情况下被通道化。这代表 UDTs 生来就是可通道化的,并且通道化和扩容 UDTs 的开发成本是「0」。所以 UDTs 不只是 layer1 上的一等公民,也是 layer2 上的一等公民,因为它可以在不做任何改动的情况下就在 layer1 和 layer2 之间自由的流入流出。 这也给予了支付通道网络最大的流动性,因为所有 layer1 上的资产都可以在无摩擦的情况下加入 layer2 的池子中。
这和比特币和以太坊上面的支付通道完全不同。比特币上的支付通道只有比特币可以运作,因为比特币是唯一的资产,并且这个区块链的能力是有限的。至于以太坊上虽然有非常多种的资产,但是它的 ERC 20 代币和通道的可组合性却是受限的。支付通道必须要和 token 通过预先已经定义好的接口进行交互,但这些接口对于不同的 token 而言可能是不同的。举例来说,Celer 才刚以硬编码的方式支持 ETH 和 ERC20 代币并且需要依赖它的 approve/transferFrom 的机制。即使 ERC token 遵循同一个标准接口来实现通道化,可能仍然是不安全的,因为接口的语意可能是不一样的,近期的 Uniswap + ERC777 事件已经说明了这一问题。相反的,CKB 上的 GPC lock,根本不需要去管 UDT 具体是如何实现的。
通用支付通道结构也能够在 anyone-can-pay 的 lock 中运行,也就是说当关闭交易的时候可以用任意的 lock 作为输出。 通用支付通道和 UDTs 的可组合性,对于用户而言是更便于使用的。例如,他们在打开通道前不需要用“Approve”方法来进行授权。他们的资金可以像往常一样安全地放在自己的钱包里面,在任何需要的时候通过单笔签名交易将资金发送到通道中即可。
这个结构的可组合性让我们可以在 CKB 上创造出一些有趣的使用范例,例如将稳定币进行通道化。不论你是算法稳定币还是基于信任的稳定币,他从第一天起就是支付通道网络上的一等公民。稳定币的支付通道网络可以带给用户超越 Visa,Paypal 以及支付宝的顺滑体验,因为它是快速的,可扩展,无需许可的并且有未来的 —— 我说的是基于 APIs 和 IoT 的流支付!
双方共同出资
以太坊上的支付/状态通道当通道被打开时也面临了原子性的问题,因为以太坊上的交易只有一个发送方,但通道的资金却可能是来自双方的。为了允许双方共同出资, Ethereum 的通道可以使用两个独立的出资交易,这也会给交易的对手方带来风险,因为一方可以在另一方已经承诺出资之后终止通道,或通过使用更多的链上交易来模拟原子双方共同出资,从而增加未来打开新通道的成本,同时这又会取决于 ERC20 特定的接口。CKB 上的通用支付通道的出资交易可以拥有多个输入,自然地就可以支持双方共同出资。
有状态的锁定模式
通用支付通道的结构只依赖于 lock script。lock script 是有状态的,它模拟了一个简单的状态机,然后在 lock script 中使用 args(而不是 cell 中的 data) 来维持内部的状态。当需要一个适度复杂的状态 lock 时,也可以在其他地方应用这个模式。
对于具备更复杂的状态机的 lock,有两种替代方案可以考虑。第一种是将 data 变成是一个在 lock script 和 type script 之间可以共享的字段,这需要预先定义好数据格式,以便每个脚本可以读/写自己的数据,而不与其他脚本发生干扰。第二种方式是使用一个单独的 cell 来维持 lock 的状态,并且将锁定的 cell 和锁定状态的 cell 一起转换。这两种方案是最明显的,但绝对还有其他可能的模式。
未来工作展望
通用支付通道是一个简单的结构,灵感源自 eltoo,然而它不仅在 CKB 上更容易实现,而且还免费获得了额外的优异性质,如上所述。cells 的“状态对象”的本质天然适合通道网络和 layer 2,我希望关于通道网络的研究可以吸引到的来自社区的关注。这里还有一些有趣的问题:
- 状态通道 —— 状态通道是支付通道的一种泛化,用于支持智能合约的链下执行。GPC lock 不包含支付的特定结构,这会是状态通道建设的一个良好的开端。
- 通道网络 —— 如何连接通用支付通道,进而任何人都可以通过一些中间节点就可以直接或者间接地向通道网络中的任何一个人进行支付?我们是像闪电网络一样使用 HTLC 还是有什么更好的解决方案?
- 瞭望塔 —— 如何通过一种无需信任的方式将纠纷委托给第三方?这样通道的参与者就可以离线去度假了
- 路径 —— 如何找到通道网络中两个点之间的最佳路径?考虑到通道的开启/关闭和通道容量的变化,整个网络处于不断变化的状态中,而节点对于整个拓扑网络仅拥有部分信息。“最佳”的定义也因人而异。由于这些限制,开发几种不同的路径策略并将选择权留给用户可能会更加合理。这一领域有很多有趣的论文。
- 多部分支付 —— 一次支付的金额越大,就越难找到一条有足够容量的路径来传递它。多部分支付的概念是将大额支付拆分成数笔小额支付,并通过不同的路径将它们发送到同一个目的地,并确保小额支付们被全部接受或者全部拒绝。