Cosmos被誉为“区块链的互联网”,旨在解决区块链可互操作性和可扩展性问题。其区块链间通讯协议可以实现区块链的互联,支持不同区块链之间的资产转移。随着区块链协同操作的需求越发强烈, Cosmos作为跨链技术的佼佼者,值得我们关注和学习。
以下是《Cosmos互联链通信技术规范》译文的前半部分。
摘要
本文给出了Cosmos IBC(互联链通信)协议的技术规范,这个协议在2016年6月Cosmos白皮书中有过首次描述[1]。其它一些技术也可以在一个原子操作中涵盖两个链,比如“哈希时间锁定合同”[2],不过很多(此类技术)都仅限于保证两个交易同时成功或失败。IBC则创建了完整的双向“侧链”,真正地允许跨链传递价值,并充分利用Tendermint的即时最终性来实现代币的快速传递。
IBC使用消息传递范式,并允许参与链保持独立。每个链都维护一个局部的部分顺序,而消息则用于跟踪所有跨链的因果关系。一旦两个链之间注册了信任关系,就可以安全地将数据包从一个链发送到另一个链上,代表从一个链上的一个账户转移代币到另一个链上的账户。该协议也可扩展到代币转移之外(的其它功能) – 尽管为其他应用类型设计安全的通信逻辑还待深入研究。该协议对链之间传输数据包时的阻塞时间或网络延迟不做假设,因此在异构环境中具有很强的鲁棒性。
本文阐述了Cosmos IBC协议的要求和结构,目标是提供足够的细节来充分理解和分析协议的安全性。
一 、简介
Cosmos IBC是围绕Cosmos网络和Tendermint共识引擎而设计的,从根本上依赖于Tendermint的即时最终性属性(意味着永远不会创建分叉)。Cosmos网络将由Cosmos枢纽和多个独立的分区组成,这些分区都通过IBC与枢纽进行通信。Cosmos枢纽还可以通过在它们之间中继IBC消息来桥接不同的分区。
我们从一个简单情形的具体例子入手,它可用于为后续的抽象解释提供一个形式框架。在此之后,将解释这个例子中使用的消息传递语义。接下来就是安全证明和数据包传输所依赖的技术基础。
在此之后,将讨论更高级的消息类型,例如超时和路由(以使更大的网络在相当长时间内能高效运作),还要对协议处理所有可能情况的能力予以充分考虑。
除了为IBC协议提供理论理解和参考文档之外,本文还旨在为协议的安全基础提出令人信服的论据。本文是对正在进行的Cosmos项目的一个报告,随着来自实践的数据不断更新理论,我们也将持续调整它的内容。
二、 示例场景
为了使讨论不那么抽象,这里用例子说明一个具体情形是如何工作的。Alice想从她在Cosmos分区X上的账户发送30个wink币(示例代币名称)给Cosmos枢纽上的Bob。假设分区X已经跟枢纽建立了一个IBC连接,Alice将在分区X上创建一个交易来发起传输。分区X会将那些代币冻结在某个托管账户,然后创建一个IBC数据包请求枢纽在Bob的账户中创建相应数量的代币。本质上,分区X上的验证节点集合会保证销毁这些代币以换取在枢纽上重铸它们。
一个独立的中继进程(任何人都可运行的客户端软件)可以从分区X提取那个IBC数据包的证明并将其发布到枢纽。枢纽将验证区块头,Merkle证明和顺序号来确保这是一个从分区X来的有效的IBC数据包。这下我们面临两个选择:要么分区X在枢纽上有足够信用来铸这30个wink币从而数据包被接受,要么信用不够而数据包被拒绝。如果数据包被接受,枢纽就在Bob的账户中铸30个新鲜的wink币,并将数据包连同成功记录存在它的传入队列中;如果数据包被拒绝,则不会创建代币,且队列中会存入一个失败记录。
我说的“信用”指的是什么?我们不希望每个连接到枢纽的链都能在枢纽上随意创建任意数量的任意代币,否则所有经济保证很快就变得毫无意义。枢纽必须使用它自己的逻辑来验证它是否足够信任分区X并接受那30个wink币。如果分区X是wink币的本源(X的原生代币),那么在发送wink币到枢纽这件事上,它将得到枢纽的极大信任。如果枢纽之前已经向分区X发送过500个wink币,那么它必须记住这个信息并允许分区X将(不超过)那500个wink币发送回枢纽,从而实现代币的自由流动。这个逻辑可以为代币传输提供安全性,其它应用则需要在不完全信任网络中所有分区的情况下,运用它们自己的逻辑来维护全局约束条件。
在枢纽成功或不成功地执行了那个交易之后,我们希望将交易的收据(连同证明)传回到分区X以完成这个循环。此收据只是另一种类型的IBC数据包,其执行方式与发送一样。唯一区别是,在发起分区处理收据绝不能产生错误。(根据收据内容)如果交易是成功的,则分区X将销毁托管的30个wink币,代币在分区间转移成功;如果交易因任何原因被拒绝,那么托管代币就会释放回Alice的账户 – 就像什么都没有发生过一样。
这意味着,在收到响应(成功或失败)之前,谁也不能碰分区X上的托管代币。除非我们有证据证明该数据包被接收链拒绝,已经发出的代币不会被释放。没有代币会被无端创建或销毁,也不可能执行双花,代币不会莫名消失在跨链操作中。发送方只需要等待两个数据包的转发和执行(很多情况下是十来秒钟)。对处理硬分叉分区(如ETH/ETC或BTC/BCH)的考虑会在下面的高级章节讨论。
三、 消息传递语义
在试图扩展区块链时,必须找到一种不违反任何安全保证的可行方法,来增加并行写的数量。防止双花需要为任何给定账户提供严格的串行访问(读和写),但是我们需要一些安全的方法来允许多个交易同时执行,而又不给恶意利用竞态条件提供可能性。
通过IBC协议,我们寻求避免一个问题,那就是允许多个独立节点将交易应用到相同的状态空间。如果你不想丢失任何数据(比如:强最终一致性),这个问题即便不考虑拜占庭角色也是困难的,在面对恶意行动人寻求利用任何不一致为自身牟利的情况下,则变得极其困难。安全地将不同的部分排序协调成一致的全局排序的最严谨方法是CRDTs(无冲突数据复制类型),它保证一组固定交易的所有可选的部分排序都将收敛到相同结果。CRDTs确实很有趣,但由于其固有特性,不允许在区块链用例中强制执行约束条件(例如,账户余额永远不能为负数)。
这个问题的另一个解决方案是使用分片,每个分片都只能访问状态空间的一部分。这可以增加吞吐量,但也会使任何涉及多个分片的交易极难正确执行。触及多个分片并需要一致视图的查询可以通过使用快照来执行,但是在高度分布的环境中这可能很困难。如果您想在两个分片上安全地、原子地查询和修改数据,就需要类似锁和三阶段提交的机制,这在许多数据库中都使用过。但是,如果您想要保证顺序并在通信层引入同步和定时假设,这就是一个阻塞操作,这使得它不适合分布式(多)区块链的场景。
我们通过定义“分区”而走了一条不同的路。每个分区都是一个独立的区块链,有自己的应用逻辑,自己的交易和数据存储。分区应该沿使用界线分开,因此绝大多数交易只影响一个分区,但我们可以保证任何跨链交易拥有安全和非阻塞的语义。每个分区都是一个完整的独立系统,它们使用消息传递进行通信。它们能够保证本地分区中的交易正确排序和执行,同时允许其它分区里的交易并行执行。唯一需要全局排序的东西是系统之间发送的消息。
分布式系统中的消息传递是一个已经被深入研究的领域,也是许多其它系统的搭建基础。我们能够对异步消息进行建模,且对信道不做时序假设。这样的结果是,我们允许每个分区以自己的速度行动,不被任何其它分区阻塞,却能够以当时网络允许的最快速度通信。
使用消息传递作为原语的另一个好处是,接收方能够对传入的消息应用自己的安全检查。仅仅因为一个我们了解的分区发送了一条消息给某个特定帐户添加50个以太币,并不意味着我们必须增加余额。我们可以在接收消息时添加我们自己的业务逻辑,以决定我们是否想拒绝该消息,以及我们想如何处理它(如果我们接受它)。在一个共享状态的场景中,很难甚至不可能做到这一点。消息传递允许每个分区确保其安全性和自治性,同时允许不同的系统作为一个整体协同工作。这可以看作是微服务体系架构的一个类比,但是跨越了组织边界。
为了在一个可证明的异步消息传递原语上构建有用的算法,我们需要定义一些更高阶的结构在所有系统间共享,从而允许我们和一些更容易的保证打交道。
3.1 可靠消息队列
我们在这里引入的第一个原语是可靠的消息队列(以下简称队列),这是异步消息传递的典型构造,它使我们能够确保因果排序并避免阻塞。
这个队列在每个区块链的Merkle数据存储中以多个键值对的形式永续保存。每个队列有一个独特的前缀,键是通过把一个代表其顺序号的8字节大端模式整数追加在这个前缀后面生成的。注意这个编码方式为我们提供了一个与键的顺序号相一致的键的字典序,这让我们能快速找到最新的数据包,也能证明在某个给定顺序位的数据包内容(或没有数据包) -- 假设像在我们的IAVL+ Merkle树[3]里那样访问范围证明。
新增的任何一个数据包的顺序号只能比当前最高数据包的顺序号大1。为了使证明更容易,我们会将顺序号也存在数据包本身(键值对的值)里面。一旦一个数据包被写入,它就是不可变的(除了在清理期间删除,这在后面会解释)。这使得接收分区可以接受位于源分区高度H的数据包Z的证明,而接收分区处理交易时尽可放心数据包在源分区里仍然以相同状态存在(异步性的要求)。区块链应用逻辑必须为所有队列保证这些约束条件,以确保消息层的正常运行。
假设我们的IBC队列的前缀是0xCAFE,我们可以看到多个数据包是如何存在Merkle树中的,而它们的顺序号又是如何追加(生成键)的,这样我们就可以轻松地为某个给定数据包的存在(或不存在)提供一个Merkle证明。下面图示了构建顺序号为2的数据包的Merkle证明的路径,用橙色高亮显示。
本节的其余部分将定义可通过此队列发送的基本消息类型。
3.2 注册链(需经许可)
在这个链之间建立起连接之前,我们需要先让它们彼此注册。这一点尤为重要,因为所有轻客户端证明都需要一个对验证消息头至关重要的信任种子,当有未经共识算法批准的恶意分支存在时,它能让我们确认那条真的链。
为了在A和B之间形成连接,我们必须在B上注册A,也要在A上注册B。这些过程是对称的,所以在这里我们只描述如何在A上注册B。在注册时,A为B添加一个可信的消息头和验证节点集合,保存在它的安全数据存储中。这用于验证来自B的所有未来的消息头,以及验证节点集合的变更。A还要创建两个具有不同名称(前缀)的队列。
ibc::out – 存放所有以B为目的地的传出数据包
ibc::in – 存放所有来自B的传入数据包连同它们的执行结果
注意,注册链应该是一个需要获得许可的操作,且执行时要有人工验证。这将产生一个信任声明,即这个消息头和验证节点集合代表了对应链的正确状态,它们不属于试图镜像这条链的某些影子链。不像PoW的最长链胜出算法,对权益证明而言信任必须在某个时点被明确授予。
而且,注册过程还必须定义好我们用来验证Merkle证明的算法(这些证明存在于队列中的IBC数据包里)。缺省设置是来自我们的IAVL树的Merkle证明格式,但是其他Merkle化的数据存储,比如Patricia Trie,也可以得到支持。这个算法必须在注册时设置一次,并且必须被参与链所支持。队列的整个生命周期都会基于可信的消息头,一致地使用该算法验证每个数据包或收据。
3.3 验证节点变更
任何生产链的验证节点集合都会随着时间推移而发展变化。当我们注册一个新链时,会针对一个特定的验证节点集合做一个信任声明 (更具体而言,我们的信任绑定的是这样一个条件:消息头拥有超过一定阈值数量的与给定公钥集相匹配的数字签名)。当我们信任的这组公钥变化时,我们需要一个消息来把这个变更通知给其它链。由于验证节点变更对共识算法至关重要,所以我们用一个标准格式把它存储在Tendermint的区块头内。
我们定义一个特殊的更新数据包,它包含一个Tendermint区块头和新的验证节点集合,我们可以将它发送给接收链。接收链可以核实验证节点集合变更的有效性,并使用它来验证所有未来的数据包。
3.4 发送数据包
发送一个IBC数据包涉及区块链应用逻辑调用IBC模块,告诉它需要发送的数据包以及目标链的标识。如果目标链已经注册好,IBC模块只需计算下一个顺序号并将其添加到传出队列out queue。顺序号与特定连接相关并由发送链生成,它们必须是单调递增和连续的。
数据包被写入Merkle化的数据存储,这样它就嵌入了和区块头相关联的证明当中。这些代表了数据包的键值对证明接下来就会被传送给其它链。
区块链应用程序必须对谁可以写一个队列的键空间做出限制,不是随便哪个智能合约都可以在那里写交易数据,只有当区块链逻辑判定数据包有效,且与数据包类型相关的全局约束条件得到满足(比如:上面例子中的托管代币被冻结),才会生成(并写入)数据包。
3.5 中继数据包
为了让数据包到达目标链,我们依靠一个或多个中继进程把链A上的传出证明out proof发布到链B的传入队列in queue中。由于这里只需要轻客户端证明,中继进程不需要是验证节点,甚至都不需要是全节点。实际上,如果每个发起创建IBC数据包的用户也负责将它中继到接收链,那就太理想了。唯一的限制是,中继进程必须能够在目标链上支付适当的费用。
为了系统自举,链开发人员可以提供一个有足够资金的特殊账号给中继进程,为所有数据包支付费用,直到用户在两个链上都有代币。不过,使用IBC的人应该自己负责支付这些费用。注意,更新消息头是一种代价高昂的交易(大约100个数字签名校验),而为一个已知消息头发布证明则要便宜得多(大约20个哈希计算)。因此,为了优化,许多数据包可以捆绑在一起在同一区块高度发送,即使它们是在不同的区块高度创建的,这样可以通过增加一点延迟来节省计算成本。
系统必须允许多个中继进程安全地并行运行,拒绝掉任何重复的消息发布。但更理想的是能在起初就防止他们尝试重复发布,因为这会浪费带宽和链上的计算时间。
3.6 收据
在一个IBC数据包发布到另一个链上并被认为是有效的(即,数据包的证明跟源链的已知消息头相匹配),则不管执行是否成功,我们都必须将它存储在传入队列in queue里,这样就有证据表明它已经被处理过。相关交易应该发送到合适的智能合约加以验证和执行,然后返回标准的ABCI结果(成功或错误);收到的数据包连同处理结果被写进传入队列。
另一个中继进程可以获得这个数据包已经在链B上处理过的证明,把它作为收据发布到链A。此收据将由链A处理,触发进一步的应用逻辑,以及对队列的清理(参见高级部分)。将请求从A中继到B的那个进程可以将响应从B中继回A,但也可以使用一个不同的进程来做这件事。
本文首发于万云Wancloud微信号,未经授权不允许转载。