您正在查看: Surou 发布的文章

深度剖析区块链跨链技术Cosmos(上篇)

区块链的公链项目越来越多,每个项目都是一个孤立的网络体系,因此区块链项目之间的互联互通能力也是技术发展的大方向。这其中Cosmos项目是其中的优秀代表。本文作者从Cosmos核心技术Tendermint开始,深度剖析Cosmos项目,让大家对跨链技术将一个比较深入的了解。

导读

2017年以来,区块链项目出现了井喷式地上升,然而在喜人增长态势的背后,人们也注意到大部分项目都缺乏与其它区块链项目互连互通的能力而成为孤立的网络体系。因此,跨链技术也逐渐进入了大家的视野,本文将为大家解读一个跨链项目Cosmos,看看它是如何做到让不同区块链的价值网络实现互通的。通过对该项目的解读,让大家对跨链项目有个初步的了解。同时受限于篇幅,我们会分为上下两篇,上篇带大家先了解Cosmos的核心技术Tendermint,下篇会着重介绍Cosmos。

Tendermint概述

提到区块链,大家想必已然不陌生了,不过更多人想到的可能会是众所周知的Bitcoin和Ethereum。的确,两者分别是区块链技术的起源和发展的代表,也是大家广泛传播和深入研究的对象。但是随着Bitcoin的不断推进,比特币工作量证明共识机制在速度和扩展性上的不足也逐步展现出来。

Cosmos的开发团队Tendermint其实早在2014年就开始意识到了其不足,并持续专注于寻求不依赖挖矿等高电力消耗的共识机制,提供快速的交易处理能力,它们的目标是为全世界所有的区块链提供速度、安全和可扩展性。目前,Tendermint加入了微软Azure区块链即服务平台,也成为了以太坊区块链联盟成员之一,同时Tendermint也是跨链技术Cosmos的核心技术。两者大致的关系如下:

图中可以轻松看出Cosmos就是在Tendermint基础上添加一些插件功能来实现的,上篇暂不对Cosmos做过多阐述,先来认识一下Tendermint。

Tendermint是什么

Tendermint的详细定义可以参考官方文档:
https://tendermint.readthedocs.io/en/master/introduction.html#what-is-tendermint

这里总结下有以下几点:

  1. Tendermint是一个能够在不同机器上,安全一致复制应用的软件,其中安全性和一致性也是分布式账本的关键概念。
  2. Tendermint具备拜占庭容错能力,是一种拜占庭容错共识算法。
  3. Tendermint主要有两部分组成:
    • Tendermint Core:区块链共识引擎,负责节点之间数据传输以及拜占庭共识。
    • ABCI:区块链应用程序接口,也是一个协议,支持任何语言的交易处理实现。

总体来讲,Tendermint可以理解为一个模块化的区块链软件框架,支持开发者个性化定制自己的区块链,而又不需要考虑共识以及网络传输的实现。

Tendermint设计原则

先简单说说区块链的概念,区块链是一个具备确定性的状态机,可以在不信任的节点之间进行状态复制,包括应用的状态和改变状态的交易。从架构的层面上,区块链可以简单分为三个概念层:

  • 网络层(Networking):负责交易和数据传输和同步。
  • 共识算法(Consensus):负责不同的验证节点处理完交易后,保证状态的一致,也就是将交易打包到区块中。
  • 应用程序(Application):交易的真正执行者。

大致框架如下:

目前大部分的区块链实现都是采用上面的框架,实现成单一的程序,但是这就很容易出现两个问题:

  1. 代码复用困难,代码库的分支管理变得复杂。
  2. 限制了应用开发的语言

如何去规避这两个问题呢?Tendermint设计了自己的一套框架,其设计原则是易使用,易理解,高性能,适用于各种分布式应用。它的创新之处在于,将区块链应用(状态)与底层共识进行了分离,将共识引擎和P2P网络层封装组成Tendermint Core。同时提供ABCI接口与应用层进行交互,应用逻辑可以用任何语言编写,应用做的事情实际上就是状态机控制。基于这种架构,应用的开发者可以方便地实现自己的区块链。

Tendermint的框架总体来讲分为ABCI Application以及Tendermint Core两部分,两者通过ABCI连接。下面会对这两部分依次展开介绍

Tendermint核心模块

ABCI Application

