参考
https://mp.weixin.qq.com/s/Kg7c-SMhzOti7aEIgFjeIw
https://developers.eos.io/eosio-nodeos/docs/consensus-protocol-upgrade-process
https://github.com/EOSIO/eos/issues/7237
稍后测试,在做补充
本文所述基于EOSv1.2.3。
EOS区块生产和同步主要涉及共识算法DPOS和aBFT,其源码实现主要涉及chain_plugin、producer_plugin、net_plugin和controller4个模块以及eosio.system智能合约等。
EOS的区块生产,遵循DPoS(Delegated Proof-of-Stake)机制。
简单来说,所有拥有EOS token的人都是EOS区块生产的参与者。
任何人都可以申请出块。
任何人都可以选择不直接完成出块工作,而是将自己所持token抵押给出块申请者(PoS),委托(Delegate)他们完成出块工作。
最终,按照token比例,选出前21名出块者(BP,Block Producer),由他们代理出块。
ps:上述部分,EOS选举主要在eosio.system智能合约中实现,设置生产者队列函数为update_elected_producers()。笔者工作EOS不用于公链而是联盟链,不需要选举,故直接调用eosio.bios中的setprods()函数设置。
21名BP依次轮流出块,不像比特币等,同一时刻所有BP是竞争关系。每个BP轮到自己出块时,连续出块12个,每个块耗时500ms。
ps:上述这一块逻辑在producer_plugin中实现。
EOS中每个区块被生产出来后,需要所有BP的确认,按照BFT共识机制确认后,才会变成不可逆的状态,在此之前都是reversible block。因此,需要进行区块在网络中的同步。
数据一致性是分布式系统数据同步的重要话题,BFT(Byzantine fault tolerance)是其中的一种代表共识机制,或者称它为算法。

如上图所示,BFT共识机制主要有以下步骤:
BFT算法对每个区块需要发送至少2条消息,EOS对此进行了优化。BM将其称为pipelined:管道式、流水线式。
即每次生产一个区块,由于本身就需要将该区块广播到p2p网络中供其他节点同步,因此将Pre-Commit和Commit数据与该区块数据一并广播出去。
如下图所示:

上半部分描述了区块number不可逆的过程,下半部分描述了每个区块由谁Pre-Commit,由谁Commit。
假设有ABCD四个BP,每个BP每次出1个区块。BFT要求2/3+1个节点确认,即2/3*4+1=3。
假设每轮都是按照A、B、C、D的顺序出块。
基于上述优化,p2p网络的带宽压力、区块哈希验证频次等大幅降低。
但带来的问题是,出块、不可逆糅合在了一起,而非并行的。
BM的解释是:虽然如此,相对其它平台(比特币,以太坊)的机制来说,一个区块从生产出来到变为不可逆,其时间不算太久,且实际上每两个不可逆块之间的间隙时间非常短。长远考虑,只有这样,才能实现更好的扩展性。
原文地址:DPOS BFT— Pipelined Byzantine Fault Tolerance
chain_plugin插件主要功能为:
下图为chain_plugin核心代码逻辑

chain_plugin启动前,先调用plugin_initialize()函数,该函数检查启动参数。除了一些基本配置外,还检查replay相关参数:
当plugin_initialize()上述操作处理完后,根据最终配置参数,对Controller模块进行初始化(emplace()-->构造函数),然后在调用plugin_startup()时调用了Controller::startup,完成了Controller模块的启动
实现了http接口中的一部分接口。
producer_plugin插件主要功能为:
producer_plugin核心函数有:
下图为producer_plugin生产、同步区块的核心代码逻辑:

如上所示,producer_plugin启动后,调用schedule_production_loop()函数,该函数是插件的最核心的函数,是一个递归函数,负责无限循环出块。
EOS500ms出一个块,因此需要启动一个定时器_timer,逻辑如下:
producer_plugin的主流程是schedule_production_loop(),其中定时器_timer会根据实际情况设置等待时间。如是轮到该节点生产区块,则每次等待的时间为:总时间(500ms)-已用时间。如果未轮到该节点生产区块,则计算下一次出块时间(more than 500ms),并启动定时器等待。
如果net_plugin插件或bnet_plugin插件收到网络上的区块,则需要同步。调用on_incoming_block()函数,该函数内部逻辑与生产区块逻辑类似,主要调用controller的abort_block(),start_block(),push_transaction(),finalize_block(),commit_block()。
注意,http和cleos提供了一些接口,其中包括push_transaction,push_block接口,其逻辑见上图,比较特殊,不做太多解释。
下图给出了区块同步的更详细的说明:

重点做以下解释:
net_plugin的主要功能如下:
net_plugin的代码结构如下:

