最近一年我在做 Fiber Network 这个新的开源项目,上个月底刚好主网第一个版本发布:
这个项目的挑战还是挺大的,上主网只是一个新的开始。我在开发过程中学到了很多东西,这是我前段时间写的一篇关于 Fiber 的大致介绍。
Fiber 简介
Fiber 是基于 CKB 构建的闪电网络协议,旨在实现快速、安全且高效的链下支付解决方案。借鉴了比特币闪电网络的核心理念,Fiber 针对 CKB 的独特架构进行了深度优化,提供低延迟、高吞吐量的支付通道,适用于微支付和高频交易等场景。与传统的闪电网络不同,Fiber 拥有多项关键特性:
- 多资产支持:不再局限于单一币种,能够处理多种资产交易,为复杂的跨链金融应用铺平道路。
- 可编程性:基于 CKB 的图灵完备智能合约,支持更复杂的条件执行和业务逻辑,拓展了支付通道的应用边界。
- 跨链互操作性:原生设计支持与其他 UTXO 链(如比特币)的闪电网络交互,提升了链间资产流动性和网络兼容性。
- 更灵活的状态管理:得益于 CKB 的 Cell 模型,Fiber 可以更高效地管理通道状态,降低链下交互的复杂度。
在这篇文章中,我们将从源码层面介绍 Fiber 的整体架构和主要模块,以及项目的后续展望和规划。
前提知识
- Rust, and actor framework,Fiber 是一个完全由 Rust 编程语言所实现的项目,另外我们在实现中采用了 actor model 的模式,依赖社区的项目 ractor 框架。
- Lightning network,Fiber 的基本思想沿用了 Bitcoin 的闪电网络,基本原理是一致的,所以 Mastering lightning network 和 Bolts: lightning/bolts 是非常有用的参考资料。
- CKB transaction and contract,Fiber 会通过 RPC 与 CKB node 进行交互,比如 funding transaction 或者 shutdown commitment transaction 可能需要通过 RPC 提交给 CKB 的节点,所以掌握 Fiber 需要了解一些 CKB 合约开发方面的知识。
重要模块
我们从最高纬度去看一个 Fiber Node,主要包含下面几个主要模块:
其中:
- Network Actor 是 Fiber Node 中负责节点内外的消息通信
- Network graph 包含一个节点对于整个网络里其他节点和 channel 的信息,当一个 Fiber Node 收到一个支付请求的时候,我们首先会尝试从 network graph 中找到一条路径能够触达收款节点,这个 network graph 结构是跟着网络上的 gossip 信息不断更新的
- PaymentSession 负责管理一个支付的生命周期
- fiber-sphinx 是我们自己实现的 onion packet 加解密 Rust 库
- Gossip 是 Fiber 节点之间的交换网络消息的协议,用于 Node 和 Channe 的发现和更新。
- Watchtower,这里负责监听 Fiber node 所关心的 channel 里面的重要事件,另外如果某个 Node 提交一个老的 commitment transaction,watch tower 负责发出 revocation transaction 来进行惩罚
- Cross hub,这个模块负责跨链的互操作,比如付款者通过 Bitcoin 的闪电网络发出 Bitcoin,而接收者收到的是 CKB,cross hub 这里会进行一个转换,将 Bitcoin 的 payment 和 invoice 和 Fiber 这边的 payment 和 invoice 进行映射管理
- Fiber-script 在一个单独的代码仓库,这里面包含了两个主要的合约,funding-lock 是一个资金锁定合约,使用 ckb-auth 库来实现一个 2-of-2 多重签名,commitment-lock 实现了 daric 协议来作为 Fiber 的惩罚机制
Actor Model 和 Channel 管理
Channel 的管理是闪电网络中非常重要、也是异常复杂的部分。其中的复杂性主要来自于 Channel 内部数据和状态的改变来自于网络上 peer 之间的交互,事件的处理可能存在并发上的问题,一个 Channel 的双边可能同时都有 TLC 的操作。
闪电网络本质上是一个 P2P 系统,节点之间通过网络消息相互通信进而改变内部的数据状态,我们发现 Actor Model 非常适合这种场景:
Actor Model 极大地简化了代码实现的复杂度,使用 Actor model 后我们不需要使用锁来保护数据的更新,当一个 Message handle 结束的时候,我们会把 channel state 的数据更新写入 db。而像 rust lightning 如果没用使用 actor model,就可能会涉及到非常复杂的锁相关的操作。
我们的所有的重要模块都采用了 Actor Model,Network Actor负责节点内外的消息通信,比如一个节点要给另外一个节点发送 Open channel 的消息,这个消息首先会通过 Fiber node A 的 channel actor 发送到 network actor,node A 的 network actor 通过更底层的网络层 tentacle 发送到 node B 的 network actor,然后 network actor 再发给 node B 里面的所对应的 channel actor。
在一个 Fiber Node 内部,每一个新的 Channel 我们都会建立一个对应的 ChannelActor,而这个 ChannelActorState 里面包含了这个 Channel 所需要持久化的所有的数据。采用 Actor Model 的另外一个好处就是我们能够在代码实现过程中直观地把 HTLC 网络协议相关的操作映射到一个函数里,比如下图中展示了 HTLC 在多个节点之间的流转过程,对于 A 到 B 之间的 AddTlc 操作,节点 A 里的 actor 0 所应对的代码实现就是 handle_add_tlc_command,而节点 B 里的 actor 1 所对应的代码实现是 handle_add_tlc_peer_message。
Channel 之间的 TLC 操作是复杂度非常高的部分,我们在实现上延用了 rust-lightning 的方式,使用状态机来表示 TLC 的状态,根据 actor 之间的 commitment_sign 和 revoke_ack 的消息来改变状态机,总的来说 AddTlc 的操作流程和两个 Peer TLC 状态的改变过程如下:
支付和多跳路由
每个 Fiber 节点都通过 Network graph 保存了自己对于整个网络的了解情况,本质上这是一个双向有向图,每一个 Fiber 节点对应于 Graph 里面的一个 vertex,每一个 Channel 对应于 Graph 里面的一个 edge,出于隐私保护的需求,Channel 的真实 balance 不会广播到网络中,所有 edge 的大小是 Channel 的 capacity。
在支付开始前,发起者会通过路径规划找到一条通往收款者的路径,如果有多条路径就需要找到各方面综合考虑最优的路径,而在信息缺失的图中找到最优路径是一个在工程上非常具有挑战性的问题,Mastering Lightning Network 对这个问题有很详细的介绍:
在 Fiber 中,支付动作由用户向 Fiber Node 通过 RPC 发起请求,节点收到请求后会创建对应的 PaymentSession 来追踪支付的生命周期。
目前我们的路径规划的算法是一个变形的 Dijkstra 算法,这个算法是通过 target 往 source 方向扩展的,搜索路径的过程中通过折算支付成功的概率、fee、TLC 的 lock time 这些因素到一个 weight 来进行排序。其中的概率估算来自于每次支付的结果记录和分析,实现在 eval_probability。路径的选择质量好坏对于整个网络的效率和支付的成功率非常重要,这部分我们今后将会继续改进,Multipart payments (MPP) 也是一个今后可能要实现的功能。
路径规划完成后下一步就是构建 Onion Packet,然后给通过 source node 发起 AddTlcCommand。后续如果 TLC 失败或者成功会通过事件通知的方式处理。
整个支付的过程可能会发生多次的重试,一个常见的场景就是我们使用 capacity 作为 Graph 里边的容量,可能路径规划出来的路线无法真实满足支付的大小,所以我们需要返回错误并更新 Graph,然后再继续自动发起下一次路径规划尝试进行支付。
节点广播协议 Gossip
Fiber 的节点之间的通过相互发送广播消息交换新的 Node 和 Channel 信息,Fiber 中的 Gossip 模块实现了 Botls 7 定义的 routing gossip。在实现过程中我们的主要技术决策在这个 PR: Refactor gossip protocol里面有描述。
当一个 Node 节点第一次启动的时候,会通过配置文件里的 bootnode_addrs来的连接第一批 peers,广播消息的类型有三类:NodeAnnouncement
、ChannelAnnouncement
、ChannelUpdate
。
Fiber 会把收到的广播的原始数据保存下来,这样方便通过 timestamp + message_id 组合的 cursor 来对广播消息进行检索,以方便来自 peer node 的 query 请求。
当一个节点启动的时候,Graph 模块会通过 load_from_store来读取所有的 messages,重新构建自己的 network graph。
我们采用基于订阅的方式在网络中传播消息。一个节点需要主动向另一个节点发送广播消息过滤器(BroadcastMessagesFilter),另一个节点收到了该消息之后会为其创建对应的 PeerFilterActor,在构造函数里创建 Gossip 消息订阅。通过基于订阅的模型这种方式,我们可以让其他节点接收在特定的 cursor 之后接收到新保存的 Gossip 消息。
隐私 Onion 加解密
处于隐私保护的需求,payment 的 TLC 在多个节点之间传播的时候,每个节点只能知道自己所需要的信息,比如当前节点接收的 TLC 的 amount、expiry、下一个传播的节点等信息,而无法获得其他不必要的信息,而且每个 hop 在发送 TLC 给下一个节点的时候也需要做相应的混淆。
类似的,如果 payment 在某个节点传播的过程中发生了错误,这个节点也可能返回一个错误信息,而这个错误信息会通过 payment 的 route 反向传递给 payment 的发起节点。这个错误信息也是需要 Onion 加密的,这样确保中间节点无法理解错误的具体内容,而只有发送者能够获得错误内容。
我们参考了 rust-lightning 在 onion packet 的实现,发现其实现方式还是不够通用 (会绑定于其项目的具体数据结构),所以我们自己从头开始实现了 fiber-sphinx,更详细的内容请参考项目的 spec。
涉及到 Onion 加解密的几个关键节点在这三个地方:
- send payment 的时候创建 onion packet,作为 AddTlcCommand 里的一个字段发送给第一个 hop
- payment route 中的每个节点收到 TLC 之后,尝试去解开 Onion Packet 的一层,就像是剥离一层洋葱皮一样,如果判断自己是接收者则处理 payment settle 相关的逻辑,如果发现后续还有接收者则处理了 TLC 相关的逻辑之后继续往下一个 hop 传递剩下的 onion packet。
- 如果某个 Hop 在转发 TLC 的过程中出现错误就生成一个新的包含有错误信息的 onion packet,并向前一个 hop 发送 onion packet。
- payment 的发起者收到 TLC fail event 的时候,从其中解开包含有错误信息的 onion packet,然后根据错误信息去判断是否需要重试和更新 Network graph。
Watchtower
Watchtower 是闪电网络中的重要安全机制,主要用于帮助离线用户防止资金被盗。它通过实时监测链上交易,并在发现违规行为时执行惩罚交易,从而维护闪电网络的公平性和安全性。
Fiber 的 watchtower 实现在 WatchtowerActor里,这个 actor 会监听 Fiber 节点中发生的关键事件,比如一个新的 Channel 创建成功时将会收到 RemoteTxComplete
,watchtower 就在数据库里插入一条对应的记录来开始监听这个通道,Channel 双方协商成功关闭时会收到 ChannelClosed
,watchtower 从数据库中移除对应的记录。
在 Channel 中 TLC 交互时候,watchertower 将会收到 RemoteCommitmentSigned
和 RevokeAndAckReceived
,分别去更新数据库中存储的 revocation_data
和 settlement_data
,这些字段将会在后续创建 revocation transaction 和 settlement trasaction 的时候用到。
Watchtower 的惩罚机制是通过比较 commitment_number
来判断 CKB 的链上交易是否使用了老的 commitment transaction,如果发现违规则构建一个 revocation transaction 提交到链上进行惩罚,否则就构建发送一个 settlement transaction 提交到链上。
其他技术决策
- 存储:我们使用 RocksDB 作为存储层,写代码的过程中可以直接使用 serde 来序列化。但因为 scheme-less,所以不同版本的数据迁移仍然是一个挑战,我们通过这个独立程序来解决,比较粗暴,但目前没想到更好的办法。
- 序列化:节点间的消息使用 Molecule 进行序列化和反序列化,带来效率、兼容性和安全性优势。要确保确定性,这样相同的消息在所有节点上序列化方式相同,这对于签名生成和验证非常重要。
后续展望
目前 Fiber 还处于前期活跃开发阶段,后续我们可能将继续做以下几个方面的改进:
- 修复还未处理好的 corner case,增强项目整体的健壮性
- 目前的 cross hub 还处于 Demo 阶段,我们会对这部分增加如 payment session 等功能
- 完善支付路由规划算法,可能会引入其他路径搜索策略,以适应用户不同的路由偏好和需求
- 扩展合约的功能,比如引入基于版本号的撤销机制和更安全的 Point Time-Locked Contracts
Let’s scale P2P finance together! 🩵