开发者定制开发的区块链应用,开发语言不受限制,可以使用任何语言进行开发,但是必须实现为一个ABCI Server,即需要满足以下几点:

  • 是一个Socket Server,需支持TSP或GRPC两种方式之一。
  • 能够处理ABCI Message。所有的ABCI消息类型都是通过protobuf来定义的,具体的消息格式可参考https://github.com/tendermint/abci/blob/master/types/types.proto
  • 实现区块链应用接口(ABCI)。ABCI是Tendermint中定义的一套Application与Tendermint Core之间交互的协议。详细定义如下(版本:0.10.3):

ABCI接口可以分为三类:信息查询、交易校验以及共识相关处理。而Tendermint Core作为ABCI Client在启动时,会与ABCI Server建立三个连接,分别用于这三类接口消息的处理。
在Tendermint Core与Application交互的所有消息类型中,有3种主要的消息类型:

  1. CheckTx消息用于验证交易。Tendermint Core中的mempool通过此消息校验交易的合法性,通过之后才会将交易广播给其它节点。
  2. DeliverTx消息是应用的主要工作流程,通过此消息真正执行交易,包括验证交易、更新应用程序的状态。
  3. Commit消息通知应用程序计算当前的世界状态,并存在下一区块头中。

Tendermint Core

Tendermint共识引擎,包含区块链需要大部分功能实现,主要有:

  • 共识算法:拜占庭POS算法。
  • P2P:采用gossip算法,默认端口是46656。
  • RPC:区块链对外接口,默认端口是46657。支持三种访问方式:URI over HTTP、JSONRPC over HTTP、JSONRPC over websockets。详细的RPC接口定义列表可以参考https://tendermint.github.io/slate
  • 其它:交易缓存池、消息队列等。

共识算法

Tendermint是一个易于理解的BFT共识协议。协议遵循一个简单的状态机,如下:

协议中有两个角色:

  • 验证人:协议中的角色或者节点,不同的验证者在投票过程中具备不同的权力(vote power)。
  • 提议人:由验证人轮流产生。

验证人轮流对交易的区块提议并对提议的区块投票。区块被提交到链上,且每个区块就是一个区块高度。但区块也有可能提交失败,这种情况下协议将选择下一个验证人在相同高度上提议一个新块,重新开始投票。

从图中可以看到,成功提交一个区块,必须经过两阶段的投票,称为pre-vote和pre-commit。当超过 2/3 的验证人在同一轮提议中对同一个块进行了pre-commit投票,那么这个区块才会被提交。

由于离线或者网络延迟等原因,可能造成提议人提议区块失败。这种情况在Tendermint中也是允许的,因此验证人会在进入下一轮提议之前等待一定时间,用于接收提议人提议的区块。

假设少于三分之一的验证人是拜占庭节点,Tendermint能够保证验证人永远不会在同一高度重复提交区块而造成冲突。为了做到这一点,Tendermint 引入了锁定机制,一旦验证人预投票了一个区块,那么该验证人就会被锁定在这个区块。然后:

  • 该验证人必须在预提交的区块进行预投票。
  • 当前一轮预提议和预投票没成功提交区块时,该验证人就会被解锁,然后进行对新块的下一轮预提交。

可以看到,Tendermint共识算法和PBFT时非常相似的,可以说是PBFT的变种,那我们来比较一下:

相同点:
  • 同属BFT体系。
  • 抗1/3拜占庭节点攻击。
  • 三阶段提交,第一阶段广播交易(区块),后两阶段广播签名(确认)。
  • 两者都需要达到法定人数才能提交块。
不同点:
  1. Tendermint与PBFT的区别主要是在超过1/3节点为拜占庭节点的情况下。当拜占庭节点数量在验证者数量的1/3和2/3之间时,PBFT算法无法提供保证,使得攻击者可以将任意结果返回给客户端。而Tendermint共识模型认为必须超过2/3数量的precommit确认才能提交块。举个例子,如果1/2的验证者是拜占庭节点,Tendermint中这些拜占庭节点能够阻止区块的提交,但他们自己也无法提交恶意块。而在PBFT中拜占庭节点却是可以提交块给客户端。
  2. 另一个不同点在于拜占庭节点概念不同,PBFT指的是节点数,而Tendermint代表的是节点的权益数,也就是投票权力。
  3. 最后一点,PBFT需要预设一组固定的验证人,而Tendermint是通过要求超过2/3法定人数的验证人员批准会员变更,从而支持验证人的动态变化。

P2P网络