见上图:
net_plugin插件在启动时,订阅了chain_plugin插件的accepted_block信号,该信号在区块被提交到本地待确认不可逆数据库(fork_db)中后发送。
收到该信号后,net_plugin插件向所有连接中的网络节点广播signed_block_message。
其他节点的net_plugin插件的start_read_message异步循环等待网络消息,收到其他节点signed_block_message后,会进行判断,如果本节点没有该区块且该区块合法,则保存到本地fork_db中,如果存在分叉,则按照最长链的原则,尝试进行合并,再启用该最长链。
每个BP连续出块12个(12*0.5s=6s),每出一个块便立即广播,理想中各节点的区块链是实时同步的,然而由于网络原因,或者后加入的节点,其往往落后其他节点很多区块。因此涉及到大量区块的同步问题。
各节点每次建立连接时会发送 handshake_message ,该消息主要用于区块同步。每次握手进行一次区块同步状态判断,同步完成后会再次发送 handshake_message,循环进行判断。
同步状态有5中场景:
同步状态1见总图,由于较简单,不再赘述。

如上图所示:

如上图所示,Bob收到Alice的通知后,与Alice的同步状态1类型,调用相关函数进行同步。

如上图所示,Alice向Bob请求区块,Bob收到消息后,以区间[Bob LIB+1,BOB HB]循环逐一发送区块。
如果Bob的HB number 为0,则为异常,需要告知Alice。

如上图所示,该状态下,参考同步状态3。
另附消息发送函数enqueue()逻辑:

net_plugin插件启动并建立连接后,会调用start_monitors(),一是监听网络连接状态,如果断开会重新尝试建立连接;二是监听交易时间,如果交易超时,则会将其移除入链队列。
Controller模块位于/libraries/chain/下,是EOS区块入链的核心控制器,内容非常多,也非常重要。
Controller主要功能为:
下图为Controller核心代码逻辑:

Controller模块由chain_plugin负责初始化和启动,chain_plugin根据启动参数启动Controller,从而对底层数据结构进行初始化操作。
区块相关的接口有:
交易相关的接口有:
其它接口:
提供了一些进度信号,producer_plugin、mongodb_plugin、net_plugin等等会进行订阅,从而完成各自的功能。
相关信号有:
例如,mongodb_plugin收到accepted_block后会将数据写入mongodb。
数据结构包括如下:
注意:
主要实现了抵押RAM、申请BP、申请代理,投票、选举BP等功能。
假设使用一台机器作为BP节点机器。大家都知道eos的data目录超级大,备份的话只能全量备份。cp 的时间过于长,影响出块和RPC业务等提供。
机器上跑2个nodeos进程,一个跑BP账号做出块节点,一个做同步几点。
保留三份data数据。出块节点初始化占一个,剩余两个,其中一个同步节点同步使用,同步节点定期切换剩余两个目录,保持2个目录数据与主网持平,当出块节点出问题导致data脏时,时切换到当时没有占用的data目录。然后删除脏目录,停止同步节点,并cp 同步节点前面用的data目录补齐第三目录。然后选一data重新运行同步节点。
且如果此机器提供RPC服务的话,可以做高可用。
本方案可保证出块节点最大的运行时间。
data 三份的占用。
关于抵押和赎回的方法,拿eosjs举例
https://eosio.github.io/eosjs/guides/2.-Transaction-Examples.html
const result = await api.transact({
actions: [{
account: 'eosio',
name: 'delegatebw',
authorization: [{
actor: 'useraaaaaaaa',
permission: 'active',
}],
data: {
from: 'useraaaaaaaa',
receiver: 'useraaaaaaaa',
stake_net_quantity: '1.0000 SYS',
stake_cpu_quantity: '1.0000 SYS',
transfer: false,
}
}]
}, {
blocksBehind: 3,
expireSeconds: 30,
});
const result = await api.transact({
actions: [{
account: 'eosio',
name: 'undelegatebw',
authorization: [{
actor: 'useraaaaaaaa',
permission: 'active',
}],
data: {
from: 'useraaaaaaaa',
receiver: 'useraaaaaaaa',
unstake_net_quantity: '1.0000 SYS',
unstake_cpu_quantity: '1.0000 SYS',
transfer: false,
}
}]
}, {
blocksBehind: 3,
expireSeconds: 30,
});
操作细节看以上demo即可,下面我们说下抵押时transfer这个参数。
先看合约 (跳转代码)
if ( transfer ) {
from = receiver;
}
如果设置transfer参数为true,及把来源的账户修改为接收账户。
也就变成了接收账户自己给自己抵押,由来源账户付钱
del_bandwidth_table del_tbl( _self, from.value );
是以来源账户为查询的scope
所以RPC查询如下
post: https://api.eoslaomao.com/v1/chain/get_table_rows
data:
{
"scope": "bcskillsurou",
"code": "eosio",
"table": "delband",
"json": true
}
返回
{
"rows": [
{
"from": "bcskillsurou",
"to": "bcskillsurou",
"net_weight": "0.0046 EOS",
"cpu_weight": "0.0147 EOS"
}
],
"more": false
}namespace fc { namespace crypto {
namespace config {
constexpr const char* public_key_legacy_prefix = "EOS"; // 修改EOS为需要的前缀即可,如BSC