1 概述
本文所述基于EOSv1.2.3。
EOS区块生产和同步主要涉及共识算法DPOS和aBFT,其源码实现主要涉及chain_plugin、producer_plugin、net_plugin和controller4个模块以及eosio.system智能合约等。
2 共识算法
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的共识机制叫DPoS(More than that)。
EOS中每个区块被生产出来后,需要所有BP的确认,按照BFT共识机制确认后,才会变成不可逆的状态,在此之前都是reversible block。因此,需要进行区块在网络中的同步。
数据一致性是分布式系统数据同步的重要话题,BFT(Byzantine fault tolerance)是其中的一种代表共识机制,或者称它为算法。
如上图所示,BFT共识机制主要有以下步骤:
- 提案1个block;
- 进入Pre-commitment阶段,所有BP确认该提案;
- 进入Commitment阶段,所有BP收到2/3+1或更多Pre-commit后,发送Commit;
- 当某个BP收到2/3+1或更多Commit后,该block即不可逆。
- 该算法适用于恶意节点不超过1/3的场景,可以保证达到最终一致性。
至此,我们可以说:EOS的共识机制叫DPoS & 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的顺序出块。
- A 出块1,Pre-Commit不可逆块为0,Commit不可逆块为0
- B 出块2,Pre-Commit不可逆块为0,Commit不可逆块为0
- C 出块3,ABC3个节点Pre-Commit区块1,故Pre-Commit不可逆块为1,Commit不可逆块为0
- D 出块4,BCD3个节点Pre-Commit区块2,故Pre-Commit不可逆块为2,Commit不可逆块为0
- A 出块5,CDA3个节点Pre-Commit区块3,故Pre-Commit不可逆块为3,CDA3个节点Commit区块1,故Commit不可逆块为1
- B 出块6,DAB3个节点Pre-Commit区块4,故Pre-Commit不可逆块为4,DAB3个节点Commit区块2,故Commit不可逆块为2
- 以此类推
基于上述优化,p2p网络的带宽压力、区块哈希验证频次等大幅降低。
但带来的问题是,出块、不可逆糅合在了一起,而非并行的。
BM的解释是:虽然如此,相对其它平台(比特币,以太坊)的机制来说,一个区块从生产出来到变为不可逆,其时间不算太久,且实际上每两个不可逆块之间的间隙时间非常短。长远考虑,只有这样,才能实现更好的扩展性。
原文地址:DPOS BFT— Pipelined Byzantine Fault Tolerance
至此,我们应当说:EOS的共识机制叫 DPOS BFT— Pipelined Byzantine Fault Tolerance。
3 chain_plugin
chain_plugin插件主要功能为:
- 检查启动参数,判断是否需要replay区块链
- 初始化和拉起Controller模块
- 提供相关信号、方法,主要用于给controller、net_plugin、producer_plugin、用户输入等架桥
- 向用户提供链的其它set/get接口,诸如:get_account、get_transaction_id……
下图为chain_plugin核心代码逻辑
3.1 检查启动参数,判断是否需要replay()区块链
chain_plugin启动前,先调用plugin_initialize()函数,该函数检查启动参数。除了一些基本配置外,还检查replay相关参数:
- export-reversible-blocks:会将原来的reversible_block进行导出备份,该参数单独优先检查
- delete-all-blocks:删除所有旧的blocks,重头开始replay区块链,包括清空State DB和Block log
- hard-replay-blockchain:清空State DB,如果有配置truncate-at-block,按照[first,truncate-at-block]区间,否则[first,end]区间重新加载Block log中的区块,即该区间的区块仍然生效不replay,其它区块全部replay。如果reversible db有数据,一并尝试恢复
- replay-blockchain:清空State DB,如果有配置fix-reversible_blocks,则尝试恢复reversible DB
- fix-reversible_blocks:尝试恢复reversible DB
- import-reversible_blocks:旧的不要,用导入的替代
3.2 初始化和拉起Controller模块
当plugin_initialize()上述操作处理完后,根据最终配置参数,对Controller模块进行初始化(emplace()-->构造函数),然后在调用plugin_startup()时调用了Controller::startup,完成了Controller模块的启动
3.3 提供相关信号、方法,主要用于给controller、net_plugin、producer_plugin、用户输入等架桥
- accepted_block:当net_plugin收到一个区块,或者用户手动push的区块(可能仅仅是一个接口,该场景应当不常见),chain_plugin通过该方法转发区块到producer_plugin中
- accepted-transactions:同理。需注意,cleos/http的push transaction和push action等,均在此处入链。
3.4 向用户提供链的其它set/get接口
实现了http接口中的一部分接口。
4 producer_plugin
producer_plugin插件主要功能为:
- 当本节点处于生产阶段时,与chain_plugin::controller交互,调用controller相关接口进行区块生产;
- 当本节点处于同步阶段时,接收net_plugin插件收到的区块,调用controller相关接口进行区块同步。
producer_plugin核心函数有:
- schedule_production_loop:递归调用,生产区块
- start_block:初始化当前区块数据
- produce_block:打包、签名、提交区块入链
- on_incoming_block:接收net_plugin收到的区块,入本节点链
- on_incoming_transaction_async:接收net_plugin收到的交易,入本节点链
- on_block:本节点区块入链成功后,相关状态更新
- on_irreversible_block:本节点区块不可逆后,相关状态更新
下图为producer_plugin生产、同步区块的核心代码逻辑:
4.1 生产区块
如上所示,producer_plugin启动后,调用schedule_production_loop()函数,该函数是插件的最核心的函数,是一个递归函数,负责无限循环出块。
EOS500ms出一个块,因此需要启动一个定时器_timer,逻辑如下:
- 关闭之前的定时器:_timer.cancel()
- 调用start_block()函数初始化新的区块信息。首先调用controller::abort_block()重置数据,然后调用controller::start_block(),根据上一个区块信息生成一个新的区块,最后将由于controller::abort_block()而未来得及入链的交易(unapplied_transactions)重新push_transaction()进新的区块。
- 重启定时器_timer(),异步等待新区块截止时间,这期间内,EOS等待并接收用户的交易请求。
- 如果用户有新交易,调用push_transaction()执行交易并保存到区块中。
- 如果没有其它情况,定时器到期后,调用produce_block(),对该区块进行打包(finalize_block())、签名(sign_block()),然后提交(commit_block())到本节点区块链(fork_db)上,commit_block()会发送accepted_block信号给订阅者,这其中包括producer_plugin::on_block(),该函数进行相关数据更新。fork_db每次新增一个区块,就会检查是否有新的不可逆区块产生,如果有,发送irrerersible_block信号给订阅者,这其中包括producer_plugin::on_irreversible(),该函数进行相关数据更新。最后,再次递归调用schedule_production_loop()进行下一个区块生产。
- 如果出现其它原因,如收到网络上发送过来的区块,且定时器未到期,则会转入区块同步逻辑,之前的所有执行会被重置,未来得及执行的交易会被备份到unapplied_transactions中。
4.2 同步区块
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接口,其逻辑见上图,比较特殊,不做太多解释。
下图给出了区块同步的更详细的说明:
重点做以下解释:
- fork_db:区块链,其中的区块尚未不可逆,当新块到达后,可能存在分叉。需要根据最长链原则,选出较长的一条链,较短的链被删除。
- 每个新区块基于其上一个区块出块,根据该信息,可以对旧链fork_db的head block(HB)和新的区块(new HB)分别进行追溯,将旧链中的区块合并到新链上,然后删除旧链,保存新链。主要代码在fetch_branch_from()中实现,思想请参考:拉链法。
5 net_plugin
net_plugin的主要功能如下:
- 与其他节点建立连接;
- 向网络中广播本节点区块;
- 接收其他节点广播的区块;
- 节点之间区块同步。
net_plugin的代码结构如下:
5.1 与其他节点建立连接
见上图:
- net_plugin插件启动时,根据p2p-listen-endpoint成员变量(配置文件或命令行参数p2p-listen-endpoint参数(默认值0.0.0.0:9876)),调用tcp::acceptor的listen()启动监听,调用start_listen_loop()递归调用async_accept()异步等待网络连接请求(参考socket通信)。
- 根据supplied_peers成员变量(配置文件或命令行参数p2p-peer-address(可多个)),逐一调用async_connect()进行连接请求。
- 其他节点收到请求后回复,并调用start_session()建立会话。
- 本节点收到回复得知成功后,亦调用start_session()建立会话。
- start_session()包含两步:
- 调用start_read_message()异步循环等待其他节点的消息;
- 调用send_handshake()发送握手消息。
- net_plugin插件中定义了9个消息类型,其中主要消息为:
- handshake_message:握手消息
- notice_message: 通知消息,主要在同步时使用,进行同步状态发送
- sync_request_message:当本节点区块链的HB number小于对方节点LIB number时发送
- request_message:当本节点区块链的HB number小于对方节点HB number时发送
- signed_block_message:每个区块由该消息逐一发送
5.2 向网络中广播本节点区块
net_plugin插件在启动时,订阅了chain_plugin插件的accepted_block信号,该信号在区块被提交到本地待确认不可逆数据库(fork_db)中后发送。
收到该信号后,net_plugin插件向所有连接中的网络节点广播signed_block_message。
5.3 接收其他节点广播的区块
其他节点的net_plugin插件的start_read_message异步循环等待网络消息,收到其他节点signed_block_message后,会进行判断,如果本节点没有该区块且该区块合法,则保存到本地fork_db中,如果存在分叉,则按照最长链的原则,尝试进行合并,再启用该最长链。
5.4 节点之间区块同步
每个BP连续出块12个(12*0.5s=6s),每出一个块便立即广播,理想中各节点的区块链是实时同步的,然而由于网络原因,或者后加入的节点,其往往落后其他节点很多区块。因此涉及到大量区块的同步问题。
各节点每次建立连接时会发送 handshake_message ,该消息主要用于区块同步。每次握手进行一次区块同步状态判断,同步完成后会再次发送 handshake_message,循环进行判断。
同步状态有5中场景:
- [State 0]双方HB id相同,id为数字摘要,如果相同说明HB完全一致,不需要同步,则Alice向Bob发送notice_message。
- [State 1]如果Alice的区块链非常短,其HB number 竟然没有对方节点LIB number大,则Alice向Bob发送sync_request_message,附带参数(start,end)表示同步区间。其中start为Alice的LIB number,end为Bob的LIB number。
- [State 2]与State 1相反,如果Alice发现Bob的HB number < Alice的LIB number,则发送notice_message让Bob主动来请求同步数据。
- [State 3]如果Alice的HB number >Bob 的LIB number,但Alice的HB number < Bob的HB number,则也需要同步,Alice向Bob发送request_message,Bob收到消息后,从Bob的LIB开始,一直到Bob的HB,逐一发送区块。
- [State 4]与State 3相反,则Alice发送notice_message让Bob主动来请求同步数据。
5.4.1 同步状态0
同步状态1见总图,由于较简单,不再赘述。
5.4.2 同步状态1
如上图所示:
- Alice向Bob请求区块同步,总区间为[Alice LIB + 1, Bob LIB]。
- 调用request_next_chunk()对总区间分组同步,默认大小为100个区块一组(配置文件或命令行参数sync-fetch-span)。
- net_plugin::sync_manager子模块顺序选择一个网络节点,发送消息,并启动定时器_timer异步等待5秒。如果5秒后未收到对方的signed_block_message,则取消该请求(再次发送sync_request_message,但区间为[0,0]),并重新选择一个网络节点发送消息。
- 如果5秒内收到对方节点的signed_block_message,取消_timer定时器,判断是否是自己想要的区块,保存后判断是否同步结束。
- 如果尚未同步结束,重启_timer定时器等待;
- 如果分组同步结束,再次调用request_next_chunk()请求下一个分组区块;
- 如果总区间同步结束,向所有网络节点再次发送握手消息,继续进行同步状态判断。
- 对方节点收到sync_request_message后,按请求区块区间,循环逐一发送区块。
5.4.3 同步状态2
如上图所示,Bob收到Alice的通知后,与Alice的同步状态1类型,调用相关函数进行同步。
5.4.4 同步状态3
如上图所示,Alice向Bob请求区块,Bob收到消息后,以区间[Bob LIB+1,BOB HB]循环逐一发送区块。
如果Bob的HB number 为0,则为异常,需要告知Alice。
5.4.5 同步状态4
如上图所示,该状态下,参考同步状态3。
另附消息发送函数enqueue()逻辑:
5.5 补充
net_plugin插件启动并建立连接后,会调用start_monitors(),一是监听网络连接状态,如果断开会重新尝试建立连接;二是监听交易时间,如果交易超时,则会将其移除入链队列。
6 controller
Controller模块位于/libraries/chain/下,是EOS区块入链的核心控制器,内容非常多,也非常重要。
Controller主要功能为:
- 被chain_plugin初始化和启动
- 对上提供区块和交易等相关接口
- 对上提供区块和交易入链进度相关信号
- 对下操作相关数据接口,进行数据管理
下图为Controller核心代码逻辑:
6.1 被chain_plugin初始化和启动
Controller模块由chain_plugin负责初始化和启动,chain_plugin根据启动参数启动Controller,从而对底层数据结构进行初始化操作。
6.2 对上提供区块和交易等相关接口
区块相关的接口有:
- abort_block() :取消上一个正在生产的区块,其中的交易转到本次新区块中处理
- start_block() :开始一个新区块生产,启动一个异步定时器等待交易被插入,定时器结束后开始打包等后续工作。此处还会进行BFT共识机制的处理。
- finalize_block():本区块时间已到,打包区块
- sign_block():对区块进行签名
- commit_block():提交区块入fork_db,等待不可逆
- push_block() : 主要用于初始化时或同步时插入区块,内部调用apply_block()函数以及上述几个函数,但没有异步定时器等代码。
- pop_block():同步时fork_db发现分叉,需要对短链进行pop_block()
- on_irreversible():区块不可逆时触发
交易相关的接口有:
- push_transaction() : 插入交易
- push_scheduled_transaction() : 插入延期的交易
其它接口:
- set_proposed_producers() :更新BP列表
6.3 对上提供区块和交易入链进度相关信号
提供了一些进度信号,producer_plugin、mongodb_plugin、net_plugin等等会进行订阅,从而完成各自的功能。
相关信号有:
- pre_accepted_block :调用push_block()(同步、刚启动时从数据库恢复),区块尚未add()到fork_db之前,先发送这个信号
- accepted_block_header:调用push_block()或commit_block()(生产区块),区块被add()到fork_db后,发送该信号
- accepted_block:调用commit_block(),区块被add()到fork_db后,发送该信号
- irreversible_block:调用push_block()恢复数据时或on_irreversible()时,发送该信号
- accepted_transaction:调用push_transaction()或push_scheduled_transaction()成功后,发送该信号
- applied_transaction,同上
例如,mongodb_plugin收到accepted_block后会将数据写入mongodb。
6.4 对下操作相关数据接口,进行数据管理
数据结构包括如下:
- pending :正在生产的区块,abort_block(),start_block()等均对它修改
- unapplied_transactions:上一个区块未出块成功,则其交易在abort_block()时转存到这里,以便新的区块start_block()时重新push_transaction()
- fork_db:区块生产完成后插入这里,可能存在分叉。当进行区块同步时,如果检测到分叉,则进行最长链的生成和选择。push_block()和commit_block()时调用add(),不可逆时或分叉时调用erase()
- head : 指向fork_db的头块,用于生成下一个区块,以及进行快速比较等操作,fork_db变化则同时变化head
- reversible_blocks:可逆的区块链。commit_block()时调用add(),on_ireeversible(),pop_block()时调用erase()
- blog:不可逆的区块链,append only,on_irreversile()时调用append()
- db : 不仅保存了区块,还保存了智能合约数据,账号数据等等其它数据。大部分数据修改的地方也会对其进行更新。
注意:
- 区块广播时发送的是signed_block结构体,其继承signed_block_header-->block_header。
- 各节点存储的数据还有更多信息,如DPOS+BFT相关的数据,存储在block_state-->block_header_state中。
- signed_block中存储了所有交易的摘要:vector
7 eosio.system
主要实现了抵押RAM、申请BP、申请代理,投票、选举BP等功能。