Tendermint的P2P网络协议借鉴了比特币的对等发现协议,更准确地说,Tendermint是采用了BTCD的P2P地址簿(Address Book)机制。当连接建立后,新节点将自身的Address信息(包含IP、Port、ID等)发送给相邻节点,相邻节点接收到信息后加入到自己的地址薄,再将此条Address信息,转播给它的相邻节点。

此外为了保证节点之间数据传输的安全性,Tendermint采用了基于Station-to-Station协议的认证加密方案,此协议是一种密钥协商方案,基于经典的DH算法,并提供相互密钥和实体认证。大致的流程如下:

  • 每一个节点都必须生成一对ED25519密钥对作为自己的ID。
  • 当两个节点建立起TCP连接时,两者都会生成一个临时的ED25519密钥对,并把临时公钥发给对方。
  • 两个节点分别将自己的私钥和对方的临时公钥相乘,得到共享密钥。这个共享密钥对称加密密钥。
  • 将两个临时公钥以一定规则进行排序,并将两个临时公钥拼接起来后使用Ripemd160进行哈希处理,后面填充4个0,这样可以得到一个24字节的随机数。
  • 得到的随机数作为加密种子,但为了保证相同的随机数不会被相同的私钥使用两次,我们将随机数最后一个bit置为1,这样就得到了两个随机数,同时约定排序更高的公钥使用反转过的随机数来加密自己的消息,而另外一个用于解密对方节点的消息。
  • 使用排序的临时公钥拼接起来,并进行SHA256哈希,得到一个挑战码。
  • 每个节点都使用自己的私钥对挑战码进行签名,并将自己的公钥和签名发给其它节点校验。
  • 校验通过之后,双方的认证就验证成功了。后续的通信就使用共享密钥和随机数进行加密,保护数据的安全。

应用示例

Tendermint官方项目里内置了ABCI Application的两个简单实现counter以及kvstore。这个两个Demo逻辑非常简单,运行起来也非常简单,以kvstore为例,只需要下面三条简单的指令就可以轻松的跑起来:

  • tendermint init
  • abci-cli kvstore
  • tendermint node

复杂一点,假设想使用Tendermint实现一套类似Ethereum的应用,最终应该是这样:

由Tendermint Core负责交易和区块的共享以及共识处理,开发者只需将go-ethereum和ABCI Server集成一个ABCI应用。Ethermint项目就是Tendermint团队开发的一个类似应用,大家可以参考,遗憾的是目前Ethermint目前只支持低版本的abci和go-ethereum。

Tendermint工作流

上图简单描述了Tenermint的工作流。大致为:

  • client通过RPC接口broadcast_tx_commit提交交易;
  • mempool调用ABCI接口CheckTx用于校验交易的有效性,比如交易序号、发送者余额等,同时订阅交易执行后的事件并等待监听。
  • 共识从mempool中获取交易开始共识排序,打包区块,确定之后依次调用ABCI相关接口更新当前的世界状态,并触发事件。
  • 最终将交易信息返回client。

总结

本文从概念、设计原则以及架构等方面对Tendermint做了详细的介绍,让大家可以直观了解到Tendermint的设计思想和实现原理。了解Tendermint之后,方便大家:

  • 可以快速开发出自己的区块链应用而无需关注共识和P2P网络。
  • 能够很好的兼容到Cosmos跨链生态中,因为Tendermint是Cosmos的核心技术。
  • 开发支持任何编程语言的区块链。

转载自:https://mp.weixin.qq.com/s/FwkYoxaOb-n4U4sH2iez6Q

transaction_trace 中的 account_ram_delta

transaction_trace具有新的可选字段account_ram_delta,如果存在,则记录支付用于存储延迟交易的特定账户的RAM使用增量:输入延迟交易将具有account_ram_delta记录该交易的第一授权者的RAM使用增加的字段。当延迟交易退出(无论哪个状态)该account_ram_delta字段将存在时记录延期交易的付款人的RAM使用减少。

参考

https://github.com/EOSIO/eos/issues/6897

EOSjs getTableRows lower and upper bound on account name

try {  
      let con_account = 'bcskillsurou';
      const encodedName = new BigNumber(Eos.modules.format.encodeName("bcskillsurou", false))
      console.log(encodedName);
      console.log(await rpc.get_table_rows(
      {
        code:con_account,
        scope:con_account,
        table:"accounttb",
        lower_bound: encodedName,
      }));

    } catch (e) {
      console.log( '\nCaught exception: ' + e);
      if (e instanceof eosjs_jsonrpc.RpcError)
        console.log( '\n\n' + JSON.stringify(e.json, null, 2));
    }

