作者简介
朱 崇 文
恺英网络区块链组技术经理
目录
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,以及确认时间有一定的要求。第二,共识机制是一种思想,以公有链为基础,可对外开放,任何人都可以参与,设立理事会和见证人角色,理事会管理区块链网络,见证人生产并验证区块,构成了一个良性的生态圈。
图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 里增加了重发的机制。
有疑问加站长微信联系(非本文作者)