1 基本概念
1 eos 每500毫秒出一个块,每个生产节点连续出12个块,然后切换到下一节点生产。
2 eos需要两轮共识
新生产/接收的块会放入内存块分叉数据库fork_db中,等待共识。
单节点不可逆块:我们把完成第一轮共识的块,叫做单节点不可逆块。
全网不可逆块: 完成第二轮共识的块,叫全网不可逆块。
完成两轮共识的全网不可逆块,才是我们常说的真正意义上的不可逆块,会从fork_db中移出写入block_log,实现落块。
3 共识相关关键变量
下面是eos中定义的block_header_state数据结构,共识用到的大多数变量值都保存在这里:
```c++
struct block_header_state {
uint32_t block_num = 0;//块号
// 1 第一轮 , 记录每个块的待确认数,
vector<uint8_t> confirm_count; // [1,1,1,...2,2,2,...3,3,3]
// 2 第二轮, 经过首轮确认,得到的候选不可逆,单节点不可逆最大块号
uint32_t dpos_proposed_irreversible_blocknum = 0;
// 记录每个生产者的最后不可逆块序号
flat_map<account_name,uint32_t> producer_to_last_implied_irb;
// 3 全网不可逆块号
uint32_t dpos_irreversible_blocknum = 0;
}
//1 第一轮 ,水印值,记录每个生产者生产的最后一个块的块号。
std::map<chain::account_name, uint32_t> _producer_watermarks;
* confirm_count:vector数组,第一轮共识使用,里面保存着未完成第一轮共识的块的待确认数。
* dpos_proposed_irreversible_blocknum :单节点不可逆块号,第二轮共识使用,记录经过第一轮共识得到的单节点不可逆块号。
* producer_to_last_implied_irb:map对象,存放生产者及单节点不可逆块号的键值对,第二轮共识用。
* dpos_irreversible_blocknum:完成第二轮共识的最新全网不可逆块块号,根据该块号实现落块。
另外生产者水印值也是一个用于共识的关键变量值,保存在生产者插件中:
```c++
//1 第一轮 生产者水印值,记录每个生产者生产的最后一个块的块号。
std::map<chain::account_name, uint32_t> _producer_watermarks;
- _producer_watermarks: map对象,生产节点水印值,第一轮共识使用,保存每个生产节点最后生产的块号。
2 共识原理举例
第一轮共识
-
生产节点会对它未确认的块进行确认共识,未确认块数=当前链头块号-它最后出块的块号。
- 节点出块时,会按拜占庭将军算法(当前生产节点数*2/3+1)计算该块需要几个节点共识,并把该值放入待共识数组confirm_count[]的最后。
-
生产节点出块时,除了会共识之前未确认过的块,也会对本次出的块进行本节点确认共识。
-
只有生产节点出块时,才会对之前节点出的块进行确认共识,节点收到块时,并不做对收到的块进行确认共识。
以3个生产节点每轮出2个块为例,假设此时轮到节点1出第7块:
1 计算节点1未确认块数:目前链头块块号6 - 节点水印值2(节点1生产的最后一个块)=4;
即生产7号块时,前面有4个块待节点1确认。
2 计算7号块需要被几个节点确认: 3(节点数)* 2/3 +1 = 3,将3放入confirm_count[]最后;
3 第一轮共识计算:实际就是对confirm_count[]数组中保存的待确认数进行减1计算。
块待确认数组confirm_count[] 在第一轮共识中的变动情况展示如下:
- 生产7块前,里面有4个待确认块。
- 先将7块的待确认数3插confirm_count的最后。
- 循环从confirm_count[]尾部对其中的值执行减1操作。当有confirm_count[i]==0成立,停止循环。i位置对应的块号,即此次共识得到的单节点不可逆块号,此时4号块成为单节点不可逆块,它之前的块不用计算,都自动成为单节点不可逆块。
- 移动confirm_count[],清除4号块之前所有块。
第一轮共识完成,得到dpos_proposed_irreversible_blocknum=4。
第二轮共识
这里主要用到producer_to_last_implied_irb,它是一个map容器,里面放的是生产节点的节点名及该节点对应的单点不可逆块号,此处的单点不可逆块号就是该节点在执行第一轮共识计算时得到的值。只有出块节点的单节单节点不可逆值才会更新。
刚才生产7号块时计算得到节点1的当前不可逆块号是4,可推得此时节点3对应的值应该是2,节点2对应值0。当节点2生产第9个块时,节点2第一轮共识得到的单节点不可逆块号6, 其它节点不变,如下图所示:
eos的第二轮共识算法,会将producer_to_last_implied_irb中的块号排序,从小到大取1/3位置的块,成为最终不可逆块。
-
7 号块生产时,取最小1/3的块,块号为0,此时没有块成为全网不可逆块。
-
9号块生产时,取最小1/3的块,块号为2,此时2号之前的块都成为全网不可逆块。
第二轮共识结束,7号块生产时,全网不可逆块dpos_irreversible_blocknum = 0;
9号块生产时,全网不可逆块dpos_irreversible_blocknum = 2;
3 eos块共识流程
下图为根据eos块生产流程画出的块共识流程,下面分别从生产节点作为生产者出块和作为验证节点收到两种情况,结合代码分别对两轮共识流程展开进行分析。
节点作为验证节点收到块,会调用conntroller_impl::apply_block()函数,处理块中数据,最终落块。图中红色底框的内容是第一轮共识用到的函数或变量,橙色底框则是第二轮共识用到的。
3.1 出块节点共识
3.1.1 第一轮共识
节点循环调用schedule_production_loop()函数进行生产循环,函数中会调用start_block()判断是否该本节点出块。
- 计算本块需要确认的块数:blocks_to_confirm = 链头块号 - 本节点水印值;
```c++
producer_plugin_impl::start_block_result producer_plugin_impl::start_block() {
//计算出块者
const auto& scheduled_producer = hbs->get_scheduled_producer(block_time);
//获取该出块者已出最后一个块的块号
auto currrent_watermark_itr =
_producer_watermarks.find(scheduled_producer.producer_name);
if (currrent_watermark_itr != _producer_watermarks.end()) {
//水印值 节点生产的最后块号
auto watermark = currrent_watermark_itr->second;
if (watermark < hbs->block_num) {
//待确认块数=链头块号-本节点最后生产的块号
blocks_to_confirm = hbs->block_num - watermark;
}
}
...
chain.start_block(block_time, blocks_to_confirm);
}
2. 轮到本节点出块,将出块时间和blocks_to_confirm作为参数传入controller_impl::start_block()函数,启动出块:
```c++
void start_block(block_timestamp_type when, uint16_t confirm_block_count,.. )
{
...
//构造block_state类型
pending->_pending_block_state = std::make_shared<block_state>( *head, when );
...
// 执行第一轮共识
pending->_pending_block_state->set_confirmed(confirm_block_count);
//生产者切换
auto was_pending_promoted =
pending->_pending_block_state->maybe_promote_pending();
...
}
-
首先构造block_state数据,存块的数据信息,block_state是block_header_state的子类,构造时会先调用block_header_state::generate_next()函数,构造block_header_state:
```c++
block_header_state::generate_next( block_timestamp_type when )const {block_header_state result;
//根据生产者数量计算生产块的待确认数required_confs
auto num_active_producers = active_schedule.producers.size();
uint32_t required_confs = (uint32_t)(num_active_producers * 2 / 3) + 1;
...
//required_confs插入confirm_count[]最后
result.confirm_count.back() = (uint8_t)required_confs;//producer_to_last_implied_irb数组构建
result.dpos_proposed_irreversible_blocknum =
dpos_proposed_irreversible_blocknum;
result.producer_to_last_implied_irb[prokey.producer_name] =
result.dpos_proposed_irreversible_blocknum;//第二轮确认,从建议不可逆块数组计算最终不可逆块号。
result.dpos_irreversible_blocknum = result.calc_dpos_last_irreversible();
} - 计算本次出块的待确认数,放入待确认数组confirm_count[]的最后。
- 更新producer_to_last_implied_irb中本节点的单节点不可逆块号。
- 根据producer_to_last_implied_irb的值进行第二轮共识计算,具体算法后面分析。
generate_next()中已将本次生产块的待确认数加入了confirm_count[]中,下面开始第一轮共识计算:
```c++
void block_header_state::set_confirmed( uint16_t num_prev_blocks ) {
//confirm_count[] [1,1,1,...2,2,2,...3,3,3] 记录所有待确认块
int32_t i = (int32_t)(confirm_count.size() - 1);
// 本次确认块数,传入确认数+1 把本次出的块也确认了
uint32_t blocks_to_confirm = num_prev_blocks + 1;
//从confirm_count最后放入的最新块开始确认,逐个减一
while( i >= 0 && blocks_to_confirm ) {
--confirm_count[i];
//减完后==0,该块被确认次数满足该块设定的待确认数
if( confirm_count[i] == 0 )
{
//计算块号
uint32_t block_num_for_i = block_num - (uint32_t)(confirm_count.size() - 1 - i);
//建议不可逆块号设为该块号
dpos_proposed_irreversible_blocknum = block_num_for_i;
//该块就是confirm_count最后一个,confirm_count重置
if (i == confirm_count.size() - 1) {
confirm_count.resize(0);
} else {
//该块之前的记录都清除
memmove( &confirm_count[0], &confirm_count[i + 1], confirm_count.size() - i - 1);
confirm_count.resize( confirm_count.size() - i - 1 );
}
return;
}
--i;
--blocks_to_confirm;
}
}
* 参数num_prev_blocks是链头块号减水印值得到的,因为对本次生产的块也要进行确认,所以,实际确认块数会加1。
* 从后到前对confirm_count[]减1,相当于节点确认了该块。
* 第一个使confirm_count[i]==0成立处的块号,就是经过第一轮共识得到的单节点不可逆块号。
出块时间到,节点会调用produce_block()函数进行一些收尾工作,比如计算默克尔根,块签名等,生产者水印值也是在这里跟新的:
```c++
void producer_plugin_impl::produce_block() {
...
chain.finalize_block();
chain.commit_block();
...
//记录生产者水印值,记录当前生产者生产的最新块块号
_producer_watermarks[new_bs->header.producer] = chain.head_block_num();
}
- chain.commit_block()函数中会将此次生产的块加入fork_db库中,且将链头块跟新为此次生产的块。
- chain.head_block_num()函数会得到当前链头块块号。
3.1.2 第二轮共识
前面已经介绍了,在generate_next()中会调用calc_dpos_last_irreversible()函数进行第二轮共识计算,下面我来看一下具体的计算过程:
```c++
uint32_t block_header_state::calc_dpos_last_irreversible()const {
vector<uint32_t> blocknums;
blocknums.reserve( producer_to_last_implied_irb.size() );
for( auto& i : producer_to_last_implied_irb ) {
blocknums.push_back(i.second);
}
if( blocknums.size() == 0 ) return 0;
std::sort( blocknums.begin(), blocknums.end() );
return blocknums[ (blocknums.size()-1) / 3 ];
}
* new_producer_to_last_implied_irb 是一个map容器结构,里面放的是<生产者,节点最后单节点不可逆号> 对,容器的到小等于active_schedule里生产节点的数量。
* 构造了blocknums[]数组,将new_producer_to_last_implied_irb中的单节点不可逆号复制到里面,所以这里blocknums的大小也等于实际生产节点的大小。
* 将blocknums按从小到大的顺序排序,取其(blocknums.size()-1) / 3位置的块号,成为全网不可逆块号。
以21个生产节点为例,(blocknums.size()-1) / 3 =6,blocknums[6]位置的块号,索引从0开始,即从块号从大到小排第15个节点的块号,由拜占庭共识算法21×2/ 3+1=15,可以看出,此处计算虽然是取的1/ 3处的块号,但它是逆序取的,所以第二轮共识实际也是一次拜占庭共识。
这里还有一个注意点,在进行生产节点切换有新节点加入时,producer_to_last_implied_irb中新加入节点的单节点不可逆块号会使用当前链头号进行初始化。
```c++
flat_map<account_name,uint32_t> new_producer_to_last_implied_irb;
for( const auto& pro : active_schedule.producers ) {
auto existing = producer_to_last_implied_irb.find( pro.producer_name );
if( existing != producer_to_last_implied_irb.end() ) {
new_producer_to_last_implied_irb[pro.producer_name] = existing->second;
} else {
new_producer_to_last_implied_irb[pro.producer_name] =
dpos_irreversible_blocknum;
}
3.2 验证节点共识
验证节点收到块,会根据块头中的confirmed变量,即该块已确认块数,在本节点重做该块的共识过程,以保证节点的状态一直性。生产节点收到块,将块放入fork_db时,会调用apply_block()函数,进行块验证,验证过程会调用controller_impl::start_block()函数,构造块的验证环境。
```c++
void apply_block( const signed_block_ptr& b, controller::block_status s )
{
......
auto producer_block_id = b->id();
//验证块前,需构造pendding块环境
start_block( b->timestamp, b->confirmed, s , producer_block_id);
//验证交易
......
}
这个start_block()和出块时调用的start_block()是同一函数,所以前面进行的两轮共识计算,这里也会做一遍。只是这次做是为了和出块节点保持状态一直,做的是实际出块者的共识过程,并不代表本节点共识了相应块。更新的不可逆块号也是实际出块者的值。
### **4 eos块落差336**
eos中是21个节点出块,每个节点连续出12个块,切换下个生产者。每个块需要:21*2/3 +1 =15个节点共识。
下图为eos节点出块序号表展示,节点1第一轮出1-12块,接着节点2出11-24块,图中pirb为单节点不可逆块号,irb为全网不可逆块号。
![](http://www.icode9.com/i/li/?n=1&i=images/blog/201908/29/35e8e75e0c692bf04a57cae32caf03d1.png?,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
* 节点1出1 - 12块,需要包括自己在内的15个节点确认,则到节点15出第169块时,1-12块变成单节点不可逆块,节点15的pirb=12;
* 以此类推,16号节点生产时,24号块成为单节点不可逆,... ,节点8第二轮生产337块时,170块成为单节点不可逆。按第二轮共识,把21个节点的单节点不可逆号放入blocknums[21]数组排序,blocknums里面的数据就是图中pirb桔色底色的值。取blocknums[(21-1)/3] 的块号成为单节点不可逆号。 i=6处的块号12成为全网不可逆块。
* 337块生产前,系统中已经生产了336个块,但没有一个块成为不可逆落块,直到337块生产开始,系统中才首次出现全网不可逆块落块。得到eos系统最大块落差336。
### 5 链接
[星河公链](http://www.3heu.com/)