参考

https://eosio.stackexchange.com/questions/813/eosjs-gettablerows-lower-and-upper-bound-on-account-name
issue: https://github.com/EOSIO/eosjs/issues/456

语义化版本 2.0.0

语义化版本 2.0.0

摘要

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。

先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

简介

在软件管理的领域里存在着被称作“依赖地狱”的死亡之谷,系统规模越大,加入的包越多,你就越有可能在未来的某一天发现自己已深陷绝望之中。

在依赖高的系统中发布新版本包可能很快会成为噩梦。如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个依赖包改版才能完成某次升级)。而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量)。当你专案的进展因为版本依赖被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。

作为这个问题的解决方案之一,我提议用一组简单的规则及条件来约束版本号的配置和增长。这些规则是根据(但不局限于)已经被各种封闭、开放源码软件所广泛使用的惯例所设计。为了让这套理论运作,你必须先有定义好的公共 API 。这可以透过文件定义或代码强制要求来实现。无论如何,这套 API 的清楚明了是十分重要的。一旦你定义了公共 API,你就可以透过修改相应的版本号来向大家说明你的修改。考虑使用这样的版本号格式:X.Y.Z (主版本号.次版本号.修订号)修复问题但不影响API 时,递增修订号;API 保持向下兼容的新增及修改时,递增次版本号;进行不向下兼容的修改时,递增主版本号。

我称这套系统为“语义化的版本控制”,在这套约定下,版本号及其更新方式包含了相邻版本间的底层代码和修改内容的信息。

语义化版本控制规范(SemVer)

以下关键词 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT、 RECOMMENDED、MAY、OPTIONAL 依照 RFC 2119 的叙述解读。(译注:为了保持语句顺畅, 以下文件遇到的关键词将依照整句语义进行翻译,在此先不进行个别翻译。)

  1. 使用语义化版本控制的软件必须(MUST)定义公共 API。该 API 可以在代码中被定义或出现于严谨的文件内。无论何种形式都应该力求精确且完整。

  2. 标准的版本号必须(MUST)采用 X.Y.Z 的格式,其中 X、Y 和 Z 为非负的整数,且禁止(MUST NOT)在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素必须(MUST)以数值来递增。例如:1.9.1 -> 1.10.0 -> 1.11.0。

  3. 标记版本号的软件发行后,禁止(MUST NOT)改变该版本软件的内容。任何修改都必须(MUST)以新版本发行。

  4. 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。

  5. 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。

  6. 修订号 Z(x.y.Z | x > 0)必须(MUST)在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。

  7. 次版本号 Y(x.Y.z | x > 0)必须(MUST)在有向下兼容的新功能出现时递增。在任何公共 API 的功能被标记为弃用时也必须(MUST)递增。也可以(MAY)在内部程序有大量新功能或改进被加入时递增,其中可以(MAY)包括修订级别的改变。每当次版本号递增时,修订号必须(MUST)归零。

  8. 主版本号 X(X.y.z | X > 0)必须(MUST)在有任何不兼容的修改被加入公共 API 时递增。其中可以(MAY)包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号必须(MUST)归零。

  9. 先行版本号可以(MAY)被标注在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。数字型的标识符禁止(MUST NOT)在前方补零。先行版的优先级低于相关联的标准版本。被标上先行版本号则表示这个版本并非稳定而且可能无法满足预期的兼容性需求。范例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

  10. 版本编译元数据可以(MAY)被标注在修订版或先行版本号之后,先加上一个加号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。当判断版本的优先层级时,版本编译元数据可(SHOULD)被忽略。因此当两个版本只有在版本编译元数据有差别时,属于相同的优先层级。范例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。

  11. 版本的优先层级指的是不同版本在排序时如何比较。判断优先层级时,必须(MUST)把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译元数据不在这份比较的列表中)。由左到右依序比较每个标识符,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较,例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。例如:1.0.0-alpha < 1.0.0。有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级必须(MUST)透过由左到右的每个被句点分隔的标识符来比较,直到找到一个差异值后决定:只有数字的标识符以数值高低比较,有字母或连接号时则逐字以 ASCII 的排序来比较。数字的标识符比非数字的标识符优先层级低。若开头的标识符都相同时,栏位比较多的先行版本号优先层级比较高。范例:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0。

为什么要使用语义化的版本控制?

