简介
eos的出块算法是DPOS,大体出块流程选择一组块生产者记录在名单上然后按照这个名单顺序依次出块,一般来说一个正常的节点出现分叉只会出现在接受到P2P节点的block之后,如果自己是生产者不可能产生分叉一定会在当前链上生产,你可以把区块链看成是一条区块前后串在一起的链,我们把head指向的这条链称为当前链,所谓分叉就是新接收的block没有把自己串到当前链的后面而是选择其他一个block后面这样就形成了分叉,形成的分叉本质上就是一颗树,形成分叉不一定立马就会切换而是必须满足一定条件才会切换分叉,所谓切换分叉,就是选择有共同祖先的2个分叉,抛弃旧的一个分叉选择新的一个分叉作为当前链。
fork_db
只有从网络中接收到block才可能产生分叉,如果自己是生产者不可能产生分叉一定会在当前链上生产(恶意节点修改程序的除外),接收block的过程,从p2p网络接收到一个区块最终fork_db.add()添加到fork_db中去
p2pnet----->on_incoming_block()----->push_block()---->fork_db.add()
- 分叉
fork_db中存放着【当前不可能块号, 当前块号】间所有的块,这些区块信息以block_state结构体存在,这些块可以随时分叉,只要这些块比当前不可能块号大,小于当前不可逆块号的块不会在fork_db中存在因为这些块已经不可能产生分叉,事实上分叉就是发生在fork_db中,每次fork_db.add()都可能产生分叉
fork_db.add(),add block之后的三种情形:
1)new_head->header.previous == head->id,不会产生分叉,新block成为当前链的head,下图中新添加的区块E成为新的head
2)new_head->id = head->id,新block添加到了一个分叉并且该分叉没有成为当前链,下图中新添加的区块E链接到了区块B后面形成一个分叉
3)new_head->header.previous != head->id,新block添加到了一个分叉并且该分叉成为了当前链
注:图中方块中的数字为块号,上方的字母为区块id
上述对于第三种情况,需要切换分叉,下面代码做的事情就是在切换分叉通过fork_db.fetch_branch_from选择有共同祖先的2个分叉,抛弃旧的一个分叉选择新的一个分叉作为当前链
// 选择有共同祖先的2个分叉
auto branches = fork_db.fetch_branch_from( new_head->id, head->id );
// 抛弃旧的一个分叉
for( auto itr = branches.second.begin(); itr != branches.second.end(); ++itr ) {
fork_db.mark_in_current_chain( *itr , false );
pop_block();
}
// 选择新的分叉作为当前链
for( auto ritr = branches.first.rbegin(); ritr != branches.first.rend(); ++ritr) {
optional except;
try {
apply_block( (*ritr)->block, (*ritr)->validated ? controller::block_status::validated : controller::block_status::complete );
head = *ritr;
fork_db.mark_in_current_chain( *ritr, true );
(*ritr)->validated = true;
}
catch (const fc::exception& e) { except = e; }
if (except) {
fork_db.set_validity( *ritr, false );
auto applied_itr = ritr.base();
for( auto itr = applied_itr; itr != branches.first.end(); ++itr ) {
fork_db.mark_in_current_chain( *itr , false );
pop_block();
}
for( auto ritr = branches.second.rbegin(); ritr != branches.second.rend(); ++ritr ) {
apply_block( (*ritr)->block, controller::block_status::validated /* we previously validated these blocks*/ );
head = *ritr;
fork_db.mark_in_current_chain( *ritr, true );
}
throw *except;
}
}
- 切换分叉条件
上述切换第三种情况的发生是需要条件的,不是任意的分叉都能成为当前链,条件在fork_db add函数中head的选取,因为只有新的block成为head才会发生切换分叉
my->head = *my->index.get().begin();
by_lib_block_num这个索引的定义如下,可以看到head的选取是依次按照dpos_irreversible_blocknum从大到小,bft_irreversible_blocknum从大到小,block_num从大到小, bft_irreversible_blocknum目前可以忽视它永远是0,block_num表示块号,dpos_irreversible_blocknum表示当前不可逆块号
struct by_block_id;
struct by_block_num;
struct by_lib_block_num;
struct by_prev;
typedef multi_index_container<
block_state_ptr,
indexed_by<
hashed_unique< tag, member, std::hash>,
ordered_non_unique< tag, const_mem_fun >,
ordered_non_unique< tag,
composite_key< block_state,
member,
member
>,
// 从小到大,第一相同按第二个从大到小排序
composite_key_compare< std::less, std::greater >
>,
ordered_non_unique< tag,
composite_key< block_header_state,
member,
member,
member
>,
// 从大到小排序
composite_key_compare< std::greater, std::greater, std::greater >
>
>
> fork_multi_index_type;
dpos_irreversible_blocknum的由来
dpos_irreversible_blocknum不可逆块号,在block_header_state.cpp中result.dpos_irreversible_blocknum = result.calc_dpos_last_irreversible(); producer_to_last_implied_irb从小到大排列,然后返取1/3高度的值返回作为dpos_irreversible_blocknum
uint32_t block_header_state::calc_dpos_last_irreversible()const {
vector 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() );
// 取1/3高度的值返回作为dpos_irreversible_blocknum
return blocknums[ (blocknums.size()-1) / 3 ];
}
- producer_to_last_implied_irb
producer_to_last_implied_irb字典存放着每一个区块生产者见证的最新不可逆区块号,可以把它看成是由生产者确定的不可逆区块号候选名单,dpos_proposed_irreversible_blocknum就是prokey.producer_name生产者见证的最新不可逆区块号
result.producer_to_last_implied_irb[prokey.producer_name] = result.dpos_proposed_irreversible_blocknum
- dpos_proposed_irreversible_blocknum
dpos_proposed_irreversible_blocknum生产者区块的不可逆区块号,该值的计算在set_confirmed函数,num_prev_blocks参数示当前区块号cur_block_num距离当前生产者上一次生产的区块号last_block_num的距离,即cur_block_num - last_block_num + 1,该函数做的就是计算当前生产者见证的2/3高度的区块号
void block_header_state::set_confirmed( uint16_t num_prev_blocks ) {
header.confirmed = num_prev_blocks;
int32_t i = (int32_t)(confirm_count.size() - 1);
uint32_t blocks_to_confirm = num_prev_blocks + 1; /// confirm the head block too
while( i >= 0 && blocks_to_confirm ) {
--confirm_count[i];
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;
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;
}
}
- confirm_count数组
该数组记录每个区块已经被多少区块见证了,当得到2/3区块见证后就从数组中删除,具体请参考 eos源码解析(三):dpos共识源码