关于在GPC中加入HTLC的兼容问题

在支付通道网络中,支付协议主要分为两种。第一种是仅存在于双方的协议,有RSMC,Eltoo等。另一种是多跳的支付协议,主要有HTLC以及Perun等。闪电网络采用的是RSMC+HTLC的组合方式。最近在看LN的实现,准备把Eltoo(GPC)和HTLC组合在一起,但是发现了一点问题。在阅读之前推荐先看一下GPC的介绍以及HTLC的介绍。

先介绍一下单跳的协议。RSMC简而言之就是如果任何一方提交旧commitment,那么另一方就可以通过拿走通道里所有的钱来惩罚作弊方。而在Eltoo(GPC)中,如果一方提交了旧commitment,那么另一方可以提交版本更新的commitment来进行替换。然后介绍一下HTLC,比如,A想要通过B和C向D支付

  1. D生成一个随机数并计算它的Hash H发给A。

  2. A向B承诺,如果能在块高120之前展示H的preimage P,那么A就向B支付100个CKB。B向C承诺,如果在块高100之前C能向B展示P,那么B会向C支付90个CKB。C也同理向D承诺。

  3. D向C展示preimage P,然后完成支付,此时C知道preimage以后,就向B展示,以此类推。

在闪电网络的实现中,时间锁是用的CLTV(相对时间锁)。同时,可以发现支付的金额随着路径的长度在减少,这里的差值就是鼓励节点参与路由的手续费。比如B就通过差值拿到了10个CKB。另外,可以看到承诺过期时间的差值随着路径长度递减,这是为了给每一个节点足够的时间在链上解决HTLC。为了解释清楚,需要介绍一下HTLC settle的方式。

  1. Happy path: 假设commitment1中有一个output是HTLC。如果双方都是诚实的,那么在D给C看了preimage以后,他们就会更新commitment1至commitment2,将HTLC的output直接划分给D。此时commitment2里就不存在这个HTLC output,而是直接放到了D的余额中。

  2. Sad path: 如果D给C看了P以后,C拒绝更新。那么此时D就会将commitment提交上链,然后通过提交P和自己的签名来解锁HTLC output。

由于链上的settlement涉及到confirmation period,同时从广播到被包含进块也需要时间,所以我们需要在HTLC的协议中设置递减的过期时间。这就保证了对于C来说,在他settle了CD之间的HTLC之后,BC之间的HTLC还没有过期。

问题出现在,当一方提交旧的commitment的时候,两个方案的区别。先谈RSMC,他允许另一方直接拿走作弊方的钱,包括HTLC。比如C向D承诺了一个HTLC,本来C需要在T个blocks之后才能解锁HTLC(退款),但是如果D发送的是过期的commitment,那么锁定脚本允许C利用revocable key直接解锁HTLC output。而在Eltoo中,我们利用的是challenge period。这可以允许一方通过不断提交closing transaction来浪费HTLC的锁定时间,请注意,closing transaction中并不包含HTLC,只有settlement中包含。还是假设A通过B,C向D支付。过期高度分别是120,100,80。当前块高是70。在块高80的时候,CD解决了他们的HTLC,此时B拒绝链下解决,并且,此时他提交版本号最低的commitment上链,并不断提交版本号为2,3,4…的commitment。同时C也会尝试提交最新的commitment。我假设B会出更高的手续费,所以他的交易总会被包含。这就是出现问题的地方,Eltoo只保证了在最后,最新的commitment可以上链,但是他并没有保证challenge period的时间。在上述的例子中,块高80的时候B不断发送旧commitment以"浪费"HTLC的时间直到块高为100,以至于使它过期。你可能会说,B这样需要付出昂贵的手续费,但是我想说的是,当HTLC的金额大于他所需要贡献的手续费的时候,这个攻击是有利可图的。

可以看出,这个攻击的核心就是利用了Eltoo中的challenge period使得HTLC过期。所以一个本能的解决方案就是,为什么我们不用相对时间锁呢?不幸的是,相对时间锁对此也无能为力。

假设此时块高为100,AB设定HTLC过期时间为60个块,BC设置过期时间为30个块。请注意,此时时间锁是相对时间锁。此时,A在块高100的时候提交了最新的commitment,那么过期时间就是160。然而,此时C不断提交旧commitment。我们假设在块高150的时候最新的BC之间的commitment才被包含进块。此时,BC的过期时间是150+30=180个块。此时,AB的过期时间是160,BC的过期时间是180。那么在块高160的时候,A拿走了AB中的HTLC的钱。因为C一直扣留着preimage,所以B此时无能为力。随后,在162块高时,C通过preimage拿走了B的钱。此时HTLC的原子性依旧被破坏了。

如何解决这个问题?初步有几个潜在的解决方案

  1. 在绝对时间锁下,根据HTLC的金额大小,对HTLC的过期时间进行扩大。可以看到,在这个攻击中,攻击者需要不断提交交易,而这是需要手续费的。同时,为了保证自己能在提交过程中获得胜利(因为善良的节点也会提交最新的commitment),他需要把手续费设置得比较大。如果HTLC的金额并不足以cover他的手续费支出,那么他就没有动机作恶。假设每个交易他需要付出20ckb,HLTC金额是100,那么我们在HTLC中额外加上20个块的过期时间。如果他要作恶,那么他每一轮都需要提交交易,那么此时他的成本就是20*20=400。一个理性的节点是不会为了100ckb的HTLC而支出400ckb的手续费的。这个解决方案的问题也很明显,对于拜占庭节点(并不是理性的)攻击依旧会发生。而且这会让HTLC的时间延长,对资金的流动性并不友好。而且善良的节点依旧失去了资金。

  2. 增加惩罚选项。每一方都需要设置一些资金来保证自己不会提交旧交易。因为提交旧交易是一个可证明的行为(在链上)。那么只要保证善良的节点最终可以通过拿走恶意节点的保证金就可以解决这个问题。

这两个想法都是非常naive的,还没有经过更深的思考,如果你有更好的意见,欢迎评论。

2 Likes

感觉 eltoo 里面关闭通道时时间的不确定性会让它在与其他组件组合的时候比较麻烦,可能这也是限制 eltoo 被应用到 lightning 里面的原因之一。

可以考虑把 eltoo GPC 改进为 “1-shot GPC”: 改造 GPC lock 和 closing tx 使得 channel 参与方只有一次提交 closing output 的机会,nonce 高的成为 settlement output,最后用 funding output 和 closing output 作为 input 构造 settlement tx。这样 GPC 会复杂一些,但是关闭通道只需要2-3步,时间有上限。

1 Like

你说的这个方案看起来更好,实现起来也比较方便。我目前能想到一个比较直接的实现方式需要修改以下几点。

  1. 在lock.args的两个pubkey后,添加一个bit的flag,用于代表对应的该pubkey的拥有者是否能提交closing tx,初始化为1。
  2. 提交closing tx的时候,在witness里额外需要一个对完整tx的签名,暂时叫submission signature,签名只要能被lock.args里的两个pubkey中的一个验证通过即可。
  3. lock script保证在closing tx的gpc output里,submission signature对应的pubkey后的flag被设置为0。

这样的设计可以保证一方只能提交一次closing tx,而且似乎不会对已有的过程造成影响。这样在设计HTLC的时候,只需要在expiry time中增加closing的最大时间(即两次closing的challenge period)即可。我觉得你说的改进非常棒,这似乎解决了eltoo最大的问题,成功地将closing的ugly case的时间降低到了O(1)。