这并不是一个新的或者革命性的想法。实际上,你可能已经在做一些近似的事情了。问题在于只是“近似”还不够。如果没有某个正式的规范可循,版本号对于依赖的管理并无实质意义。将上述的想法命名并给予清楚的定义,让你对软件使用者传达意向变得容易。一旦这些意向变得清楚,弹性(但又不会太弹性)的依赖规范就能达成。

举个简单的例子就可以展示语义化的版本控制如何让依赖地狱成为过去。假设有个名为“救火车”的函式库,它需要另一个名为“梯子”并已经有使用语义化版本控制的包。当救火车创建时,梯子的版本号为 3.1.0。因为救火车使用了一些版本 3.1.0 所新增的功能, 你可以放心地指定依赖于梯子的版本号大等于 3.1.0 但小于 4.0.0。这样,当梯子版本 3.1.1 和 3.2.0 发布时,你可以将直接它们纳入你的包管理系统,因为它们能与原有依赖的软件兼容。

作为一位负责任的开发者,你理当确保每次包升级的运作与版本号的表述一致。现实世界是复杂的,我们除了提高警觉外能做的不多。你所能做的就是让语义化的版本控制为你提供一个健全的方式来发行以及升级包,而无需推出新的依赖包,节省你的时间及烦恼。

如果你对此认同,希望立即开始使用语义化版本控制,你只需声明你的函式库正在使用它并遵循这些规则就可以了。请在你的 README 文件中保留此页连结,让别人也知道这些规则并从中受益。

FAQ

在 0.y.z 初始开发阶段,我该如何进行版本控制?

最简单的做法是以 0.1.0 作为你的初始化开发版本,并在后续的每次发行时递增次版本号。

如何判断发布 1.0.0 版本的时机?

当你的软件被用于正式环境,它应该已经达到了 1.0.0 版。如果你已经有个稳定的 API 被使用者依赖,也会是 1.0.0 版。如果你很担心向下兼容的问题,也应该算是 1.0.0 版了。

这不会阻碍快速开发和迭代吗?

主版本号为零的时候就是为了做快速开发。如果你每天都在改变 API,那么你应该仍在主版本号为零的阶段(0.y.z),或是正在下个主版本的独立开发分支中。

对于公共 API,若即使是最小但不向下兼容的改变都需要产生新的主版本号,岂不是很快就达到 42.0.0 版?

这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多依赖代码的软件中。升级所付出的代价可能是巨大的。要递增主版本号来发行不兼容的改版,意味着你必须为这些改变所带来的影响深思熟虑,并且评估所涉及的成本及效益比。

为整个公共 API 写文件太费事了!

为供他人使用的软件编写适当的文件,是你作为一名专业开发者应尽的职责。保持专案高效一个非常重要的部份是掌控软件的复杂度,如果没有人知道如何使用你的软件或不知道哪些函数的调用是可靠的,要掌控复杂度会是困难的。长远来看,使用语义化版本控制以及对于公共 API 有良好规范的坚持,可以让每个人及每件事都运行顺畅。

万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?

一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文件中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。

如果我更新了自己的依赖但没有改变公共 API 该怎么办?

由于没有影响到公共 API,这可以被认定是兼容的。若某个软件和你的包有共同依赖,则它会有自己的依赖规范,作者也会告知可能的冲突。要判断改版是属于修订等级或是次版等级,是依据你更新的依赖关系是为了修复问题或是加入新功能。对于后者,我经常会预期伴随着更多的代码,这显然会是一个次版本号级别的递增。

如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)

自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住, 语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。

我该如何处理即将弃用的功能?

弃用现存的功能是软件开发中的家常便饭,也通常是向前发展所必须的。当你弃用部份公共 API 时,你应该做两件事:(1)更新你的文件让使用者知道这个改变,(2)在适当的时机将弃用的功能透过新的次版本号发布。在新的主版本完全移除弃用功能前,至少要有一个次版本包含这个弃用信息,这样使用者才能平顺地转移到新版 API。

语义化版本对于版本的字串长度是否有限制呢?

没有,请自行做适当的判断。举例来说,长到 255 个字元的版本已过度夸张。再者,特定的系统对于字串长度可能会有他们自己的限制。

关于

语义化版本控制的规范是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立。

如果您有任何建议,请到 GitHub 上提出您的问题

许可证

知识共享 署名 3.0 (CC BY 3.0)

转载自:https://semver.org/lang/zh-CN/