作者简介
朱 崇 文
恺英网络区块链组技术经理
目
录
Go 版本以太坊
为何选择DPOS机制
拓展共识改造实践
智能合约的实践
压力测试下暴露的问题
1. Go 版本以太坊
1.1 以太坊的客户端
?? 首先是以太坊技术社区的一些客户端的实现。以太坊技术协议本身是协议,它包含了一些接口协议,规范或参数定义和内部具体的实现逻辑和流程等。基于这个技术协议,可以用各自语言实现一个以太坊节点。以太坊的官方团队使用Go 语言实现了官方版本。partiy 的实现是第二大的客户端。
1.2 以太坊的工具组
?核心组件包括 Solidity,Web3.js。对于大多数的开发者来说,最关心的是这两个组件。Swarm 一种外部存储的实现,希望拓展以太坊本身对于较大数据存放的问题,另外 IPFS 也是类似的功能实现。
1.3 以太坊公联网络拓扑
?
图3
图3 是以太坊网络的拓扑图。包含了各种不同的客户端,他们之间相互组成了图中的网络。通过图3 就可以看到整个以太坊的公有链是非常开放的。
2. 为何选择DPOS机制
2.1 共识机制对比
DPOS 是一种共识机制,源自于石墨烯技术。简单对比一下以太坊的官方社区提供的 POW 机制。POW 是消耗你的计算力来产块,它出块速度慢,确认慢。而 DPOS 是代理人的模式。现阶段实现的 DPOS 的机制 TPS 可达 1000,平均确认的时间在 1~3 秒钟。
2.2 DPOS机制的优势
我们实现了的 DPOS 机制的优势主要在于两个方面。第一是系统可靠性。在商业场景下,网络性能可控,在异常情况下能快速处理并恢复,同时对TPS/QPS,以及确认时间有一定的要求。第二,共识机制是一种思想,以公有链为基础,可对外开放,任何人都可以参与,设立理事会和见证人角色,理事会管理区块链网络,见证人生产并验证区块,构成了一个良性的生态圈。
2.3 DPOS机制的理念
图4
图4表示,我们把一个公司和社区定义为三种角色,普通社区成员会通过自己手上的投票权选举理事会。理事会通过自己的判断或认知来委任见证人。见证人根据自己实际情况维持整个链的运作,这是 DPOS 机制的理念。
3. 拓展共识改造实践
3.1 共识框架引擎
3.1.1 改造共识层逻辑
图5
图5显示,如何实现 DPOS 的机制。以太坊的接口服务是提供客户端调用,网络通讯是为 p2p 提供服务。 中间是一个共识层,是去做挖矿还是去做 DPOS 等其他共识,在这个上面都可以做一个扩展。最下面就是存储,区块的存储和状态都是通过这层做的。
3.1.2 官方实现引擎 :Ethash/Clique
?图6
图6 显示 ,我们通过共识层来达到DPOS目的 。图上代码直接截取了以太坊的代码,这是一个接口引擎,提供了一些方法,看到这些方法名字,很多都是在 Verify。Verify 对整个以太坊非常重要,区块链是不可篡改的,它需要对各种各样的异常情况做验证。 最上面的文字写到官方上有两个引擎,第一个是在公有链上所使用的引擎,是 POW 机制的实现。第二个是 Clique,修改为 Dpos 很大的灵感来自于 Clique 引擎。
3.1.3 Seal 核心方法调用
?
图7
图7有点复杂,它能帮助我们了解以太坊内部的逻辑,首先通过 agent 和 worker 两个方法,去监听他们自己的对象,这两个 channel 一旦有数据会触发下面的事情。我们先看 worker channel,这个 channel 里面有新的 work出来会发生不同的事情。worker 是有工作的命令,一旦收到这个 worker 的 channel,就要判断自己有没有能力挖矿,是否要做打包工作,触发这个 channel 之后,就调用引擎里面的方法,然后 seal 就去解决问题。最终的题目是谁解出来,谁就有权利或者资格去挖这个区块,这个 seal 方法就成功了,接着会产生一个Result 的对象,会触发下面的工作,一旦产生出块之后就会有新的 worker 放在新的 channel 上去,如果我一直可以产块,就可以不断地做这个流程,这样就构成了以太坊矿工节点里面核心挖矿的流程。此外 work channel 也有一个自己的触发机制。这个时候就需要依赖外部的节点了,当其他节点产生区块的时候,会向我们这个节点广播,我就会同步出块,通知这个 channel 有新的工作区去做。刚刚讲的是接口引擎里面最重要的 Seal 方法,我们可以通过修改这个方法,把原来需要解决题目的挖矿改成不解决题目。
3.2 借鉴 Clique (POA) 的实现
?
图8
另一种共识机制是 Clique。以太坊公链使用 POW 机制,通过节点数量来防止别人恶意攻击。同时,以太坊提供一些测试链。但测试链用 POW 发布之后有一个问题,测试链没有很多人同时参与维护,如果有人恶意搞它就非常简单,通过一些机器和算力很容易把它搞瘫痪掉。为了能够让测试链比较稳定的运行,开发了另外一种共识机制取代了 POW,只有授权的机器才可以生产区块,这样维护成本非常低,找一些机器授权给他就能够去维持这个网络。整个网络通过授权过的人去出块,节点之间通过投票的方式来授权或者剔除授权,这些额外的投票机制记录在区块头的 extra data 字段里面。
图8右边的图是测试链的产块逻辑。假定有 A、B、C 三个节点的话,正常逻辑 A 完成之后到 B,B 完成之后到 C。图中有一个竞争出块的概念,出 C 的时候不仅仅是 C 产块,A 也可以产块,为什么呢? 因为避免 C 节点有问题的时候,不能产块,A 节点的块也能提供出来加入到链里面。同时,如果 C 在当前轮次里面,字段有一个 difficult 字段值就是 2,而这个 A 虽然可以产块,但不在这个轮次里面,所以 difficult 就是 1,尽可能保证 C 这个节点产生的块被大多数的节点所接受,这是以太坊最常见的概念。
图9
图9 是轮次问题,也做了小的改动,这两个节点可以竞争出块,对于它,我们希望 C 节点的块尽量被接受,所以对 A 节点来说,如果不是这个轮次里面的话,可以让A节点出块的时间变得稍微延后一些。比如在现在这个时间点应该可以出块的,但我要控制让它晚一些时候出块,这样就避免会产生过多的分叉,虽然可以通过 difficult 值来决定谁是最长链,但过多的分叉会导致链的状态越不稳定。
3.3 扩展区块头结构
3.3.1 增加见证人列表
?图10
我们借鉴 Clique 的想法之后,会想怎么扩展我们的 DPOS 机制。首先我们会先修改数据结构,对区块的 Header 的结构,定义新的字段,下面的 WitnessVotes 是我们自己定义的字段,我们希望通过这个字段来为 DPOS 机制提供节点授权,我们扩展这个字段,把见证人的地址列出,按照字典做排序,这样能避免出块轮次发生变化的问题。以太坊对于字段扩展是非常容易做的,在代码的框架里体统了框架的主体架构,我们只需要增加一些代码,就可以很容易扩容它的 Header 了。
3.3.2 见证人列表生成规则?? 图11
图11是 witness 的列表。对以太坊来的验证机制而言,两个区块,即便区块数据一样,但是区块的成产者不一样,也是不被接受的。为了达到这个目的,我们就需要约定好方案,对于见证人的列表来说,必须依托于父节点。如果现在区块链上被确认的区块高度是 Block N,现在要产生 Block N+1 的区块,我们如何填充区块的见证人列表? 我们必须通过 Block N 的区块达成共识,当前的 Block N 上面的见证人列表是谁,就填到下一个见证人列表当中去。对创始块来说,我们通过创世配置文件,把见证人列表直接写在创世块中。
3.4 轮流生产者的实现
3.4.1判断当前轮次是否需要产块
当每个节点收到一些条件之后,如何判断自己可以出块?普通的是通过计算题目,现在是通过一些条件判断,我们根据以下几个条件值: 当前的时间戳、当前区块的产块人,Parent 区块的见证人列表,Parent 区块的产块人,Parent 区块的时间戳,产块周期来共同计算出来当前矿工到底有没有资格出块。如果现在没有资格,那不应该出块。
3.4.2 轮流生产者的实现—分析
图中,如果我们有 A、B、C 三个节点,这是他们的 parent 节点的数据结构,parent 见证人的列表是 ABC,时间戳是 T,产块周期是 3 秒钟,当前的时间点是 T+3,这三个节点点各自判断有没有资格出块。根据图,我们发现见证人列表永远都是 ABC,父节点产块人是 A,下一个要 B 产块,B 在 T+3 时间可以产块,A 和 C 在 T+3 的时间节点是不能产块的。A 可能要等到 T+9 的时间点产块,C 是 T+6 的时间点可以产块。这样才能保证整个网络持续的往下运行。
?
逻辑通过代码实现,我们根据条件来计算一个值,计算出轮次查, 我们可以根据时间戳和产块节点和当地时间戳判断是不是可以出块。
?
还有一个问题是产块没到自己的轮次或现在不应该产块,所以不会产生结果对象,也就不会触发提交新的 worker,也没有其他的时间节点触发这个 channel,这个时候我收不到任何消息,什么都不能干,这个是 Clique 测试网络存在的问题。我们不希望这种情况发生,希望网络永远是向前推进,即便是网络出现故障或者异常,只要保证后面的节点 OK 还是可以往前走的。解决这个问题就需要做一些改变,我们把 worker 产生的通知机制去掉。每次产生 seal 的方法不是通过这两个地方通知他,而是通过 Seal 本身判断自己,Seal 判断完之后注册下一个 Seal,这样永远保证它可以判断它自己。
?? 根据上述内容,自己判断自己的话,需要给自己注册一个动作的定时器。还是以刚刚的场景分析,对于 A 节点来说,这个时间节点 A 不能出块,因为没有符合条件,他给自己定时器是什么时间点呢?一种方案是我在 T+9 时去出块,我就等 9 秒钟或 6 秒钟判断我自己是不是有出块的资格。在实际测试中这种方案的效果并不是特别好。有两个原因;
第一个是见证人的列表会发生变化,如果这个列表发生变化之后,下一次被唤醒的时候,出块的轮次也发生变化,这个时候就有可能错过了出块的机会,会导致整个网络的产块周期变的更加不稳定。 第二,我的迭代时间比较长,降低自己出块的频次,一旦整个网络发生不稳定的情况下,会处于 side fork 的情况。整个网络每个节点都在出块,但是没有被其他的节点接受,我们希望尽可能的在短时间之内同步,也不能过分的小,过分的小会使产块的性能有问题。我们定义两个时段,一个是 3 秒钟,第二是定义一个最小值,其实在出块的时候,在节点出块之前会做打包、验证的工作。我们扣除时间消耗,打包时间过长,最小的值就会有用,可以帮助快速的出块。这种情况下,前面因为某种原因导致你的产块速度慢,或你有可能这个时间点应该出块,但压根没有出块,这个时候就小于 0 了,这个时候马上让他出块。
在每个 Seal 方案里面会注册一个方法,使其在下次被调用,这个方法叫registernext
3.4.3 自定义奖励规则
我们对奖励也做了变化, 对我们应用场景来说会改变奖励的分配规则,使我们适应不同的业务场景。?? 首先在创世块当中确认奖励规则,这是一个 default-reward 的规则,这个规则可以在节点挖矿完之后按照这个规则去发放奖励,具体怎么做呢:就定一个接口,定义一个方法,比如我希望出块人可以获得五个币的奖励,在每次出完块之后,其他节点验证完之后,最后在这个数据结构里面,把账号里面的钱就增加5个币,这是最简单的实践。我们把代码抽出来之后扩展自己奖励分配的规则,根据时间出块,或者不给他奖励,或者通过其他方式奖励,或者把建立放在一个池里面。
4.智能合约的支点实践
4.1 合约语言 Solidity
接下来讲一下我们在智能合约里面的实现。我们说的 DPOS 的机制,很大程度上是要跟智能合约所绑定在一起的,我们的投票人如何投票,我们的理事会成员如何选举见证人都是通过 DPOS 去做,这个可以和共识做一个相关联。
4.2 实现投票合约
4.2.1 理事会
? 我们对于理事会的投票思路就是普通用户,通过自己持有的投票权,质押投票权可以选举理事会的成员,会通过 Top N 的计算,自动去发生变化,现在一些 DPOS 的机制是有换届的概念,每天换一届,我们这边是自动的,这个时间点你只要票数够高,马上就成为理事会的成员。
4.2.2 见证人
? 见证人是通过理事会任命的,一个理事会成员有资格去发起一个提案,这个提案可能包含提名或者是移除一个见证人,他发起提案之后,其他理事会成员可以选择通过还是不通过,这个提案有一定时效期,超过这个时间窗口也是自动作废的,一旦超过半数就通过,如果半数以上否认掉那就是作废了。
4.3 智能合约设计模式
对于智能合约有很多资料,你可以通过业务的智能合约代码和控制器拆开,我们的业务是会变化的。对于智能合约来说,一旦进入到这个区块链是不能改的,这时你必须要写一个新的合约替代它,但是对外暴露的接口只有一个,可以把控制器和业务层分开,你只需要把控制器变更下面业务合约的地址就行。此外还有一种思路,我们可能会在智能合约里面存一些数据比如说存证数据,但是一旦新的业务实现之后,这个数据不通用,这两个智能合约虽然类似,但是数据是不通用的,我们把数据单独的划分出来,数据也是一个合约,不需要设计的过分的复杂,可以同时被上面的业务层所通用。
4.4 智能合约并不智能,反而有太多坑
智能合约本身有很多的限制,对于参数的数量限制,对于返回值的定长设置,包括智能合约的大小都不能定得特别大,所以说智能合约从现在的角度来说,还是需要一段时间优化。
4.5 智能合约设计模式,使用单一合约
因为它本身也有一些问题在,所以理想的设计模式,有控制器层和数据存储层,我们都不要了,一开始是只有一个合约,或者只有一类合约来做所有的工作。后来发现一旦拆下来这些层之后,很多功能就不能这么做了,我们开发智能合约的思路就是跟我们写代码的思路是一样的,定义返回值,定义变向量,就做不了,最后通过单一合约去做这些事情。
5. 压力测试下暴露的问题
5.1 以太坊公链并不会有压力测试的场景,需要大量的优化和测试
?
5.2 流量控制 / 重发机制
?? 最简单的一点是通过一些流量控制和重发机制,我们对外业务调用的时候,不是直接连接到见证节点,而是网关节点,代码都是一样的,在里面会对所有的请求做一个限制。达到一定的程度之后就会拒绝请求。
5.2.1 对交易请求进行检查,是否到达上限
??
实际的代码实现中,控制一下调用的接口,我们不想对 p2p 网络造成影响。
5.2.2 重发机制,防止p2p网络交易广播失败
??
这个在节点规模很大的情况下不大容易出现问题,但是在范围较小的情况下就会出现,我们在程序中的 pending list 里增加了重发的机制。
2018年的 Gopher Meetup 将在深圳开启巡回第一站,这一次邀请了很多新的讲师给大家一起交流分享Go的使用经验?
点击阅读原文报名参加